diff --git a/Dockerfile.worker b/Dockerfile.worker index fafa543..c146ea8 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -1,5 +1,6 @@ # CUDA 11.8 + Ubuntu 22.04 (cudnn 필요 없으면 -runtime- 만 사용) -#FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 +#FROM nvcr.io/nvidia/tensorrt:23.10-py3 + FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 # ---------- 시스템 의존성 (Pango/Cairo/HarfBuzz/Fonts 포함) ---------- diff --git a/Dockerfile.worker.tersorrt b/Dockerfile.worker.tersorrt new file mode 100644 index 0000000..24598b7 --- /dev/null +++ b/Dockerfile.worker.tersorrt @@ -0,0 +1,62 @@ +# CUDA 11.8 + Ubuntu 22.04 + cuDNN8 + TensorRT 8.6 + Python3.10 +FROM nvcr.io/nvidia/tensorrt:23.10-py3 + +#FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 + +# ---------- 시스템 의존성 (Pango/Cairo/HarfBuzz/Fonts 포함) ---------- + RUN apt-get update && apt-get install -y --no-install-recommends \ + python3.10 python3-pip python3.10-distutils \ + # OpenCV 런타임 + libgl1-mesa-glx libgl1-mesa-dri libglib2.0-0 libgomp1 \ + libsm6 libxext6 libxrender1 \ + cuda-cudart-dev-11-8 \ + # Pango / Cairo / HarfBuzz / GI 바인딩 + libcairo2 libcairo2-dev \ + libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 \ + libharfbuzz0b libharfbuzz-icu0 \ + gobject-introspection libgirepository1.0-dev \ + python3-gi python3-gi-cairo \ + # 글꼴과 폰트 설정 + fontconfig fonts-nanum fonts-noto-cjk fonts-dejavu-core \ + # 기타 + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# ---------- Python 기본 설정 ---------- +RUN ln -sf /usr/bin/python3.10 /usr/local/bin/python && \ + python -m pip install --upgrade pip + +WORKDIR /app + +# ---------- 파이썬 패키지 ---------- +COPY requirements_worker.txt . +COPY packages_worker/fastdeploy_gpu_python-1.0.7-cp310-cp310-manylinux1_x86_64.whl . +# constraints 복사 +COPY constraints.txt . + +RUN pip install --no-cache-dir --progress-bar off \ + --index-url https://download.pytorch.org/whl/cu118 \ + torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 +RUN pip install --no-cache-dir --progress-bar off -r requirements_worker.txt -c constraints.txt +RUN pip install --no-cache-dir --progress-bar off fastdeploy_gpu_python-1.0.7-cp310-cp310-manylinux1_x86_64.whl + +# ---------- 메모리 추척 ---------- +RUN pip install --no-cache-dir --progress-bar off pynvml + +# ---------- 프로젝트 파일 ---------- +COPY worker /app/worker +# COPY fonts /app/fonts + +ENV PYTHONUNBUFFERED=1 +ENV TEMP_STORAGE=/app/temp_files +RUN mkdir -p $TEMP_STORAGE + +# (선택) TensorRT 런타임이 정말 필요하면 NVIDIA 레포 추가 후 libnvinfer8 설치 필요. +# 없다면 FastDeploy가 자동으로 Paddle Inference로 폴백하니 생략해도 동작은 함. +# 성능 이유로 TRT를 쓰려면 별도 설치 가이드대로 세팅 권장. + +# ---------- Celery 엔트리포인트 ---------- +# ★ 중요: -A 경로를 실제 celery_app 이 있는 모듈:변수 로 맞추세요. +# 예) celery_worker.py 안에 celery_app = Celery(...) 라면: +CMD ["celery", "-A", "worker.celery_worker:celery_app", "worker", "-l", "info", "--concurrency=1"] + diff --git a/requirements_worker.txt b/requirements_worker.txt index a3a679f..8e222a6 100644 --- a/requirements_worker.txt +++ b/requirements_worker.txt @@ -31,10 +31,18 @@ requests==2.32.3 # loguru==0.7.2 # 사용시 python-dotenv==1.0.1 # .env 파싱시 -# onnxruntime +# ONNX Runtime (MIGAN 인페인팅용) onnxruntime-gpu==1.16.3 onnx==1.14.0 +# 🔥 MIGAN 인페인팅 추가 패키지 +# TensorRT (성능 최적화용 - NVIDIA GPU 환경) +# tensorrt>=8.6.0 # TensorRT 라이브러리 (별도 설치 필요) + +# 텍스트 렌더링 품질 개선 (Cairo/Pango 경고 해결) +pycairo>=1.20.0 # Cairo 그래픽 라이브러리 +PyGObject>=3.42.0 # GTK/Pango 바인딩 + # Background removal rembg[gpu]==2.0.67 diff --git a/test1/translated_result.png b/test1/translated_result.png index 03152f5..60f0771 100644 Binary files a/test1/translated_result.png and b/test1/translated_result.png differ diff --git a/test1/worker_test.py b/test1/worker_test.py index a665642..80eeb0c 100644 --- a/test1/worker_test.py +++ b/test1/worker_test.py @@ -37,14 +37,24 @@ unwanted_texts = { toggle_states = {"inpaint_method": "lama:cuda", "min_masks_for_lama": 2, 'title': False, 'title_shuffle': False, 'title_trans_type': False, 'collect_method_combo': '쇼핑API', 'ocr': True, 'unwanted_words': {'할인': '', '무료': '', '증정': '', '이벤트': '', '특가': '', '세일': '', '사은품': '', '보증': '', '품절': '', '행사': '', '할인가': '', '무료배송': '', '가격설명': ''}, 'interval': 3.0, 'watingTime': 20, 'memo': False, 'memo_toggle_exposer': False, 'memo_toggle_order': False, 'optionTrnas': True, 'optionTrnas_method': True, 'optionIMGTrans': True, 'optionIMGTrans_type': '자체서버', 'optionAutoSelect': True, 'price': False, 'tag': False, 'tag_ai': False, 'thumb': False, 'thumb_trans_type': 'CPU', 'thumb_nukki': False, 'remove_background_white': True, 'detail_Option': False, 'detail_IMGTrans': True, 'detail_IMGTrans_type': '자체서버', 'debug_mode': True, 'ed_mode': False, 'discord': False, 'is_localServer': False, 'watermark_toggle': False, 'clientID': '', 'clientSecret': '', 'discord_webhook': '', 'watermark_text': '', 'thumb_rmb_count': 3, 'max_option_count': 6, 'opacity_percent': 20, 'group_index': 4, 'remove_overprice': False, 'cat_rec': False, 'fixed_keywords': False, 'fixed_keywords_count': 2, 'title_length_limit': 27, 'base_dir': 'C:\\Program Files\\Edit_PartTimer\\lib\\src', 'TEMP_IMAGE_DIR': 'C:\\Program Files\\Edit_PartTimer\\lib\\src\\temp_images', 'ERROR_SCREENSHOT_DIR': 'C:\\Program Files\\Edit_PartTimer\\lib\\src\\error_screenshots', 'image_font_path': 'C:\\Program Files\\Edit_PartTimer\\lib\\src\\fonts\\HakgyoansimDunggeunmisoTTFB.ttf', 'watermark_font_path': 'C:\\Program Files\\Edit_PartTimer\\lib\\src\\fonts\\HakgyoansimDunggeunmisoTTFB.ttf', 'request_inpainting_server_url': 'http://171.101.232.45:50205', 'request_rembg_server_url': 'http://171.101.232.45:50205', 'request_rembg_server_url_local': 'http://192.168.0.150:35756', 'membership_level': 'premium', 'image_worker_restart_every': 10, 'image_worker_restart_count': 0, 'products_per_context_restart': 19, 'is_admin': False, 'admin_id': 'matia0514@naver.com', 'admin_pw': '', 'user_id': 'dreamm8985', 'user_pw': '112233', 'unwanted_words_button': False, 'font_type': '폰트5', 'cmb_button': False, 'detail_text_button': False, 'watermark': False} toggle_states.update({ - "use_roi_optimized_mask": True, # True: 새 방식, False: 기존 방식 - "enable_mask_refinement": False, # ROI 마스크 정제 비활성화 - "context_expansion_ratio": 0.4, # 최소 확장 - "blend_mode": "simple", # 단순 블렌딩 - "performance_mode": True, # 빠른 경로 사용 - "max_image_size": 1280, # 더 작은 크기 제한 - "roi_area_high": 0.0 # 기본값: 0.60 → 0.0으로 변경 # 풀프레임 인페인팅 강제 - + # 🔥 MIGAN 인페인팅 사용 설정 + "inpaint_method": "migan", # ROI → MIGAN 변경 + "migan_use_cuda": True, # CUDA 사용 + "migan_use_tensorrt": True, # TensorRT 최적화 + "migan_trt_fp16_enable": True, # FP16 활성화 + "migan_trt_engine_cache_enable": True, # 엔진 캐시 활성화 + "migan_max_image_size": 1600, # 최대 이미지 크기 + "migan_output_max_width": 800, # 출력 최대 가로 + "migan_enable_output_resize": True, # 출력 리사이즈 활성화 + + # 기존 ROI 설정들 (MIGAN에서는 사용되지 않지만 호환성 유지) + "use_roi_optimized_mask": True, # True: 새 방식, False: 기존 방식 + "enable_mask_refinement": False, # ROI 마스크 정제 비활성화 + "context_expansion_ratio": 0.4, # 최소 확장 + "blend_mode": "simple", # 단순 블렌딩 + "performance_mode": True, # 빠른 경로 사용 + "max_image_size": 1280, # 더 작은 크기 제한 + "roi_area_high": 0.0 # 기본값: 0.60 → 0.0으로 변경 # 풀프레임 인페인팅 강제 }) def call_translate(img_path: pathlib.Path): @@ -58,7 +68,8 @@ def call_translate(img_path: pathlib.Path): "toggle_states": json.dumps(toggle_states, ensure_ascii=False), "unwanted_texts": json.dumps(unwanted_texts, ensure_ascii=False), "ocr_method": "paddleocr", - "inpaint_method": "lama", + # "inpaint_method": "lama", # ← 이 줄을 제거하거나 + "inpaint_method": "migan", # ← migan으로 변경 "sync": "true", "wait": "90", }, diff --git a/worker/MIGAN_SETUP.md b/worker/MIGAN_SETUP.md new file mode 100644 index 0000000..ee91d32 --- /dev/null +++ b/worker/MIGAN_SETUP.md @@ -0,0 +1,306 @@ +# MIGAN 인페인팅 모듈 설정 안내 + +## 개요 +MIGAN ONNX 파이프라인을 사용한 전체 이미지 인페인팅 모듈이 추가되었습니다. 기존 ROI 기반 인페인팅과 함께 사용할 수 있으며, `toggle_states`를 통해 선택할 수 있습니다. + +## 모델 파일 설정 + +### 1. 모델 디렉토리 생성 +```bash +mkdir -p worker/models +``` + +### 2. MIGAN 모델 파일 배치 +`migan_pipeline_v2.onnx` 파일을 다음 경로에 배치하세요: +``` +worker/models/migan_pipeline_v2.onnx +``` + +### 3. TensorRT 캐시 디렉토리 (선택사항) +TensorRT 엔진 캐시를 위한 디렉토리: +```bash +mkdir -p /app/temp_files/trt_cache +``` + +## 사용 방법 + +### 인페인팅 방법 선택 +`toggle_states`에서 `inpaint_method` 키를 통해 인페인팅 방법을 선택할 수 있습니다: + +```python +# ROI 기반 인페인팅 (기본값) +toggle_states = { + "inpaint_method": "roi" +} + +# MIGAN 전체 이미지 인페인팅 +toggle_states = { + "inpaint_method": "migan" +} +``` + +### MIGAN 설정 옵션 + +#### 기본 설정 +```python +toggle_states = { + "inpaint_method": "migan", + "migan_onnx_path": "/app/worker/models/migan_pipeline_v2.onnx", + "migan_use_cuda": True, + "migan_use_tensorrt": True, + "migan_trt_fp16_enable": True, + "migan_trt_engine_cache_enable": True, + "migan_max_image_size": 1600, + "migan_output_max_width": 800, + "migan_enable_output_resize": True +} +``` + +#### 설정 옵션 설명 + +**ONNX 모델 설정:** +- `migan_onnx_path`: ONNX 모델 파일 경로 +- `migan_use_cuda`: CUDA 사용 여부 (기본: True) +- `migan_intra_threads`: ONNX Runtime 스레드 수 (기본: 0 = 자동) +- `migan_inter_threads`: ONNX Runtime 스레드 수 (기본: 0 = 자동) + +**TensorRT 최적화:** +- `migan_use_tensorrt`: TensorRT 사용 여부 (기본: True) +- `migan_trt_fp16_enable`: FP16 최적화 활성화 (기본: True) +- `migan_trt_engine_cache_enable`: 엔진 캐시 활성화 (기본: True) + +**이미지 처리:** +- `migan_max_image_size`: 최대 이미지 크기 제한 (긴 변 기준, 기본: 1600px) +- `migan_output_max_width`: 출력 최대 가로 크기 (기본: 800px) +- `migan_enable_output_resize`: 출력 크기 조정 활성화 (기본: True) + +#### 직접 모듈 사용 (개발/테스트용) +```python +from worker.migan_inpainting_module import MIGANInpaintingModule + +# ⚠️ 프로덕션에서는 celery_worker의 전역 인스턴스 사용 권장 +inpainter = MIGANInpaintingModule(config={ + 'use_cuda': True, + 'max_image_size': 1600 +}) + +# 인페인팅 실행 +result = inpainter.inpaint_with_migan(image, mask) +# cleanup_memory() 호출하지 말 것 - 전역 인스턴스는 계속 유지 +``` + +#### 전역 인스턴스 사용 (권장) +```python +# celery_worker.py에서 자동으로 처리됨 +# toggle_states만 설정하면 됨 + +toggle_states = { + "inpaint_method": "migan", + "migan_use_tensorrt": True, + "migan_trt_fp16_enable": True, + "migan_max_image_size": 2048 +} + +# worker가 자동으로 전역 인스턴스를 사용하고 설정을 동적 업데이트 +``` + +## 주요 특징 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +### MIGAN vs ROI 인페인팅 비교 + +| 항목 | MIGAN | ROI | +|------|-------|-----| +| 처리 방식 | 전체 이미지 | 영역별 분할 | +| 전처리 | 모델 내장 | 외부 처리 | +| ROI 분석 | 불필요 | 필수 | +| 메모리 사용 | 고정적 | 적응적 | +| 속도 | 일정 | 영역 수에 따라 변동 | +| 품질 | 일관적 | 영역별 최적화 | + +### MIGAN 모듈 장점 +1. **내장 전처리**: 리사이즈, 정규화 등이 ONNX 모델에 포함 +2. **TensorRT 최적화**: FP16, 엔진 캐시를 통한 성능 향상 +3. **세션 재사용**: 메모리 효율성 및 초기화 시간 단축 +4. **자동 폴백**: MIGAN 실패 시 ROI 방식으로 자동 전환 +5. **크기 제한**: 메모리 사용량 제어를 위한 이미지 크기 제한 +6. **🔥 전역 인스턴스**: 모델은 한 번만 로딩되어 메모리에 상주 +7. **🔥 동적 설정**: 실행 중 설정 변경 가능 (필요시 세션 재생성) + +## 환경 변수 + +```bash +# TensorRT 엔진 캐시 디렉토리 (선택사항) +export TRT_ENGINE_CACHE_DIR="/app/temp_files/trt_cache" + +# 디버그 아티팩트 저장 디렉토리 +export DEBUG_DUMP_DIR="/app/temp_files/debug" +``` + +## 로그 분석 + +### 정상 처리 로그 예시 +``` +[MIGAN] ONNX Runtime 세션 생성 완료: ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'] +[INPAINT] MIGAN 처리 통계: 마스크 커버리지 15.3%, 최대 크기 제한: 1600px +이미지 스케일링: 2000x1500 → 1600x1200 (factor=0.800) +MIGAN 인페인팅 완료: 총 1.234s (추론: 0.987s) +``` + +### 에러 및 폴백 로그 예시 +``` +[MIGAN] ONNX 파일을 찾을 수 없습니다: /app/worker/models/migan_pipeline_v2.onnx +[INPAINT] MIGAN 인페인팅 실패, ROI로 폴백: FileNotFoundError +``` + +## 메모리 관리 및 성능 + +### 전역 인스턴스 방식 +- **모델 로딩**: 워커 프로세스 시작 시 한 번만 로딩 +- **메모리 상주**: 모델이 메모리에 계속 유지되어 초기화 오버헤드 없음 +- **세션 재사용**: ONNX Runtime 세션이 캐시되어 반복 사용 +- **동적 설정**: 필요시에만 세션 재생성 (경로 변경 등) + +### 주의사항 +- `cleanup_memory()` 메소드를 전역 인스턴스에서 호출하지 마세요 +- 프로덕션 환경에서는 celery_worker의 전역 인스턴스를 사용하세요 +- 개발/테스트에서만 직접 인스턴스 생성을 사용하세요 + +## 성능 최적화 팁 + +1. **TensorRT 활용**: GPU가 있는 환경에서는 TensorRT를 활성화하여 성능 향상 +2. **엔진 캐시**: 첫 실행 후 생성되는 TensorRT 엔진을 캐시하여 재시작 시 빠른 로딩 +3. **크기 제한**: 메모리 부족 시 `migan_max_image_size`를 조정하여 처리 가능한 크기로 제한 +4. **출력 크기**: 클라이언트 요구사항에 맞춰 `migan_output_max_width` 조정 + +## 문제 해결 + +### 일반적인 문제들 + +1. **모델 파일 없음** + - 에러: `FileNotFoundError: ONNX 파일을 찾을 수 없습니다` + - 해결: `worker/models/migan_pipeline_v2.onnx` 파일 확인 + +2. **CUDA 메모리 부족** + - 에러: GPU 메모리 관련 오류 + - 해결: `migan_max_image_size` 값을 줄이거나 `migan_use_cuda: False`로 설정 + +3. **TensorRT 초기화 실패** + - 에러: TensorRT Provider 초기화 실패 + - 해결: `migan_use_tensorrt: False`로 설정하여 CUDA Provider만 사용 + +4. **세션 생성 실패** + - 에러: ONNX Runtime 세션 생성 실패 + - 해결: CPU 모드로 폴백 (`migan_use_cuda: False`) + +### 디버깅 + +1. **로그 레벨 조정**: 디버그 로그를 활성화하여 상세 정보 확인 +2. **아티팩트 저장**: 디버그 이미지를 저장하여 각 단계별 결과 확인 +3. **성능 모니터링**: GPU 메모리 사용량 및 처리 시간 모니터링 + +## 업데이트 및 확장 + +새로운 MIGAN 모델 버전을 사용하려면: +1. 새 ONNX 파일을 `worker/models/` 디렉토리에 배치 +2. `toggle_states`에서 `migan_onnx_path` 업데이트 +3. 필요시 입출력 형태에 맞춰 모듈 코드 수정 \ No newline at end of file diff --git a/worker/celery_worker.py b/worker/celery_worker.py index 292282b..f4f17a4 100644 --- a/worker/celery_worker.py +++ b/worker/celery_worker.py @@ -48,6 +48,7 @@ from worker.loggerModule import Logger # from worker.inpaint_module import Inpainter, InpaintBackends from worker.utils_debug import save_debug_artifacts, draw_ocr_overlay from worker.roi_inpainting_module import ROIInpaintingModule +from worker.migan_inpainting_module import MIGANInpaintingModule # from deep_translator import GoogleTranslator @@ -117,6 +118,7 @@ _mask: MaskModule | None = None _text: TextRenderingModule | None = None # _inpainter: Inpainter | None = None _roi_inpainter: ROIInpaintingModule | None = None +_migan_inpainter: MIGANInpaintingModule | None = None _translator = get_translator() # ✅ 워커 부팅 시 생성 & 재사용 # def get_lama(): @@ -170,6 +172,15 @@ def get_roi_inpainter(): _gpu_tracker.log_snapshot(tag="after ROI Inpainter init") return _roi_inpainter +def get_migan_inpainter(): + """MIGAN 인페인팅 모듈 초기화 및 반환""" + global _migan_inpainter + if _migan_inpainter is None: + _migan_inpainter = MIGANInpaintingModule(logger=clogger) + # MIGAN 인페인팅 초기화 직후 VRAM 스냅샷 + _gpu_tracker.log_snapshot(tag="after MIGAN Inpainter init") + return _migan_inpainter + # 번역기 워밍업 try: _ = _translator.translate_batch(["测试"], src="zh-CN", dest="ko") @@ -211,6 +222,9 @@ def _warm_up_models(**_): roi_inpainter = get_roi_inpainter() roi_inpainter._get_simple_lama() # SimpleLama 사전 로딩 + # 🔥 MIGAN 인페인팅 모듈 사전 초기화 (옵션) + get_migan_inpainter() # 필요 시 주석 해제 + logger.info("✅ 모델 사전 로딩 완료 (성능 최적화 포함)") except Exception as e: @@ -251,6 +265,54 @@ def _parse_font_number_from_toggle(toggle_states: Dict[str, Any]) -> int | None: logger.warning(f"[font] font_type 파싱 실패: {e}") return None +def _parse_inpaint_method(toggle_states: Dict[str, Any]) -> str: + """ + toggle_states에서 인페인팅 방법을 파싱 + + Returns: + 'roi': ROI 기반 인페인팅 (기본값) + 'migan': MIGAN 전체 이미지 인페인팅 + """ + method = str(toggle_states.get("inpaint_method", "roi")).lower().strip() + + # MIGAN 방식 키워드들 + migan_keywords = ["migan", "mi-gan", "full", "full_image", "onnx"] + + if any(keyword in method for keyword in migan_keywords): + return "migan" + + # 기본값은 ROI + return "roi" + +def _lightweight_memory_cleanup(trace_id: Optional[str] = None): + """ + 경량 메모리 정리 - 모델은 유지하고 중간 처리 데이터만 정리 + + Args: + trace_id: 트레이스 ID (로깅용) + """ + try: + import gc + import torch + + # Python 가비지 컬렉션 + collected = gc.collect() + + # GPU 캐시 정리 (모델은 유지됨) + gpu_freed = 0 + if torch.cuda.is_available(): + gpu_freed = torch.cuda.memory_allocated() / 1024 / 1024 # MB + torch.cuda.empty_cache() + gpu_freed = gpu_freed - (torch.cuda.memory_allocated() / 1024 / 1024) + + clogger.log( + f"[MEMORY][{trace_id or ''}] 경량 정리 완료: Python객체={collected}개, GPU캐시={gpu_freed:.1f}MB", + level=logging.DEBUG + ) + + except Exception as e: + clogger.log(f"[MEMORY][{trace_id or ''}] 메모리 정리 중 오류: {e}", level=logging.WARNING) + # def _parse_inpaint_backend( # toggle_states: Dict[str, Any], # *, @@ -570,41 +632,97 @@ def translate_task(self, *, image_b64: str, filename: str, with track_phase("INPAINT", trace_id): - # 🔥 ROI 기반 인페인팅 최적화 (모듈화) - roi_inpainter = get_roi_inpainter() + # 🔥 인페인팅 방법 선택 + inpaint_method = _parse_inpaint_method(toggle_states) + clogger.log(f"[TRACE][{trace_id}] 선택된 인페인팅 방법: {inpaint_method}", level=logging.INFO) - # ROI 처리 설정 (toggle_states에서 오버라이드 가능) - roi_config = { - 'min_component_area': toggle_states.get('min_component_area', 100), - 'merge_distance': toggle_states.get('merge_distance', 50), - 'margin_ratio': toggle_states.get('margin_ratio', 0.15), - 'large_mask_threshold': toggle_states.get('large_mask_threshold', 0.5), - # 🔥 마스크 정제 비활성화 (마스크 모듈에서 이미 최적화됨) - 'enable_mask_refinement': toggle_states.get('enable_mask_refinement', False), - 'mask_erosion_kernel': 0, # 비활성화 - 'mask_dilation_kernel': 0, # 비활성화 - 'mask_blur_kernel': 0, # 비활성화 - 'context_expansion_ratio': toggle_states.get('context_expansion_ratio', 0.1), # 줄임 - 'blend_mode': toggle_states.get('blend_mode', 'simple'), # 단순 블렌딩 - 'feather_blend_size': toggle_states.get('feather_blend_size', 5), # 줄임 - # 🔥 형상 최적화 설정 - 'enable_shape_optimization': toggle_states.get('enable_shape_optimization', True), - 'performance_tracking': toggle_states.get('performance_tracking', True), - } - - # 처리 전 통계 로깅 - stats = roi_inpainter.get_processing_stats(src_bgr, mask) - clogger.log( - f"[INPAINT] 처리 통계: {stats['num_components']}개 컴포넌트 → " - f"{stats['num_merged_rois']}개 ROI, 메모리 효율성: {stats['memory_efficiency']*100:.1f}%", - level=logging.INFO - ) - - # ROI 기반 인페인팅 실행 - dst_bgr = roi_inpainter.inpaint_with_roi(src_bgr, mask, config=roi_config) - - # 메모리 정리 - roi_inpainter.cleanup_memory() + if inpaint_method == "migan": + # 🔥 MIGAN 전체 이미지 인페인팅 + try: + migan_inpainter = get_migan_inpainter() # 전역 인스턴스 재사용 + + # MIGAN 설정 오버라이드 (toggle_states에서) + migan_config = { + 'onnx_path': toggle_states.get('migan_onnx_path', '/app/worker/models/migan_pipeline_v2.onnx'), + 'use_cuda': toggle_states.get('migan_use_cuda', True), + 'use_tensorrt': toggle_states.get('migan_use_tensorrt', True), + 'trt_fp16_enable': toggle_states.get('migan_trt_fp16_enable', True), + 'trt_engine_cache_enable': toggle_states.get('migan_trt_engine_cache_enable', True), + 'max_image_size': int(toggle_states.get('migan_max_image_size', 1600)), + 'output_max_width': int(toggle_states.get('migan_output_max_width', 800)), + 'enable_output_resize': toggle_states.get('migan_enable_output_resize', True), + } + + # 처리 전 통계 로깅 + stats = migan_inpainter.get_processing_stats(src_bgr, mask) + clogger.log( + f"[INPAINT] MIGAN 처리 통계: 마스크 커버리지 {stats['mask_coverage_ratio']*100:.1f}%, " + f"최대 크기 제한: {stats['max_size_limit']}px", + level=logging.INFO + ) + + # MIGAN 인페인팅 실행 (전역 인스턴스, 메모리 정리 안함) + dst_bgr = migan_inpainter.inpaint_with_migan(src_bgr, mask, config=migan_config) + + # 🔥 경량 메모리 정리 - 중간 처리 데이터만 정리, 모델은 유지 + _lightweight_memory_cleanup(trace_id) + + except Exception as e: + clogger.log(f"[INPAINT] MIGAN 인페인팅 실패, ROI로 폴백: {e}", level=logging.WARNING) + # MIGAN 실패 시 ROI로 폴백 + roi_inpainter = get_roi_inpainter() + roi_config = { + 'min_component_area': toggle_states.get('min_component_area', 100), + 'merge_distance': toggle_states.get('merge_distance', 50), + 'margin_ratio': toggle_states.get('margin_ratio', 0.15), + 'large_mask_threshold': toggle_states.get('large_mask_threshold', 0.5), + 'enable_mask_refinement': toggle_states.get('enable_mask_refinement', False), + 'context_expansion_ratio': toggle_states.get('context_expansion_ratio', 0.1), + 'blend_mode': toggle_states.get('blend_mode', 'simple'), + 'feather_blend_size': toggle_states.get('feather_blend_size', 5), + 'enable_shape_optimization': toggle_states.get('enable_shape_optimization', True), + 'performance_tracking': toggle_states.get('performance_tracking', True), + } + dst_bgr = roi_inpainter.inpaint_with_roi(src_bgr, mask, config=roi_config) + # 🔥 경량 메모리 정리 추가 + _lightweight_memory_cleanup(trace_id) + + else: + # 🔥 ROI 기반 인페인팅 (기본값) + roi_inpainter = get_roi_inpainter() + + # ROI 처리 설정 (toggle_states에서 오버라이드 가능) + roi_config = { + 'min_component_area': toggle_states.get('min_component_area', 100), + 'merge_distance': toggle_states.get('merge_distance', 50), + 'margin_ratio': toggle_states.get('margin_ratio', 0.15), + 'large_mask_threshold': toggle_states.get('large_mask_threshold', 0.5), + # 🔥 마스크 정제 비활성화 (마스크 모듈에서 이미 최적화됨) + 'enable_mask_refinement': toggle_states.get('enable_mask_refinement', False), + 'mask_erosion_kernel': 0, # 비활성화 + 'mask_dilation_kernel': 0, # 비활성화 + 'mask_blur_kernel': 0, # 비활성화 + 'context_expansion_ratio': toggle_states.get('context_expansion_ratio', 0.1), # 줄임 + 'blend_mode': toggle_states.get('blend_mode', 'simple'), # 단순 블렌딩 + 'feather_blend_size': toggle_states.get('feather_blend_size', 5), # 줄임 + # 🔥 형상 최적화 설정 + 'enable_shape_optimization': toggle_states.get('enable_shape_optimization', True), + 'performance_tracking': toggle_states.get('performance_tracking', True), + } + + # 처리 전 통계 로깅 + stats = roi_inpainter.get_processing_stats(src_bgr, mask) + clogger.log( + f"[INPAINT] ROI 처리 통계: {stats['num_components']}개 컴포넌트 → " + f"{stats['num_merged_rois']}개 ROI, 메모리 효율성: {stats['memory_efficiency']*100:.1f}%", + level=logging.INFO + ) + + # ROI 기반 인페인팅 실행 + dst_bgr = roi_inpainter.inpaint_with_roi(src_bgr, mask, config=roi_config) + + # 🔥 경량 메모리 정리 - 중간 처리 데이터만 정리, 모델은 유지 + _lightweight_memory_cleanup(trace_id) # 인페인트 후 (렌더 전) save_debug_artifacts(debug_dir, guid, inpaint=dst_bgr, _stage="INPAINT") diff --git a/worker/migan_inpainting_module.py b/worker/migan_inpainting_module.py new file mode 100644 index 0000000..135714a --- /dev/null +++ b/worker/migan_inpainting_module.py @@ -0,0 +1,585 @@ +# -*- coding: utf-8 -*- +""" +MIGAN ONNX 파이프라인 인페인팅 모듈 +- MIGAN ONNX 모델을 사용한 전체 이미지 인페인팅 +- TensorRT 최적화 지원 +- 세션 캐시 및 버퍼 재사용 +- 이미지 크기 제한 및 후처리 +""" + +import os +import sys +import time +import logging +import tempfile +from typing import Optional, Dict, Any, Tuple +from pathlib import Path + +import cv2 +import numpy as np +import onnxruntime as ort + +# OpenCV 내부 최적화 off (호환성) +cv2.setUseOptimized(False) + + +def _np_uint8_2d(arr, name="mask"): + """2D uint8 배열 검증 및 변환""" + if arr is None: + raise ValueError(f"{name} is None") + if not isinstance(arr, np.ndarray): + raise TypeError(f"{name} must be np.ndarray, got {type(arr)}") + if arr.ndim != 2: + raise ValueError(f"{name} must be 2D, got shape={arr.shape}") + if arr.dtype != np.uint8: + arr = arr.astype(np.uint8, copy=False) + return arr + + +def _ensure_logger(logger: Optional[object]) -> logging.Logger: + """로거 어댑터 생성""" + if logger and hasattr(logger, "log"): + return logger + + pylogger = logging.getLogger("MIGAN") + if not pylogger.handlers: + pylogger.setLevel(logging.DEBUG) + h = logging.StreamHandler(stream=sys.stdout) + h.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s] %(message)s")) + pylogger.addHandler(h) + + class _Adapter: + def __init__(self, _lg): + self._lg = _lg + def log(self, msg, level=logging.INFO, **kwargs): + self._lg.log(level, msg) + return _Adapter(pylogger) + + +class MIGANInpaintingModule: + """ + MIGAN ONNX 파이프라인 인페인팅 모듈 + - 전체 이미지 처리 (ROI 비활성화) + - TensorRT 최적화 지원 + - 세션 캐시 및 메모리 효율성 + """ + + _SESSION_CACHE = {} # 세션 캐시 + _TRT_ENGINE_CACHE_DIR = None # TensorRT 엔진 캐시 디렉토리 + + def __init__(self, logger=None, config: Dict[str, Any] = None): + """ + MIGAN 인페인팅 모듈 초기화 + + Args: + logger: 로깅 객체 + config: 설정 딕셔너리 + """ + self.logger = _ensure_logger(logger) + + # 기본 설정 + self.default_config = { + 'onnx_path': '/app/worker/models/migan_pipeline_v2.onnx', + 'use_cuda': True, + 'use_tensorrt': True, + 'trt_fp16_enable': True, + 'trt_engine_cache_enable': True, + 'intra_threads': 0, + 'inter_threads': 0, + 'max_image_size': 1600, # 최대 한 변 제한 + 'output_max_width': 800, # 출력 가로 최대 크기 + 'enable_output_resize': True, # 출력 리사이즈 활성화 + } + + if config: + self.default_config.update(config) + + # TensorRT 엔진 캐시 디렉토리 설정 + if self.default_config['trt_engine_cache_enable']: + self._setup_trt_cache_dir() + + self.session = None + self.input_names = None + self.output_names = None + + self.logger.log("MIGAN 인페인팅 모듈 초기화 완료", level=logging.INFO) + + def update_config(self, new_config: Dict[str, Any]): + """ + 설정을 동적으로 업데이트 + + Args: + new_config: 새로운 설정 딕셔너리 + """ + old_config = self.default_config.copy() + self.default_config.update(new_config) + + # 세션에 영향을 주는 설정이 변경되었는지 확인 + session_affecting_keys = { + 'onnx_path', 'use_cuda', 'use_tensorrt', + 'trt_fp16_enable', 'intra_threads', 'inter_threads' + } + + config_changed = any( + old_config.get(key) != self.default_config.get(key) + for key in session_affecting_keys + ) + + if config_changed: + self.logger.log("설정 변경으로 인한 세션 재초기화 필요", level=logging.INFO) + # 기존 세션 정보 리셋 (새로운 세션이 생성되도록) + self.session = None + self.input_names = None + self.output_names = None + + # TensorRT 캐시 디렉토리 업데이트 + if self.default_config['trt_engine_cache_enable']: + self._setup_trt_cache_dir() + + self.logger.log(f"설정 업데이트 완료: {list(new_config.keys())}", level=logging.INFO) + + def _setup_trt_cache_dir(self): + """TensorRT 엔진 캐시 디렉토리 설정""" + if self._TRT_ENGINE_CACHE_DIR is None: + cache_dir = os.getenv('TRT_ENGINE_CACHE_DIR', '/app/temp_files/trt_cache') + os.makedirs(cache_dir, exist_ok=True) + self._TRT_ENGINE_CACHE_DIR = cache_dir + self.logger.log(f"TensorRT 엔진 캐시 디렉토리: {cache_dir}", level=logging.INFO) + + def _get_session_key(self, config: Dict[str, Any]) -> tuple: + """세션 캐시 키 생성""" + return ( + config['onnx_path'], + config['use_cuda'], + config['use_tensorrt'], + config['trt_fp16_enable'], + config['intra_threads'], + config['inter_threads'] + ) + + def _create_session(self, config: Dict[str, Any]) -> ort.InferenceSession: + """ONNX Runtime 세션 생성""" + onnx_path = config['onnx_path'] + + if not os.path.exists(onnx_path): + raise FileNotFoundError(f"ONNX 파일을 찾을 수 없습니다: {onnx_path}") + + # 세션 옵션 설정 + session_options = ort.SessionOptions() + if config['intra_threads'] > 0: + session_options.intra_op_num_threads = config['intra_threads'] + if config['inter_threads'] > 0: + session_options.inter_op_num_threads = config['inter_threads'] + + # 최적화 레벨 설정 + session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + + # Provider 설정 + providers = [] + provider_options = [] + + if config['use_cuda']: + if config['use_tensorrt']: + # TensorRT Provider 설정 + trt_options = { + 'device_id': 0, + 'trt_fp16_enable': config['trt_fp16_enable'], + 'trt_engine_cache_enable': config['trt_engine_cache_enable'], + } + + if config['trt_engine_cache_enable'] and self._TRT_ENGINE_CACHE_DIR: + trt_options['trt_engine_cache_path'] = self._TRT_ENGINE_CACHE_DIR + + providers.append('TensorrtExecutionProvider') + provider_options.append(trt_options) + + self.logger.log(f"TensorRT 설정: FP16={config['trt_fp16_enable']}, 캐시={config['trt_engine_cache_enable']}", level=logging.INFO) + + # CUDA Provider + cuda_options = { + 'device_id': 0, + 'arena_extend_strategy': 'kNextPowerOfTwo', + 'gpu_mem_limit': 2 * 1024 * 1024 * 1024, # 2GB + 'cudnn_conv_algo_search': 'EXHAUSTIVE', + 'do_copy_in_default_stream': True, + } + providers.append('CUDAExecutionProvider') + provider_options.append(cuda_options) + + # CPU Provider (폴백) + providers.append('CPUExecutionProvider') + provider_options.append({}) + + try: + if provider_options: + session = ort.InferenceSession( + onnx_path, + sess_options=session_options, + providers=providers, + provider_options=provider_options + ) + else: + session = ort.InferenceSession( + onnx_path, + sess_options=session_options, + providers=providers + ) + + actual_providers = session.get_providers() + self.logger.log(f"ONNX Runtime 세션 생성 완료: {actual_providers}", level=logging.INFO) + + return session + + except Exception as e: + self.logger.log(f"GPU 세션 생성 실패, CPU로 폴백: {e}", level=logging.WARNING) + # CPU 전용 폴백 + session = ort.InferenceSession( + onnx_path, + sess_options=session_options, + providers=['CPUExecutionProvider'] + ) + self.logger.log("CPU 세션으로 폴백 완료", level=logging.INFO) + return session + + def _get_or_create_session(self, config: Dict[str, Any]) -> ort.InferenceSession: + """세션 가져오기 또는 생성 (캐시 활용)""" + cache_key = self._get_session_key(config) + + if cache_key in self._SESSION_CACHE: + self.logger.log("캐시된 ONNX 세션 재사용", level=logging.DEBUG) + return self._SESSION_CACHE[cache_key] + + session = self._create_session(config) + self._SESSION_CACHE[cache_key] = session + + return session + + def _prepare_session(self, config: Dict[str, Any]): + """세션 및 입출력 정보 준비""" + if self.session is None: + self.session = self._get_or_create_session(config) + + # 입출력 정보 저장 + inputs = self.session.get_inputs() + outputs = self.session.get_outputs() + + if len(inputs) != 2: + raise ValueError(f"MIGAN 모델은 2개의 입력이 필요합니다. 현재: {len(inputs)}개") + if len(outputs) != 1: + raise ValueError(f"MIGAN 모델은 1개의 출력이 필요합니다. 현재: {len(outputs)}개") + + self.input_names = [inp.name for inp in inputs] + self.output_names = [out.name for out in outputs] + + # 입출력 형태 로깅 + for i, inp in enumerate(inputs): + self.logger.log(f"입력 {i}: {inp.name}, 형태: {inp.shape}, 타입: {inp.type}", level=logging.DEBUG) + for i, out in enumerate(outputs): + self.logger.log(f"출력 {i}: {out.name}, 형태: {out.shape}, 타입: {out.type}", level=logging.DEBUG) + + def scale_image_if_needed(self, image: np.ndarray, mask: np.ndarray, + max_size: int) -> Tuple[np.ndarray, np.ndarray, Dict]: + """ + 이미지 크기 제한 적용 + + Args: + image: 입력 이미지 + mask: 입력 마스크 + max_size: 최대 크기 (긴 변 기준) + + Returns: + (스케일된 이미지, 스케일된 마스크, 스케일 정보) + """ + h, w = image.shape[:2] + max_dimension = max(h, w) + + if max_dimension <= max_size: + return image, mask, {'scaled': False, 'original_size': (h, w)} + + # 스케일 계산 + scale_factor = max_size / max_dimension + new_h = int(h * scale_factor) + new_w = int(w * scale_factor) + + # 8의 배수로 조정 (ONNX 모델 호환성) + new_h = ((new_h + 7) // 8) * 8 + new_w = ((new_w + 7) // 8) * 8 + + # 리사이즈 + scaled_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) + scaled_mask = cv2.resize(mask, (new_w, new_h), interpolation=cv2.INTER_NEAREST) + + self.logger.log( + f"이미지 스케일링: {w}x{h} → {new_w}x{new_h} (factor={scale_factor:.3f})", + level=logging.INFO + ) + + return scaled_image, scaled_mask, { + 'scaled': True, + 'original_size': (h, w), + 'scale_factor': scale_factor, + 'scaled_size': (new_h, new_w) + } + + def restore_original_scale(self, image: np.ndarray, scale_info: Dict) -> np.ndarray: + """ + 처리된 이미지를 원본 크기로 복원 + + Args: + image: 처리된 이미지 + scale_info: 스케일 정보 + + Returns: + 복원된 이미지 + """ + if not scale_info.get('scaled', False): + return image + + original_h, original_w = scale_info['original_size'] + restored = cv2.resize(image, (original_w, original_h), interpolation=cv2.INTER_CUBIC) + + self.logger.log( + f"이미지 복원: {image.shape[1]}x{image.shape[0]} → {original_w}x{original_h}", + level=logging.INFO + ) + + return restored + + def resize_output_if_needed(self, image: np.ndarray, max_width: int) -> np.ndarray: + """ + 출력 이미지 크기 조정 (가로 기준) + + Args: + image: 출력 이미지 + max_width: 최대 가로 크기 + + Returns: + 크기 조정된 이미지 + """ + h, w = image.shape[:2] + + if w <= max_width: + return image + + # 비율 유지하며 리사이즈 + scale_factor = max_width / w + new_w = max_width + new_h = int(h * scale_factor) + + resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) + + self.logger.log( + f"출력 리사이즈: {w}x{h} → {new_w}x{new_h} (max_width={max_width})", + level=logging.INFO + ) + + return resized + + def inpaint_with_migan(self, image: np.ndarray, mask: np.ndarray, + config: Dict[str, Any] = None) -> np.ndarray: + """ + MIGAN을 사용한 전체 이미지 인페인팅 + + Args: + image: 입력 이미지 (BGR) + mask: 마스크 (0~255, 텍스트영역=255) + config: 설정 오버라이드 + + Returns: + 인페인팅된 이미지 (BGR) + """ + start_time = time.time() + + # 🔥 동적 설정 업데이트 지원 + if config is not None: + self.update_config(config) + + effective_config = self.default_config + + try: + # 1. 세션 준비 + self._prepare_session(effective_config) + + # 2. 이미지 크기 제한 + max_size = effective_config['max_image_size'] + scaled_image, scaled_mask, scale_info = self.scale_image_if_needed( + image, mask, max_size + ) + + # 3. 임시 파일로 이미지 저장 (MIGAN 모듈이 파일 경로를 요구함) + with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file: + temp_path = tmp_file.name + cv2.imwrite(temp_path, scaled_image) + + try: + # 4. 마스크 전처리: 이진화 → 반전 (MIGAN 규칙에 맞춤) + mask_uint8 = _np_uint8_2d(scaled_mask, "mask") + _, mask_bin = cv2.threshold(mask_uint8, 128, 255, cv2.THRESH_BINARY) + mask_known255 = 255 - mask_bin # 텍스트영역(255) → hole(0) + + # 5. 이미지 로드 및 RGB 변환 + bgr = cv2.imread(temp_path, cv2.IMREAD_COLOR) + if bgr is None: + raise ValueError(f"임시 이미지 로드 실패: {temp_path}") + + rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + H, W = rgb.shape[:2] + + if mask_known255.shape != (H, W): + raise ValueError(f"마스크 크기 불일치: mask={mask_known255.shape}, img={(H,W)}") + + # 6. ONNX 추론을 위한 배치 차원 추가 및 차원 순서 변경 + inference_start = time.time() + + # 이미지: (H, W, 3) → (1, 3, H, W) + rgb_batch = np.expand_dims(rgb, 0).transpose(0, 3, 1, 2).astype(np.uint8) + + # 마스크: (H, W) → (1, 1, H, W) + mask_batch = np.expand_dims(mask_known255, (0, 1)).astype(np.uint8) + + self.logger.log( + f"추론 입력 - 이미지: {rgb_batch.shape}, 마스크: {mask_batch.shape}", + level=logging.DEBUG + ) + + # ONNX 추론 실행 + inputs = { + self.input_names[0]: rgb_batch, + self.input_names[1]: mask_batch + } + + outputs = self.session.run(self.output_names, inputs) + result = outputs[0] # (1, 3, H, W) 형태 예상 + + inference_time = time.time() - inference_start + + # 7. 출력 후처리: (1, 3, H, W) → (H, W, 3) + if result.ndim == 4 and result.shape[0] == 1: + result = result[0].transpose(1, 2, 0) # (1,3,H,W) → (H,W,3) + elif result.ndim == 3 and result.shape[0] == 3: + result = result.transpose(1, 2, 0) # (3,H,W) → (H,W,3) + + if not isinstance(result, np.ndarray) or result.ndim != 3: + raise ValueError(f"출력 형식 오류: shape={result.shape}, dtype={result.dtype}") + + # uint8 보장 + if result.dtype != np.uint8: + result = np.clip(result, 0, 255).astype(np.uint8) + + # 8. BGR 변환 + result_bgr = cv2.cvtColor(result, cv2.COLOR_RGB2BGR) + + # 9. 원본 크기로 복원 + if scale_info['scaled']: + result_bgr = self.restore_original_scale(result_bgr, scale_info) + + # 10. 출력 크기 조정 (옵션) + if effective_config.get('enable_output_resize', True): + max_width = effective_config.get('output_max_width', 800) + result_bgr = self.resize_output_if_needed(result_bgr, max_width) + + total_time = time.time() - start_time + + self.logger.log( + f"MIGAN 인페인팅 완료: 총 {total_time:.3f}s (추론: {inference_time:.3f}s)", + level=logging.INFO + ) + + return result_bgr + + finally: + # 임시 파일 정리 + try: + os.unlink(temp_path) + except: + pass + + except Exception as e: + self.logger.log(f"MIGAN 인페인팅 실패: {e}", level=logging.ERROR) + import traceback + self.logger.log(traceback.format_exc(), level=logging.DEBUG) + return image # 실패 시 원본 반환 + + def cleanup_memory(self): + """ + 🔥 경고: 전역 인스턴스에서는 이 메소드를 호출하지 마세요! + 메모리 정리 (개발/테스트 환경에서만 사용) + """ + self.logger.log("⚠️ cleanup_memory 호출됨 - 전역 인스턴스에서는 권장하지 않음", level=logging.WARNING) + import gc + gc.collect() + try: + # GPU 메모리 정리 + if hasattr(self, 'session') and self.session: + # ONNX Runtime은 자동으로 메모리를 관리하므로 특별한 정리 불필요 + pass + except Exception as e: + self.logger.log(f"메모리 정리 중 오류: {e}", level=logging.WARNING) + + self.logger.log("MIGAN 메모리 정리 완료", level=logging.INFO) + + def get_processing_stats(self, image: np.ndarray, mask: np.ndarray) -> Dict[str, Any]: + """ + MIGAN 처리 통계 정보 반환 + + Args: + image: 입력 이미지 + mask: 입력 마스크 + + Returns: + 처리 통계 딕셔너리 + """ + total_area = image.shape[0] * image.shape[1] + mask_area = np.sum(mask > 128) + + return { + 'total_image_size': total_area, + 'mask_area': mask_area, + 'mask_coverage_ratio': mask_area / total_area if total_area > 0 else 0.0, + 'processing_method': 'full_image_migan', + 'roi_processing': False, + 'max_size_limit': self.default_config['max_image_size'], + 'output_resize_enabled': self.default_config['enable_output_resize'] + } + + +# 편의 함수들 +def create_migan_inpainter(logger=None, config=None): + """MIGAN 인페인팅 모듈 팩토리 함수""" + return MIGANInpaintingModule(logger, config) + + +def build_migan_from_toggle(toggle_states: Dict[str, Any], logger=None) -> MIGANInpaintingModule: + """ + toggle_states로부터 설정을 읽어 MIGAN 인페인팅 모듈 생성 + + Args: + toggle_states: 설정 딕셔너리 + logger: 로거 객체 + + Returns: + MIGAN 인페인팅 모듈 인스턴스 + """ + config = { + 'onnx_path': toggle_states.get('migan_onnx_path', '/app/worker/models/migan_pipeline_v2.onnx'), + 'use_cuda': toggle_states.get('migan_use_cuda', True), + 'use_tensorrt': toggle_states.get('migan_use_tensorrt', True), + 'trt_fp16_enable': toggle_states.get('migan_trt_fp16_enable', True), + 'trt_engine_cache_enable': toggle_states.get('migan_trt_engine_cache_enable', True), + 'intra_threads': int(toggle_states.get('migan_intra_threads', 0) or 0), + 'inter_threads': int(toggle_states.get('migan_inter_threads', 0) or 0), + 'max_image_size': int(toggle_states.get('migan_max_image_size', 1600)), + 'output_max_width': int(toggle_states.get('migan_output_max_width', 800)), + 'enable_output_resize': toggle_states.get('migan_enable_output_resize', True), + } + + return MIGANInpaintingModule(logger, config) + + +def quick_migan_inpaint(image: np.ndarray, mask: np.ndarray, + logger=None, config=None) -> np.ndarray: + """간단한 MIGAN 인페인팅 수행""" + inpainter = create_migan_inpainter(logger, config) + result = inpainter.inpaint_with_migan(image, mask, config) + inpainter.cleanup_memory() + return result \ No newline at end of file diff --git a/worker/models/migan_pipeline_v2.onnx b/worker/models/migan_pipeline_v2.onnx new file mode 100644 index 0000000..3f75ade Binary files /dev/null and b/worker/models/migan_pipeline_v2.onnx differ diff --git a/worker/test_migan_example.py b/worker/test_migan_example.py new file mode 100644 index 0000000..6012401 --- /dev/null +++ b/worker/test_migan_example.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MIGAN 인페인팅 모듈 테스트 스크립트 + +이 스크립트는 MIGAN 모듈의 기본 사용법과 설정을 보여줍니다. +실제 운영 환경에서는 celery_worker.py를 통해 사용하세요. +""" + +import os +import sys +import numpy as np +import cv2 +from pathlib import Path + +# 현재 디렉토리를 Python 경로에 추가 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +try: + from migan_inpainting_module import MIGANInpaintingModule, build_migan_from_toggle, quick_migan_inpaint + from loggerModule import Logger +except ImportError as e: + print(f"모듈 import 실패: {e}") + print("worker 디렉토리에서 실행하거나 PYTHONPATH를 설정하세요.") + sys.exit(1) + + +def create_test_image_and_mask(): + """테스트용 이미지와 마스크 생성""" + # 테스트 이미지 생성 (800x600, 컬러) + image = np.random.randint(0, 255, (600, 800, 3), dtype=np.uint8) + + # 중앙에 텍스트 영역 시뮬레이션 (여러 개의 사각형) + mask = np.zeros((600, 800), dtype=np.uint8) + + # 텍스트 영역들 (마스크에서 255는 텍스트 영역) + rectangles = [ + (100, 100, 200, 50), # x, y, w, h + (300, 200, 150, 40), + (500, 300, 180, 60), + (200, 400, 220, 45), + ] + + for x, y, w, h in rectangles: + mask[y:y+h, x:x+w] = 255 + + return image, mask + + +def test_basic_usage(): + """기본 사용법 테스트""" + print("=== MIGAN 인페인팅 모듈 기본 테스트 ===") + + # 로거 생성 + logger = Logger() + + # 테스트 이미지 및 마스크 생성 + print("1. 테스트 이미지 및 마스크 생성...") + image, mask = create_test_image_and_mask() + print(f" 이미지 크기: {image.shape}") + print(f" 마스크 크기: {mask.shape}") + print(f" 마스크 영역: {np.sum(mask > 0)} 픽셀") + + # MIGAN 모듈 초기화 + print("\n2. MIGAN 모듈 초기화...") + try: + # 기본 설정으로 모듈 생성 + config = { + 'onnx_path': '/app/worker/models/migan_pipeline_v2.onnx', + 'use_cuda': False, # CPU 모드로 테스트 + 'use_tensorrt': False, + 'max_image_size': 1024, + 'output_max_width': 600, + } + + inpainter = MIGANInpaintingModule(logger=logger, config=config) + print(" MIGAN 모듈 초기화 성공") + + # 처리 통계 확인 + print("\n3. 처리 통계 분석...") + stats = inpainter.get_processing_stats(image, mask) + for key, value in stats.items(): + print(f" {key}: {value}") + + # 인페인팅 실행 (모델 파일이 없으면 실패할 것임) + print("\n4. MIGAN 인페인팅 실행...") + try: + result = inpainter.inpaint_with_migan(image, mask) + print(f" 인페인팅 성공! 결과 이미지 크기: {result.shape}") + except FileNotFoundError: + print(" ⚠️ 모델 파일을 찾을 수 없습니다: /app/worker/models/migan_pipeline_v2.onnx") + print(" 모델 파일을 올바른 위치에 배치한 후 다시 시도하세요.") + except Exception as e: + print(f" 인페인팅 실패: {e}") + + # 메모리 정리 + inpainter.cleanup_memory() + + except Exception as e: + print(f" MIGAN 모듈 초기화 실패: {e}") + + +def test_toggle_states_usage(): + """toggle_states를 사용한 설정 테스트""" + print("\n=== toggle_states 설정 테스트 ===") + + # 로거 생성 + logger = Logger() + + # toggle_states 설정 예시들 + test_configs = [ + { + "name": "CPU 모드", + "toggle_states": { + "inpaint_method": "migan", + "migan_use_cuda": False, + "migan_use_tensorrt": False, + "migan_max_image_size": 800, + "migan_output_max_width": 600, + } + }, + { + "name": "GPU 모드 (TensorRT 비활성화)", + "toggle_states": { + "inpaint_method": "migan", + "migan_use_cuda": True, + "migan_use_tensorrt": False, + "migan_max_image_size": 1600, + "migan_output_max_width": 800, + } + }, + { + "name": "최고 성능 모드 (TensorRT + FP16)", + "toggle_states": { + "inpaint_method": "migan", + "migan_use_cuda": True, + "migan_use_tensorrt": True, + "migan_trt_fp16_enable": True, + "migan_trt_engine_cache_enable": True, + "migan_max_image_size": 2048, + "migan_output_max_width": 1200, + } + } + ] + + for i, config in enumerate(test_configs, 1): + print(f"\n{i}. {config['name']} 테스트") + print(f" 설정: {config['toggle_states']}") + + try: + # toggle_states로부터 MIGAN 모듈 생성 + inpainter = build_migan_from_toggle(config['toggle_states'], logger) + print(" 모듈 생성 성공") + + # 설정 확인 + print(f" CUDA 사용: {inpainter.default_config['use_cuda']}") + print(f" TensorRT 사용: {inpainter.default_config['use_tensorrt']}") + print(f" 최대 이미지 크기: {inpainter.default_config['max_image_size']}") + + except Exception as e: + print(f" 모듈 생성 실패: {e}") + + +def test_quick_inpaint(): + """빠른 인페인팅 함수 테스트""" + print("\n=== 빠른 인페인팅 함수 테스트 ===") + + # 테스트 이미지 생성 + image, mask = create_test_image_and_mask() + + # 빠른 인페인팅 설정 + config = { + 'use_cuda': False, + 'use_tensorrt': False, + 'max_image_size': 512, + } + + print("1. quick_migan_inpaint 함수 실행...") + try: + result = quick_migan_inpaint(image, mask, config=config) + print(f" 성공! 결과 크기: {result.shape}") + except FileNotFoundError: + print(" ⚠️ 모델 파일을 찾을 수 없습니다.") + except Exception as e: + print(f" 실패: {e}") + + +def test_error_handling(): + """에러 처리 테스트""" + print("\n=== 에러 처리 테스트 ===") + + logger = Logger() + + # 잘못된 모델 경로로 테스트 + print("1. 잘못된 모델 경로 테스트...") + config = { + 'onnx_path': '/nonexistent/path/model.onnx', + 'use_cuda': False, + } + + try: + inpainter = MIGANInpaintingModule(logger=logger, config=config) + image, mask = create_test_image_and_mask() + result = inpainter.inpaint_with_migan(image, mask) + print(" 예상과 다르게 성공함") + except Exception as e: + print(f" 예상된 에러 발생: {type(e).__name__}") + + # 잘못된 입력 데이터 테스트 + print("\n2. 잘못된 입력 데이터 테스트...") + try: + config = {'use_cuda': False, 'use_tensorrt': False} + inpainter = MIGANInpaintingModule(logger=logger, config=config) + + # 잘못된 형태의 이미지/마스크 + bad_image = np.zeros((100, 100), dtype=np.uint8) # 2D 이미지 + bad_mask = np.zeros((50, 50), dtype=np.uint8) # 다른 크기 + + result = inpainter.inpaint_with_migan(bad_image, bad_mask) + print(" 예상과 다르게 성공함") + except Exception as e: + print(f" 예상된 에러 발생: {type(e).__name__}") + + +def show_usage_examples(): + """사용 예시 출력""" + print("\n=== 사용 예시 ===") + + print("\n1. Celery worker에서 ROI vs MIGAN 선택:") + print(""" +# ROI 기반 인페인팅 (기본) +toggle_states = { + "inpaint_method": "roi" +} + +# MIGAN 인페인팅 +toggle_states = { + "inpaint_method": "migan", + "migan_use_cuda": True, + "migan_trt_fp16_enable": True +} +""") + + print("\n2. 직접 MIGAN 모듈 사용:") + print(""" +from worker.migan_inpainting_module import MIGANInpaintingModule + +# 모듈 생성 +inpainter = MIGANInpaintingModule(config={ + 'use_cuda': True, + 'max_image_size': 1600 +}) + +# 인페인팅 실행 +result = inpainter.inpaint_with_migan(image, mask) +""") + + print("\n3. toggle_states로 모듈 생성:") + print(""" +from worker.migan_inpainting_module import build_migan_from_toggle + +toggle_states = { + "migan_use_tensorrt": True, + "migan_trt_fp16_enable": True, + "migan_max_image_size": 2048 +} + +inpainter = build_migan_from_toggle(toggle_states) +""") + + +def main(): + """메인 테스트 실행""" + print("MIGAN 인페인팅 모듈 테스트 시작") + print("=" * 50) + + # 환경 정보 출력 + print(f"Python 버전: {sys.version}") + print(f"작업 디렉토리: {os.getcwd()}") + print(f"NumPy 버전: {np.__version__}") + print(f"OpenCV 버전: {cv2.__version__}") + + # 모델 파일 존재 확인 + model_path = "/app/worker/models/migan_pipeline_v2.onnx" + if os.path.exists(model_path): + print(f"✅ 모델 파일 발견: {model_path}") + else: + print(f"⚠️ 모델 파일 없음: {model_path}") + print(" 모델 파일을 배치한 후 테스트하면 실제 인페인팅이 실행됩니다.") + + # 테스트 실행 + try: + test_basic_usage() + test_toggle_states_usage() + test_quick_inpaint() + test_error_handling() + show_usage_examples() + + except KeyboardInterrupt: + print("\n사용자에 의해 중단됨") + except Exception as e: + print(f"\n예상치 못한 에러: {e}") + import traceback + traceback.print_exc() + + print("\n" + "=" * 50) + print("테스트 완료") + + +if __name__ == "__main__": + main() \ No newline at end of file