wing-ops/prediction/image/optimization.md
jeonghyo.k 3946ff6a25 feat(prediction): 이미지 분석 서버 Docker 패키징 + DB 코드 제거
- prediction/image/ FastAPI 서버 Docker 환경 구성
  - Dockerfile: PyTorch 2.1 + CUDA 12.1 기반 GPU 이미지
  - docker-compose.yml: GPU 할당 + 데이터 볼륨 마운트
  - requirements.txt: 서버 의존성 목록
  - .env.example: 환경변수 템플릿
  - DOCKER_USAGE.md: 빌드/실행/API 사용법 문서
  - Dockerfile에 .dockerignore 제외 폴더 mkdir -p 추가
- .gitignore: prediction/image 결과물 및 모델 가중치(.pth) 제외 추가
- dbInsert_csv.py, dbInsert_shp.py 삭제 (미사용 DB 로직)
- api.py: dbInsert import 및 주석 처리된 DB 호출 코드 제거
- aerialRouter.ts: req.params 타입 오류 수정
2026-03-10 18:37:36 +09:00

135 lines
5.8 KiB
Markdown

# run-script API 성능 최적화 기록
## 결과 요약
| 단계 | 소요 시간 |
|------|----------|
| 최적화 전 | 35.12초 |
| 1차 최적화 후 | ~12초 |
| 2차 최적화 후 | ~8~10초 (예상) |
---
## 1차 최적화 (~35초 → ~12초)
### 문제 원인
| 위치 | 원인 |
|------|------|
| `Inference.py` 모듈 레벨 | `init_segmentor()` 호출 → 요청마다 GPU 모델 재로딩 (15~20초) |
| `api.py` | `subprocess.run()` 으로 `Combine_module.py` 실행 → 매 요청마다 Python 인터프리터 재시작 |
| `Combine_module.py` | Step1~4를 순차 subprocess 4개로 실행 |
| `Georeference.py` 내부 루프 | `np.ones((3,3), np.uint8)` dilate 커널을 루프마다 재생성 |
| `Oilshape.py pixel_to_geo()` | `rasterio.transform.xy()` 를 좌표 1개씩 루프로 호출 |
| `Inference.py` 내부 루프 | `palette_array = np.array(model.PALETTE)` 를 이미지마다 재생성 |
### 개선 내용
**`mx15hdi/Detect/Inference.py`**
- `load_model()` 함수 분리 — 모델 초기화를 서버 시작 시 1회로 분리
- `run_inference(model, file_id)` 함수화 — 사전 로드된 모델을 인자로 수신
- `palette_array` 루프 외부로 이동 (이미지마다 재생성 제거)
- `cv2.cvtColor` 제거 → numpy 슬라이싱 `color_mask[:, :, ::-1].copy()` 로 대체
**`mx15hdi/Metadata/Scripts/Export_Metadata_mx15hdi.py`**
- 모듈 레벨 `PaddleOCR()` 초기화 제거 → `_get_ocr_engine()` lazy 초기화로 변경
- `run_metadata_export(file_id)` 함수화 + 절대 경로(`_MX15HDI_DIR`) 기반으로 전환
- deprecated API 수정: `fillna(method='ffill')``ffill()` / `bfill()`
**`mx15hdi/Georeference/Scripts/Create_Georeferenced_Images_nadir.py`**
- `run_georeference(file_id)` 함수화 + 절대 경로 기반으로 전환
- `dilate_kernel = np.ones((3,3), np.uint8)` 루프 외부로 이동 (루프마다 재생성 제거)
**`mx15hdi/Polygon/Scripts/Oilshape.py`**
- `run_oilshape(file_id)` 함수화 + 절대 경로 기반으로 전환
- `pixel_to_geo()` 벡터화: 좌표 배열 일괄 처리 (`rasterio.transform.xy` 배열 입력)
**`mx15hdi/Main/Combine_module.py`**
- subprocess 4개 → 직접 함수 호출로 교체
- `run_pipeline(file_id, model=None)` 함수 추가
**`api.py`**
- FastAPI `lifespan` 이벤트로 서버 시작 시 모델 1회 로딩
- `ThreadPoolExecutor(max_workers=4)` 추가
- `_run_mx15hdi_pipeline()` 비동기 함수: Step1(추론) + Step2(메타데이터)를 `asyncio.gather`로 병렬 실행
---
## 2차 최적화 (~12초 → ~8~10초)
### 문제 원인
| 위치 | 원인 |
|------|------|
| `Inference.py` | `cv2.imread(image_path)` 로드 후 `inference_segmentor(model, image_path)` 에 경로 문자열 전달 → mmseg 내부에서 동일 이미지 재읽기 |
| `Inference.py``Georeference.py` | blended/mask를 `Detect/result/`, `Detect/Mask_result/` 에 저장 후 georeference에서 다시 읽음 (디스크 왕복 1) |
| `Georeference.py``Oilshape.py` | mask를 `Georeference/Mask_Tif/` 에 LZW 압축 GeoTIF로 저장 후 oilshape에서 다시 읽음 (디스크 왕복 2) |
| `Georeference.py` | `scipy.ndimage.binary_dilation` 사용 (Python 구현, OpenCV 대비 느림) |
| `Georeference.py` | rgb와 mask의 빈 픽셀 fill_mask를 중복 계산 |
### 개선 내용
**`mx15hdi/Detect/Inference.py`**
- `inference_segmentor(model, img_bgr)` — 경로 대신 배열 직접 전달 (이중 읽기 제거)
- `write_files: bool = False` 파라미터 추가 — 중간 파일 저장 선택적 처리
- `inference_cache` dict 반환: `{filename: {'blended': ndarray, 'mask': ndarray, 'ext': str}}`
**`mx15hdi/Georeference/Scripts/Create_Georeferenced_Images_nadir.py`**
- `inference_cache: dict = None` 파라미터 추가 — 있으면 메모리 배열 사용, 없으면 디스크 폴백
- `scipy.ndimage.binary_dilation` 제거 → `cv2.dilate` 로 통일
- rgb/mask 공통 `fill_mask` 재사용 — 중복 계산 제거
- Mask_Tif GeoTIF 디스크 저장 생략 — `georef_cache` 로 반환
- `georef_cache` dict 반환: `{filename: {'mask': ndarray, 'transform': ..., 'crs': ...}}`
**`mx15hdi/Polygon/Scripts/Oilshape.py`**
- `georef_cache: Optional[dict] = None` 파라미터 추가
- 있으면 메모리 배열 직접 처리, 없으면 `Mask_Tif/` 디스크 폴백
- `_process_mask_entry()` 헬퍼 함수 분리
**`api.py`**
- `_run_mx15hdi_pipeline()` 캐시 체이닝:
```python
inference_cache, _ = await asyncio.gather(
loop.run_in_executor(_executor, run_inference, _model, file_id),
loop.run_in_executor(_executor, run_metadata_export, file_id),
)
georef_cache = await loop.run_in_executor(
_executor, run_georeference, file_id, inference_cache
)
await loop.run_in_executor(_executor, run_oilshape, file_id, georef_cache)
```
### 디스크 I/O 흐름 변화
**최적화 전:**
```
Inference → Detect/result/ (write)
Detect/Mask_result/ (write)
Georeference ← Detect/result/ (read) ← 왕복 1
Detect/Mask_result/ (read)
Georeference → Georeference/Mask_Tif/ (write)
Oilshape ← Georeference/Mask_Tif/ (read) ← 왕복 2
```
**최적화 후:**
```
Inference → inference_cache (메모리)
Georeference ← inference_cache (메모리) ← 왕복 1 제거
Georeference → georef_cache (메모리)
Oilshape ← georef_cache (메모리) ← 왕복 2 제거
Georeference → Georeference/Tif/ (write) ← /get-image/ API 전용, 유지
```
---
## 수정 파일 목록
| 파일 | 1차 | 2차 |
|------|-----|-----|
| `mx15hdi/Detect/Inference.py` | ✅ | ✅ |
| `mx15hdi/Metadata/Scripts/Export_Metadata_mx15hdi.py` | ✅ | — |
| `mx15hdi/Georeference/Scripts/Create_Georeferenced_Images_nadir.py` | ✅ | ✅ |
| `mx15hdi/Polygon/Scripts/Oilshape.py` | ✅ | ✅ |
| `mx15hdi/Main/Combine_module.py` | ✅ | — |
| `api.py` | ✅ | ✅ |