- 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 타입 오류 수정
7.8 KiB
이미지 업로드 유류 분석 기능 구현 계획
Context
드론/항공 촬영 이미지를 업로드하면 AI 세그멘테이션으로 유류 확산 정보(위치·유종·면적·부피)를 자동 추출하고, 결과를 DB에 저장한 뒤 예측정보 입력 폼에 자동 채워주는 기능이다.
이미지 분석 서버(prediction/image/api.py, FastAPI, 포트 5001)는 이미 구현되어 있으며, 프론트↔백엔드↔이미지 분석 서버 연동 및 결과 자동 채우기를 구현한다.
전체 흐름
[프론트] 이미지 선택 → 분석 요청 버튼
↓ POST /api/prediction/image-analyze (multipart: image)
[백엔드]
├─ fileId = UUID 생성
├─ camTy = "mx15hdi" (하드코딩, 추후 이미지 EXIF 카메라 정보로 자동 판별 예정)
├─ 이미지 분석 서버로 전달 POST http://IMAGE_API_URL/run-script/
├─ 응답 파싱: meta(위경도 DMS→십진수 변환), data[0].classId→유종
├─ ACDNT INSERT (lat/lon/임시사고명)
├─ SPIL_DATA INSERT (유종/면적/img_rslt_data JSONB)
└─ 응답: { acdntSn, lat, lon, oilType, area, volume }
↓
[프론트] 폼 자동 채우기 (좌표·유종·유출량)
→ 사용자가 나머지 입력 후 "확산예측 실행"
구현 단계
Step 1 — DB 마이그레이션 (database/migration/017_spil_img_rslt.sql)
SPIL_DATA 테이블에 이미지 분석 결과 컬럼 추가.
ALTER TABLE wing.spil_data
ADD COLUMN IF NOT EXISTS img_rslt_data JSONB;
Step 2 — 백엔드: 이미지 분석 엔드포인트
파일: backend/src/prediction/predictionRouter.ts (라우트 등록)
신규 파일: backend/src/prediction/imageAnalyzeService.ts
엔드포인트
POST /api/prediction/image-analyze
Content-Type: multipart/form-data
Body: image (file)
imageAnalyzeService.ts 핵심 로직
// 1. fileId 생성 (crypto.randomUUID)
// 2. 이미지 분석 서버 호출
// camTy는 현재 "mx15hdi"로 하드코딩한다.
// TODO: 추후 이미지 EXIF에서 카메라 모델명을 읽어 camTy를 자동 판별하는 로직을
// 이미지 분석 서버(api.py)에 추가할 예정이다. (check_camera_info 함수 활용)
// FormData: { camTy: 'mx15hdi', fileId, image }
// → POST ${IMAGE_API_URL}/run-script/
// 응답: { meta: string, data: OilPolygon[] }
// 3. meta 문자열 파싱 (mx15hdi CSV 컬럼 순서 사용)
// [Filename, Tlat_d, Tlat_m, Tlat_s, Tlon_d, Tlon_m, Tlon_s, ...]
// DMS → 십진수: d + m/60 + s/3600
// 4. 유종 매핑 (data[0].classId → UI 유종명)
// classId → oilType: { '검정': '벙커C유', '갈색': '벙커C유', '무지개': '경유', '은색': '등유' }
// 5. ACDNT INSERT (임시 사고명 = "이미지분석_YYYY-MM-DD HH:mm", lat, lon, occurredAt = 촬영시각)
// 6. SPIL_DATA INSERT (acdntSn, matTyCd, matVol=data[0].volume, imgRsltData=JSON.stringify(response))
// 7. 반환
interface ImageAnalyzeResult {
acdntSn: number;
lat: number;
lon: number;
oilType: string; // UI 유종명 (벙커C유 등)
area: number; // m²
volume: number; // m³
fileId: string;
}
환경변수 추가 (backend/.env)
IMAGE_API_URL=http://localhost:5001
에러 처리
| 조건 | 응답 |
|---|---|
| 이미지에 GPS EXIF 없음 | 422 { error: 'GPS_NOT_FOUND' } |
| 이미지 서버 타임아웃(300s) | 504 |
Step 3 — 프론트엔드: API 서비스
파일: frontend/src/tabs/prediction/services/predictionApi.ts
interface ImageAnalyzeResult {
acdntSn: number;
lat: number;
lon: number;
oilType: string;
area: number;
volume: number;
fileId: string;
}
export const analyzeImage = async (
file: File
): Promise<ImageAnalyzeResult> => {
const formData = new FormData();
formData.append('image', file);
const { data } = await api.post<ImageAnalyzeResult>(
'/prediction/image-analyze',
formData,
{ headers: { 'Content-Type': 'multipart/form-data' }, timeout: 330000 }
);
return data;
};
Step 4 — 프론트엔드: Props 타입 확장
파일: frontend/src/tabs/prediction/components/leftPanelTypes.ts
// 기존 Props에 추가
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void;
Step 5 — 프론트엔드: PredictionInputSection 수정
파일: frontend/src/tabs/prediction/components/PredictionInputSection.tsx
변경 사항
-
"이미지 분석 실행" 버튼 (이미지 선택 후 활성화)
- 클릭 시
analyzeImage(file)호출 (camTy는 백엔드에서 처리) - 로딩 스피너 표시 (분석 소요시간 수십 초~수 분)
- 클릭 시
-
분석 결과 표시 (성공 시)
- "분석 완료: 위도 XX.XXXX / 경도 XXX.XXXX / 유종: OO" 요약 메시지
-
onImageAnalysisResult콜백 호출- 분석 성공 시 부모로 결과 전달
-
에러 처리
- GPS_NOT_FOUND: "GPS 정보가 없는 이미지입니다" 메시지 표시
- 타임아웃: "분석 서버 응답 없음" 메시지 표시
-
로컬 상태 교체:
uploadedImage(Base64 DataURL) 제거,uploadedFile: File | null로 교체
Step 6 — 프론트엔드: OilSpillView 결과 처리
파일: frontend/src/tabs/prediction/components/OilSpillView.tsx
const handleImageAnalysisResult = useCallback((result: ImageAnalyzeResult) => {
// 1. 사고 좌표 자동 채우기
setIncidentCoord({ lat: result.lat, lon: result.lon })
setFlyToCoord({ lat: result.lat, lon: result.lon })
// 2. 유종/유출량 자동 채우기
setOilType(result.oilType)
setSpillAmount(parseFloat(result.volume.toFixed(4)))
setSpillUnit('m³')
// 3. 분석 선택 상태 갱신 (acdntSn 연결 — 시뮬레이션 실행 시 기존 사고 사용)
setSelectedAnalysis({
acdntSn: result.acdntSn,
acdntNm: '',
// ... 나머지 기본값
})
}, [])
LeftPanel에 onImageAnalysisResult={handleImageAnalysisResult} 전달.
수정 대상 파일 요약
| 파일 | 변경 유형 |
|---|---|
database/migration/017_spil_img_rslt.sql |
신규 — SPIL_DATA 컬럼 추가 |
backend/src/prediction/imageAnalyzeService.ts |
신규 — 이미지 분석 서비스 |
backend/src/prediction/predictionRouter.ts |
수정 — 라우트 추가 |
backend/.env |
수정 — IMAGE_API_URL 추가 |
frontend/src/tabs/prediction/services/predictionApi.ts |
수정 — analyzeImage 함수 추가 |
frontend/src/tabs/prediction/components/leftPanelTypes.ts |
수정 — Props 타입 추가 |
frontend/src/tabs/prediction/components/PredictionInputSection.tsx |
수정 — 분석 실행 UI |
frontend/src/tabs/prediction/components/OilSpillView.tsx |
수정 — 결과 처리 핸들러 |
검증 방법
-
이미지 분석 서버 직접 테스트
curl -X POST http://localhost:5001/run-script/ \ -F "camTy=mx15hdi" -F "fileId=test001" -F "image=@drone_image.jpg" -
백엔드 엔드포인트 테스트
curl -X POST http://localhost:3001/api/prediction/image-analyze \ -F "image=@drone_image.jpg" \ -H "Cookie: <auth_cookie>"- 응답:
{ acdntSn, lat, lon, oilType, area, volume, fileId } - DB 확인: ACDNT, SPIL_DATA 레코드 생성 여부
- 응답:
-
프론트엔드 E2E 테스트
- 이미지 업로드 모드 선택 → GPS EXIF 있는 이미지 업로드 → "이미지 분석 실행" 클릭
- 로딩 표시 → 완료 시: 지도 이동, 유종/좌표 폼 자동 채워짐 확인
- 나머지 필드(예상시각·유출시간 등) 직접 입력 후 "확산예측 실행" → 정상 시뮬레이션 확인
-
에러 케이스 확인
- GPS 없는 이미지 → "GPS 정보가 없는 이미지입니다" 메시지