# Project Brief — prediction/image ## 1. 프로젝트 목적 항공/드론 카메라로 촬영된 해양 유류 오염 이미지를 자동 분석하여 유류 확산 정보를 추출·반환하는 **이미지 분석 백엔드 서비스**이다. - 드론(mx15hdi) 또는 열화상(starsafire) 카메라 이미지를 입력받아 - AI 세그멘테이션으로 유류 유형(검정/갈색/무지개/은색)을 탐지하고 - 지리참조(GeoTIFF) 변환 후 유류 면적·부피가 담긴 Shapefile을 생성하며 - 최종적으로 위치 메타데이터와 유류 분석 결과를 JSON으로 반환한다. 지원 카메라 타입: `mx15hdi` (EO/나디르 드론), `starsafire` (열화상 카메라) --- ## 2. 전체 처리 흐름 ``` 클라이언트 │ │ POST /run-script/ (camTy, fileId, image 파일) ▼ api.py ├─ GPS EXIF 검증 (check_gps_info) ├─ 이미지 저장 → {camTy}/Metadata/Image/Original_Images/{fileId}/ │ └─ subprocess: Combine_module.py {fileId} │ ├── [1] Detect/Inference.py AI 세그멘테이션 │ └─ 결과: Detect/result/{fileId}/ (블렌딩 이미지) │ Detect/Mask_result/{fileId}/ (컬러 마스크) │ ├── [2] Metadata/Scripts/Export_Metadata_mx15hdi.py │ └─ EXIF 추출 + 보간 저장 │ Metadata/CSV/{fileId}/mx15hdi.csv │ Metadata/CSV/{fileId}/mx15hdi_interpolation.csv │ ├── [3] Georeference/Scripts/Create_Georeferenced_Images_nadir.py │ └─ 핀홀 투영 → GeoTIFF 저장 │ Georeference/Tif/{fileId}/ (컬러 블렌딩 TIF) │ Georeference/Mask_Tif/{fileId}/ (마스크 TIF) │ └── [4] Polygon/Scripts/Oilshape.py └─ 마스크 TIF → 폴리곤 추출 → Shapefile 저장 Polygon/Shp/{fileId}/*.shp │ ├─ extract_data.get_metadata() → CSV에서 첫 번째 행 추출 └─ extract_data.get_oil_type() → Shapefile에서 유류 폴리곤 목록 추출 │ ▼ JSON 응답 { meta: "...", data: [...] } ``` --- ## 3. 주요 스크립트 설명 ### 3-1. `api.py` — FastAPI 서버 (포트 5001) 프로젝트의 진입점. 모든 엔드포인트를 정의하며 파이프라인 실행 및 결과 반환을 담당한다. **보조 함수** | 함수 | 설명 | |------|------| | `check_gps_info(image_path)` | 이미지 EXIF에서 GPS IFD 존재 여부 확인 | | `check_camera_info(image_file)` | 이미지 EXIF에서 카메라 모델명 추출 | --- ### 3-2. `extract_data.py` — 결과 데이터 추출 파이프라인 완료 후 CSV·Shapefile에서 결과를 읽어 API 응답에 포함시키는 유틸리티 모듈. **`get_metadata(camTy, fileId)`** - CSV 파일 경로를 카메라 타입에 따라 선택 - mx15hdi: `{camTy}/Metadata/CSV/{fileId}/mx15hdi_interpolation.csv` - starsafire: `{camTy}/Metadata/CSV/{fileId}/Metadata_Extracted.csv` - 헤더 이후 첫 번째 데이터 행을 쉼표로 이어 문자열 반환 **`get_oil_type(camTy, fileId)`** - `{camTy}/Polygon/Shp/{fileId}/*.shp` 에서 첫 번째 Shapefile 로드 - CRS를 EPSG:4326으로 변환 - 각 피처에서 `class_id`, `area_m2`, `volume_m3`, `note` 추출 - 유류 유형별 두께 매핑 적용 (mm → m 변환) - 1(검정/Emulsion): 1.0mm, 2(갈색/Crude): 0.1mm, 3(무지개): 0.0003mm, 4(은색): 0.0001mm - 반환값: `[{classId, area, volume, note, thickness, wkt}, ...]` --- ### 3-3. `dbInsert_csv.py` — CSV 메타데이터 DB 저장 현재 API에서는 주석 처리되어 있으며 독립 실행(CLI)으로도 사용 가능한 스크립트. - CSV에서 촬영 일시 및 대상 위경도(DMS → 십진수 변환)를 파싱 - `env_safe.unmnd_poll_info` 테이블에 INSERT - 카메라 타입에 따라 CSV 컬럼 파싱 방식이 다름 - mx15hdi: `Date1/Date2/Date3`, `Time1/Time2/Time3`, DMS 6필드 - starsafire: `Date`, `Time`, DMS 4필드(초 없음) --- ### 3-4. `dbInsert_shp.py` — Shapefile 유류 폴리곤 DB 저장 현재 API에서는 주석 처리되어 있으며 독립 실행(CLI)으로도 사용 가능한 스크립트. - Shapefile에서 유류 폴리곤 피처를 읽어 `env_safe.poll_mat_info` 테이블에 배치 INSERT - 폴리곤 지오메트리를 `ST_GeomFromText(wkt, 4326)`으로 저장 - 컬럼: `poll_id`, `algo_ty(유류명)`, `mat_ty(note)`, `mat_area`, `mat_thick`, `mat_vol`, `mat_geom` --- ### 3-5. `pic_gps.py` — 드론/폰 사진 스티칭 + GPS 보존 `/stitch` 엔드포인트가 호출하는 이미지 합성 전용 스크립트. CLI 독립 실행도 지원. - **스티칭 모드**: drone(SCANS 우선) / phone(PANORAMA 우선), 실패 시 폴백 가능 - **전처리**: 최대 해상도 리사이즈, CLAHE 대비 보정(`--enhance`) - **GPS 전략**: center(GPS 중앙값), first(첫 이미지 GPS), none - Haversine 공식으로 중심 좌표에서 가장 가까운 이미지 선택 - **출력**: GPS EXIF가 삽입된 JPEG 합성 이미지 --- ### 3-6. `mx15hdi/Main/Combine_module.py` — 파이프라인 오케스트레이터 `/run-script/` 엔드포인트가 subprocess로 실행하는 메인 파이프라인 스크립트. - 커맨드라인 인자: `fileId` - 다음 스크립트를 순서대로 실행: 1. `../Detect/Inference.py` 2. `../Metadata/Scripts/Export_Metadata_mx15hdi.py` 3. `../Georeference/Scripts/Create_Georeferenced_Images_nadir.py` 4. `../Polygon/Scripts/Oilshape.py` --- ### 3-7. `mx15hdi/Detect/Inference.py` — AI 세그멘테이션 추론 MMSegmentation 기반 유류 세그멘테이션 모델을 실행하는 스크립트. - 모델: `V7_SPECIAL.py` 설정 + `epoch_165.pth` 가중치, `cuda:0` 디바이스 - 분류 클래스: background, black, brown, rainbow, silver (5클래스) - 처리 흐름: 1. `Original_Images/{fileId}/` 내 이미지 파일 열거 2. `inference_segmentor()`로 세그멘테이션 맵 생성 3. 팔레트 기반 컬러 마스크 생성 (alpha=0.6 블렌딩) 4. 블렌딩 이미지 → `Detect/result/{fileId}/` 5. 컬러 마스크 이미지 → `Detect/Mask_result/{fileId}/` --- ### 3-8. `mx15hdi/Metadata/Scripts/Export_Metadata_mx15hdi.py` — 메타데이터 추출 이미지의 EXIF 정보를 파싱하여 CSV로 저장하는 스크립트. - **`meta_info.extract_and_save_image_metadata()`**: Pillow로 EXIF 읽기 - 촬영 시각(DateTimeOriginal), GPS(위도/경도/고도) 추출 - DMS 형식으로 변환하여 CSV 저장 (`mx15hdi.csv`) - 컬럼: `Filename`, `Tlat_d/m/s`, `Tlon_d/m/s`, `Alat_d/m/s`, `Alon_d/m/s`, `Az`, `El`, `Alt`, `Date1/2/3`, `Time1/2/3` - **`meta_info.geo_info()`**: OCR(PaddleOCR)로 이미지 HUD에서 메타데이터 추출 (현재 주석 처리) - **`meta_info.interpolation()`**: 결측값 전방/후방 보간 후 `mx15hdi_interpolation.csv` 저장 --- ### 3-9. `mx15hdi/Georeference/Scripts/Create_Georeferenced_Images_nadir.py` — 지리참조 변환 나디르(수직 하향) 촬영 이미지를 GeoTIFF로 변환하는 스크립트. - 카메라 파라미터: Hasselblad L2D-20c 기준 (초점거리 12.29mm, 4/3" 센서) - 처리 흐름: 1. `mx15hdi_interpolation.csv`에서 위경도(DMS) 및 고도(feet) 로드 2. EPSG:4326 → EPSG:5187(한국 TM 좌표계) 변환 3. 핀홀 카메라 투영으로 픽셀 → 지상 좌표 매핑 4. GSD(지상 샘플 거리) 계산 및 출력 해상도 결정 5. 블렌딩 이미지 → `Georeference/Tif/{fileId}/*_gsd.tif` 6. 마스크 이미지 → `Georeference/Mask_Tif/{fileId}/*_gsd.tif` - 결측 픽셀 보완: binary_dilation + cv2.dilate 2회 반복 --- ### 3-10. `mx15hdi/Polygon/Scripts/Oilshape.py` — 유류 폴리곤 생성 마스크 GeoTIFF에서 유류 영역을 폴리곤으로 추출하여 Shapefile로 저장하는 스크립트. - **`get_class_mask()`**: RGB 마스크를 유클리드 거리 기반으로 클래스 ID 마스크로 변환 - **`mask_to_polygons()`**: `cv2.findContours(RETR_CCOMP)`로 외곽선 추출, 홀(내부 폴리곤) 처리, `approxPolyDP`로 단순화 - **`save_polygons_to_shapefile()`**: - 픽셀 좌표 → 지리 좌표 변환 (`rasterio.transform.xy`) - Shapely Polygon 생성 + `.buffer(0)`으로 geometry 정규화 - 면적(m²), 두께(mm→m), 부피(m³), note 계산 후 GeoDataFrame 저장 - 출력: `Polygon/Shp/{fileId}/*.shp` --- ## 4. API 엔드포인트 상세 ### `POST /run-script/` 전체 분석 파이프라인을 실행하고 결과를 반환하는 메인 엔드포인트. **입력 (multipart/form-data)** | 파라미터 | 타입 | 필수 | 설명 | |----------|------|------|------| | `camTy` | string | ✅ | 카메라 타입. `"mx15hdi"` 또는 `"starsafire"` | | `fileId` | string | ✅ | 분석 세션 식별자 (파일 저장 폴더명으로 사용) | | `image` | file | ✅ | 분석할 이미지 파일 (PNG/JPG) | **처리 흐름** 1. `camTy` 유효성 검사 2. 이미지를 `{camTy}/Metadata/Image/Original_Images/{fileId}/`에 저장 3. GPS EXIF 검증 — GPS 없으면 `{"detail": "GPS Infomation Not Found"}` 반환 4. `Combine_module.py {fileId}` subprocess 실행 (타임아웃 300초) 5. `get_metadata()` + `get_oil_type()` 호출 **출력 (200 OK)** ```json { "meta": "filename,lat_d,lat_m,lat_s,...", "data": [ { "classId": "검정", "area": 152.3, "volume": 0.1523, "note": "Black - Emulsion", "thickness": 0.001, "wkt": "POLYGON ((...))" } ] } ``` **에러 응답** | 코드 | 조건 | |------|------| | 400 | camTy가 유효하지 않은 경우 | | 404 | Combine_module.py 파일이 없는 경우 | | 500 | subprocess 타임아웃 또는 기타 오류 | --- ### `GET /get-metadata/{camTy}/{fileId}` 이미 처리된 결과에서 메타데이터와 유류 정보를 조회하는 엔드포인트 (파이프라인 실행 없음). **입력 (Path Parameters)** | 파라미터 | 타입 | 설명 | |----------|------|------| | `camTy` | string | 카메라 타입 (`mx15hdi` / `starsafire`) | | `fileId` | string | 분석 세션 식별자 | **출력 (200 OK)** `/run-script/`와 동일한 응답 구조. ```json { "meta": "filename,lat_d,lat_m,...", "data": [ { "classId": "갈색", "area": ..., "wkt": "..." } ] } ``` --- ### `GET /get-original-image/{camTy}/{fileId}` 저장된 원본 이미지를 Base64 문자열로 반환하는 엔드포인트. **입력 (Path Parameters)** | 파라미터 | 타입 | 설명 | |----------|------|------| | `camTy` | string | 카메라 타입 | | `fileId` | string | 분석 세션 식별자 | **처리**: `{camTy}/Metadata/Image/Original_Images/{fileId}/` 디렉토리에서 `.png` 또는 `.jpg` 파일을 찾아 Base64 인코딩 **출력 (200 OK)** ``` "" ``` --- ### `GET /get-image/{camTy}/{fileId}` 지리참조된 GeoTIFF를 PNG로 변환하여 좌표 경계(Bounding Box)와 함께 반환하는 엔드포인트. **입력 (Path Parameters)** | 파라미터 | 타입 | 설명 | |----------|------|------| | `camTy` | string | 카메라 타입 | | `fileId` | string | 분석 세션 식별자 | **처리**: 1. `{camTy}/Georeference/Tif/{fileId}/*.tif` 파일 탐색 2. rasterio로 CRS 및 Bounds 추출 3. CRS가 EPSG:4326이 아닌 경우 pyproj로 변환 4. raster 데이터를 PNG로 변환 후 Base64 인코딩 **출력 (200 OK)** ```json { "minLon": 126.123456, "minLat": 34.123456, "maxLon": 126.234567, "maxLat": 34.234567, "image": "" } ``` --- ### `POST /stitch` 여러 장의 드론/폰 사진을 스티칭하여 합성 이미지를 반환하는 엔드포인트. **입력 (multipart/form-data)** | 파라미터 | 타입 | 필수 | 설명 | |----------|------|------|------| | `files` | file[] | ✅ | 합성할 이미지 파일 목록 (최소 2장) | | `fileId` | string | ✅ | 저장 디렉토리 식별자 | **처리 흐름** 1. 파일 수 최소 2장 검증 2. 각 파일의 카메라 모델명 추출 (`check_camera_info`) 3. `stitch/{fileId}/` 디렉토리에 파일 저장 (파일명 형식: `{model}_{idx:03d}_{원본파일명}`) 4. 가장 많이 나온 카메라 모델을 `--model` 인자로 사용 5. `pic_gps.py --mode drone --input ... --out ... --model ... --enhance` subprocess 실행 (타임아웃 300초) **출력 (200 OK)** 합성된 JPEG 이미지 파일 (`FileResponse`, `image/jpeg`) **에러 응답** | 코드 | 조건 | |------|------| | 400 | 이미지가 2장 미만인 경우 | | 500 | 스티칭 subprocess 실패 | --- ## 5. 디렉토리 구조 ``` prediction/image/ ├── api.py FastAPI 서버 (포트 5001) ├── extract_data.py 결과 데이터 추출 유틸리티 ├── dbInsert_csv.py CSV → PostgreSQL (현재 비활성화) ├── dbInsert_shp.py Shapefile → PostgreSQL (현재 비활성화) ├── pic_gps.py 이미지 스티칭 + GPS 보존 ├── mx15hdi/ mx15hdi 카메라 처리 모듈 │ ├── Main/ │ │ └── Combine_module.py 파이프라인 오케스트레이터 │ ├── Detect/ │ │ ├── Inference.py MMSeg AI 세그멘테이션 │ │ ├── V7_SPECIAL.py 모델 설정 │ │ ├── epoch_165.pth 학습된 모델 가중치 │ │ └── mmsegmentation/ MMSegmentation 라이브러리 │ ├── Metadata/ │ │ ├── Image/Original_Images/ 업로드 이미지 저장소 │ │ ├── CSV/ 메타데이터 CSV 출력 │ │ └── Scripts/ │ │ └── Export_Metadata_mx15hdi.py │ ├── Georeference/ │ │ ├── Tif/ 컬러 GeoTIFF 출력 │ │ ├── Mask_Tif/ 마스크 GeoTIFF 출력 │ │ └── Scripts/ │ │ └── Create_Georeferenced_Images_nadir.py │ ├── Polygon/ │ │ ├── Shp/ Shapefile 출력 │ │ └── Scripts/ │ │ └── Oilshape.py │ └── GSD/ GSD 중간 데이터 ├── starsafire/ starsafire 카메라 처리 모듈 (구조 동일) └── stitch/ 스티칭 결과 저장소 ``` --- ## 6. 주요 의존성 | 라이브러리 | 용도 | |------------|------| | fastapi, uvicorn | API 서버 | | rasterio, pyproj, osgeo(GDAL) | 지리참조·좌표 변환 | | geopandas, shapely | Shapefile 처리 | | mmsegmentation, torch | AI 세그멘테이션 | | paddleocr | HUD OCR (starsafire 전용) | | opencv-contrib-python | 이미지 처리·스티칭 | | Pillow, piexif | EXIF 메타데이터 | | psycopg2 | PostgreSQL 연결 | | pandas, numpy | 데이터 처리 |