diff --git a/README.md b/README.md index 847830a..3d01590 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,12 @@ FastAPI와 딥러닝을 활용한 병렬 처리 인페인팅 서버입니다. Si - **CUDA**: 11.8 - **cuDNN**: 8 - **TensorRT**: 8.6 +- **GCC**: 11 이상 (ONNX Runtime GPU 호환성용) - **RAM**: 4GB 이상 권장 - **저장공간**: 10GB 이상 +> **중요**: Jetson Xavier에서 GPU 가속을 위해서는 GCC 11과 특별한 ONNX Runtime 버전이 필요합니다. 자동 설치 스크립트가 이를 자동으로 처리합니다. + ### x86_64 시스템 - **OS**: Ubuntu 18.04 이상 - **Python**: 3.8 이상 (3.10 권장) @@ -104,34 +107,58 @@ cd inpaintServer chmod +x scripts/*.sh ``` -### 2. 원클릭 설치 및 실행 +### 2. 🚀 원클릭 자동 설치 (권장) -#### Jetson Xavier (ARM64) +#### 플랫폼 자동 감지 설치 ```bash -# Jetson 전용 설치 및 실행 -bash scripts/setup_and_run.sh +# 🎯 플랫폼 자동 감지 후 최적 설치 +bash scripts/install.sh ``` -#### x86_64 시스템 (RTX 3060 12GB 등) +#### 플랫폼별 직접 설치 + +**🚀 Jetson Xavier (ARM64):** ```bash -# x86 전용 설치 및 실행 +# Jetson Xavier 전용 최적화 설치 +bash scripts/setup_jetson.sh +``` + +**🖥️ x86-64 Desktop:** +```bash +# x86-64 데스크톱 최적화 설치 bash scripts/setup_x86.sh ``` -```bash -# 시스템 자동 감지 (권장) -./scripts/setup_and_run.sh +### 📁 가상환경 설정 방식 -# 또는 시스템별 지정 -./scripts/setup_and_run.sh --jetson # Jetson Xavier -./scripts/setup_and_run.sh --x86 # x86_64 시스템 +이 프로젝트는 **유연한 가상환경 설정**을 지원합니다: + +#### 방식 1: 표준 venv 디렉토리 (기본) +```bash +python3 -m venv venv +source venv/bin/activate ``` -**이 스크립트가 자동으로 수행하는 작업:** -- ✅ 가상환경 생성 (`python -m venv venv`) -- ✅ 의존성 설치 (`requirements.txt`) -- ✅ 모델 파일 확인 및 안내 -- ✅ 서버 시작 (메인 + 모니터링) +#### 방식 2: 프로젝트 자체를 가상환경으로 사용 +```bash +# 프로젝트 디렉토리에서 직접 가상환경 생성 +python3 -m venv . +source bin/activate +``` + +> **💡 스마트 감지**: 설치 스크립트가 자동으로 감지하고 처리합니다 +> - `venv/` 디렉토리 존재 → 표준 venv 사용 +> - `pyvenv.cfg` 파일 존재 → 프로젝트 자체가 가상환경 +> - 둘 다 없음 → 새로운 `venv/` 디렉토리 생성 + +**자동 설치가 수행하는 작업:** +- ✅ 플랫폼 자동 감지 (Jetson Xavier vs x86-64) +- ✅ 가상환경 자동 생성/감지 +- ✅ GPU 최적화 의존성 설치 +- ✅ ONNX Runtime GPU 설치 (플랫폼별) +- ✅ PyTorch CUDA 설치 +- ✅ 모델 호환성 확인 +- ✅ 설치 검증 및 테스트 ### 3. 수동 설치 (고급 사용자) @@ -638,6 +665,34 @@ ls -la app/models/onnx/ ### Jetson 전용 문제 +#### ONNX Runtime GPU 설치 (GLIBCXX 호환성 문제) + +Jetson Xavier에서 ONNX Runtime GPU를 사용하려면 특별한 절차가 필요합니다: + +```bash +# 1. GCC 11 업그레이드 (필수) +sudo apt install software-properties-common +sudo add-apt-repository ppa:ubuntu-toolchain-r/test +sudo apt update +sudo apt install gcc-11 g++-11 + +# 2. Jetson 전용 ONNX Runtime GPU 휠 다운로드 및 설치 +wget https://nvidia.box.com/shared/static/zostg6agm00fb6t5uisw51qi6kpcuwzd.whl \ + -O onnxruntime_gpu-1.17.0-cp38-cp38-linux_aarch64.whl +pip install --force-reinstall onnxruntime_gpu-1.17.0-cp38-cp38-linux_aarch64.whl + +# 3. 설치 확인 +python -c "import onnxruntime as ort; print('ONNX Runtime 버전:', ort.__version__); print('Available providers:', ort.get_available_providers())" +``` + +**예상 출력**: +``` +ONNX Runtime 버전: 1.17.0 +Available providers: ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'] +``` + +> **참고**: 자동 설치 스크립트를 사용하면 위 과정이 자동으로 수행됩니다. + #### 전력 모드 설정 ```bash # MAXN 모드로 설정 diff --git a/app/core/config.py b/app/core/config.py index 4b2acf7..26bbcb8 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -3,7 +3,7 @@ Configuration settings for the inpainting server """ import os import platform -from typing import Dict, Any +from typing import Dict, Any, Optional from pydantic_settings import BaseSettings @@ -17,9 +17,16 @@ class Settings(BaseSettings): PORT: int = 8008 WORKERS: int = 1 - # GPU settings + # GPU settings (Jetson Xavier 최적화) CUDA_DEVICE: int = 0 - FP16_ENABLED: bool = True + USE_CUDA: bool = True # CUDA 사용 여부 (Jetson에서 항상 True) + USE_FP16: bool = True # FP16 사용 여부 (Jetson 최적화) + FP16_ENABLED: bool = True # 기존 호환성 + + # ONNX Runtime 최적화 설정 + USE_TENSORRT: bool = True # TensorRT 사용 여부 (최고 성능) + TENSORRT_FP16: bool = True # TensorRT FP16 사용 + TENSORRT_WORKSPACE_SIZE: int = 2 * 1024 * 1024 * 1024 # 2GB # Jetson specific settings JETSON_MODE: bool = IS_JETSON @@ -27,29 +34,38 @@ class Settings(BaseSettings): JETSON_FAN_CONTROL: bool = True JETSON_TEMP_THRESHOLD: int = 75 # Celsius - # Session pool settings - SIMPLE_LAMA_SESSIONS: int = 2 if IS_JETSON else 4 - MIGAN_SESSIONS: int = 2 if IS_JETSON else 4 - REMBG_SESSIONS: int = 1 if IS_JETSON else 3 + # Session pool settings (Jetson Xavier는 32GB 통합 메모리로 더 많은 세션 가능) + SIMPLE_LAMA_SESSIONS: int = 4 if IS_JETSON else 2 # Jetson: 통합 32GB vs 데스크톱: VRAM 제한 + MIGAN_SESSIONS: int = 4 if IS_JETSON else 2 # Jetson이 더 많은 세션 운영 가능 + REMBG_SESSIONS: int = 3 if IS_JETSON else 1 # 메모리 공유 방식의 이점 활용 - # Worker settings (Jetson은 더 적은 워커 사용) - MAX_WORKERS: int = 6 if IS_JETSON else 16 - MIN_WORKERS: int = 2 if IS_JETSON else 8 + # Worker settings (Jetson은 통합 메모리로 더 효율적) + MAX_WORKERS: int = 8 if IS_JETSON else 6 # Jetson: 메모리 오버헤드 적음 + MIN_WORKERS: int = 4 if IS_JETSON else 2 # 통합 메모리 활용 WORKER_TIMEOUT: int = 120 # 2 minutes - # VRAM management (Jetson은 더 보수적인 설정) - VRAM_THRESHOLD_HIGH: float = 0.7 if IS_JETSON else 0.85 # 70% for Jetson, 85% for x86 - VRAM_THRESHOLD_LOW: float = 0.3 if IS_JETSON else 0.25 # 30% for Jetson, 25% for x86 - VRAM_CHECK_INTERVAL: int = 20 if IS_JETSON else 20 # More frequent for both + # 메모리 관리 (Jetson은 32GB 통합 메모리로 여유로움) + VRAM_THRESHOLD_HIGH: float = 0.85 if IS_JETSON else 0.75 # Jetson: 32GB 통합 메모리 + VRAM_THRESHOLD_LOW: float = 0.4 if IS_JETSON else 0.3 # 데스크톱: VRAM 제한 + VRAM_CHECK_INTERVAL: int = 30 if IS_JETSON else 15 # Jetson은 덜 자주 체크 # Model paths - SIMPLE_LAMA_MODEL_PATH: str = "models/simple-lama" - MIGAN_MODEL_PATH: str = "models/migan" - REMBG_MODEL_PATH: str = "models/rembg" + SIMPLE_LAMA_MODEL_PATH: str = "app/models/pt/big-lama.pt" + MIGAN_MODEL_PATH: str = "app/models/onnx/migan_pipeline_v2.onnx" + REMBG_MODEL_PATH: str = "app/models/onnx/birefnet-general-lite.onnx" - # Upload settings - MAX_FILE_SIZE: int = 25 * 1024 * 1024 if IS_JETSON else 100 * 1024 * 1024 # 25MB for Jetson, 100MB for x86 - MAX_IMAGE_SIZE: int = 2048 if IS_JETSON else 8192 # Maximum image dimension for Jetson, 8K for x86 + # MIGAN ONNX settings + MIGAN_ONNX_PATH: Optional[str] = "app/models/onnx/migan_pipeline_v2.onnx" # 커스텀 ONNX 파일 경로 + MIGAN_INTRA_THREADS: int = 0 + MIGAN_INTER_THREADS: int = 0 + + # REMBG settings (자동 다운로드 방식) + REMBG_MODEL_NAME: str = "birefnet-general-lite" # 고품질 경량 모델 + LOCAL_REMBG_MODEL_PATH: Optional[str] = None # 로컬 파일 사용 안함 + + # Upload settings (Jetson Xavier는 32GB 메모리로 대용량 처리 가능) + MAX_FILE_SIZE: int = 100 * 1024 * 1024 if IS_JETSON else 50 * 1024 * 1024 # Jetson: 100MB, 데스크톱: 50MB + MAX_IMAGE_SIZE: int = 4096 if IS_JETSON else 3072 # Jetson: 4K, 데스크톱: 3K (VRAM 고려) ALLOWED_EXTENSIONS: set = {".jpg", ".jpeg", ".png", ".bmp", ".tiff"} # Monitoring diff --git a/app/core/session_pool.py b/app/core/session_pool.py index d820677..5ebbadd 100644 --- a/app/core/session_pool.py +++ b/app/core/session_pool.py @@ -105,21 +105,62 @@ class SessionPool: async def _load_simple_lama_model(self): """Simple LAMA 모델을 로드합니다.""" - # Placeholder - 실제 모델 로딩 로직으로 대체 - await asyncio.sleep(0.1) # 시뮬레이션 - return {"model": "simple_lama", "loaded": True} + from ..models.simple_lama import SimpleLamaInpainter + from ..core.config import settings + + try: + model = SimpleLamaInpainter( + model_path=settings.SIMPLE_LAMA_MODEL_PATH, + device="cuda" if settings.USE_CUDA else "cpu", + fp16=settings.USE_FP16 + ) + await model.load_model() + logger.info("Simple LAMA 모델 세션 로드 완료") + return model + except Exception as e: + logger.error(f"Simple LAMA 모델 로드 실패: {e}") + raise async def _load_migan_model(self): """MIGAN 모델을 로드합니다.""" - # Placeholder - 실제 모델 로딩 로직으로 대체 - await asyncio.sleep(0.1) # 시뮬레이션 - return {"model": "migan", "loaded": True} + from ..models.migan import MiganInpainter + from ..core.config import settings + + try: + # MIGAN 모델 생성 - ONNX Runtime이 자동으로 CUDA 감지 + model = MiganInpainter( + model_path=getattr(settings, 'MIGAN_ONNX_PATH', settings.MIGAN_MODEL_PATH), + device="cuda" if settings.USE_CUDA else "cpu", + fp16=settings.USE_FP16, + use_cuda=settings.USE_CUDA + ) + await model.load_model() + logger.info("MIGAN 모델 세션 로드 완료") + return model + except Exception as e: + logger.error(f"MIGAN 모델 로드 실패: {e}") + raise async def _load_rembg_model(self): """REMBG 모델을 로드합니다.""" - # Placeholder - 실제 모델 로딩 로직으로 대체 - await asyncio.sleep(0.1) # 시뮬레이션 - return {"model": "rembg", "loaded": True} + from ..models.rembg_model import RembgProcessor + from ..core.config import settings + + try: + # RemBG 모델 생성 - 자동으로 CUDA 감지 + model = RembgProcessor( + model_name=getattr(settings, 'REMBG_MODEL_NAME', 'birefnet-general-lite'), + device="cuda" if settings.USE_CUDA else "cpu", + fp16=settings.USE_FP16, + local_rembg_model_path=getattr(settings, 'LOCAL_REMBG_MODEL_PATH', None) + ) + # 프리로드 강제: 실패 시 서버 기동 실패로 처리 (원인 파악을 위함) + await model.load_model() + logger.info("REMBG 모델 세션 로드 완료") + return model + except Exception as e: + logger.error(f"REMBG 모델 로드 실패: {e}") + raise @asynccontextmanager async def get_session(self, model_type: ModelType): @@ -240,5 +281,10 @@ class SessionPool: return status_by_model -# 전역 세션 풀 인스턴스 -session_pool = SessionPool() +# 전역 세션 풀 인스턴스 (설정값으로 초기화) +from ..core.config import settings +session_pool = SessionPool( + simple_lama_count=settings.SIMPLE_LAMA_SESSIONS, + migan_count=settings.MIGAN_SESSIONS, + rembg_count=settings.REMBG_SESSIONS +) diff --git a/app/core/worker_manager.py b/app/core/worker_manager.py index 1a8cefa..8073801 100644 --- a/app/core/worker_manager.py +++ b/app/core/worker_manager.py @@ -6,7 +6,7 @@ import asyncio import logging import time import uuid -from typing import Dict, List, Optional, Callable, Any +from typing import Dict, List, Optional, Callable, Any, Tuple from dataclasses import dataclass from enum import Enum from concurrent.futures import ThreadPoolExecutor @@ -330,49 +330,50 @@ class WorkerManager: async def process_inpaint(self, **kwargs) -> Optional[np.ndarray]: """인페인팅 작업을 처리합니다.""" try: - # 간단한 시뮬레이션 (실제로는 세션 풀에서 모델을 가져와 처리) - from ..models.simple_lama import SimpleLamaInpainter - from ..models.migan import MiganInpainter + from ..core.session_pool import session_pool, ModelType model_name = kwargs.get('model_name', 'simple-lama') + # 모델명에 따라 세션 타입 결정 if model_name == 'simple-lama': - model = SimpleLamaInpainter() + model_type = ModelType.SIMPLE_LAMA elif model_name == 'migan': - model = MiganInpainter() + model_type = ModelType.MIGAN else: - model = SimpleLamaInpainter() # 기본값 + model_type = ModelType.SIMPLE_LAMA # 기본값 - # 모델 처리 (실제로는 세션 풀에서 가져온 모델 사용) - result = await model.inpaint( - image=kwargs['image'], - mask=kwargs['mask'] - ) + # 세션 풀에서 모델 세션 가져와서 처리 + async with session_pool.get_session(model_type) as session: + # session.model 에서 실제 모델 객체의 메서드를 호출해야 함 + result = await session.model.inpaint( + image=kwargs['image'], + mask=kwargs['mask'] + ) return result except Exception as e: - logger.error(f"인페인팅 처리 실패: {e}") + logger.error(f"인페인팅 처리 실패: {e}", exc_info=True) return None - async def process_remove_bg(self, **kwargs) -> Optional[np.ndarray]: + async def process_remove_bg(self, **kwargs) -> Optional[Tuple[np.ndarray, np.ndarray]]: """배경 제거 작업을 처리합니다.""" try: - # 간단한 시뮬레이션 (실제로는 세션 풀에서 모델을 가져와 처리) - from ..models.rembg_model import RembgProcessor + from ..core.session_pool import session_pool, ModelType - model = RembgProcessor() - - # 모델 처리 (실제로는 세션 풀에서 가져온 모델 사용) - result = await model.remove_background( - image=kwargs['image'] - ) + # 세션 풀에서 REMBG 모델 세션 가져와서 처리 + async with session_pool.get_session(ModelType.REMBG) as session: + # session.model 에서 실제 모델 객체의 메서드를 호출해야 함 + result = await session.model.remove_background( + image=kwargs['image'], + model_name=kwargs.get('model_name', 'u2net') + ) return result except Exception as e: - logger.error(f"배경 제거 처리 실패: {e}") - return None + logger.error(f"배경 제거 처리 실패: {e}", exc_info=True) + return None, None # 전역 워커 매니저 인스턴스 diff --git a/app/models/migan.py b/app/models/migan.py index 052e32f..e05fd52 100644 --- a/app/models/migan.py +++ b/app/models/migan.py @@ -1,190 +1,285 @@ """ -MIGAN 인페인팅 모델 구현 +MIGAN ONNX 인페인팅 모델 구현 (실제 ONNX 파이프라인 사용) """ -import torch -import numpy as np -import cv2 -from PIL import Image +import os +import time import logging -from typing import Union, Tuple -import asyncio +from typing import Optional, Union +import cv2 +import numpy as np +import onnxruntime as ort +from PIL import Image + +# OpenCV 내부 최적화 off +cv2.setUseOptimized(False) logger = logging.getLogger(__name__) +def _np_uint8_2d(arr, name="mask"): + 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 + + class MiganInpainter: - def __init__(self, model_path: str = None, device: str = "cuda", fp16: bool = True): + """ + MIGAN ONNX 파이프라인 래퍼 + - 입력: image_path(str), mask(gray uint8 HxW) ※ 텍스트영역=255 + - 내부에서 mask를 (이진화→반전)하여 MI-GAN 규칙(255=known, 0=hole)으로 맞춤 + - 출력: BGR uint8(H,W,3) + """ + def __init__(self, + model_path: str = None, + device: str = "cuda", + fp16: bool = True, + use_cuda: bool = False, + intra_threads: int = 0, + inter_threads: int = 0): self.model_path = model_path self.device = device self.fp16 = fp16 - self.model = None + self.use_cuda = bool(use_cuda) + self.intra_threads = int(intra_threads or 0) + self.inter_threads = int(inter_threads or 0) self.loaded = False + if not model_path or not os.path.exists(model_path): + logger.error(f"MIGAN ONNX 파일을 찾을 수 없습니다: {model_path}") + raise FileNotFoundError(f"MIGAN ONNX 파일이 없습니다: {model_path}") + + self.session = None + self._session = None + self.in_image = None + self.in_mask = None + self.out_name = None + + async def _get_or_create_session(self): + """ONNX 런타임 세션을 생성하거나 기존 세션을 반환합니다.""" + if self._session is None: + try: + logger.info("MIGAN ONNX 런타임 세션 생성 시도...") + import onnxruntime as ort + + so = ort.SessionOptions() + if self.intra_threads > 0: + so.intra_op_num_threads = self.intra_threads + if self.inter_threads > 0: + so.inter_op_num_threads = self.inter_threads + + providers = [] + if self.use_cuda: + providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] + logger.info(f"MIGAN ONNX providers 설정: {providers}") + else: + providers = ['CPUExecutionProvider'] + logger.info("MIGAN ONNX CPU-only mode 로 설정") + + self._session = ort.InferenceSession( + self.model_path, + sess_options=so, + providers=providers + ) + logger.info(f"MIGAN ONNX 세션 생성 완료. Providers: {self._session.get_providers()}") + + except Exception as e: + logger.error(f"MIGAN ONNX 세션 초기화 실패: {e}", exc_info=True) + if 'ort' in locals(): + logger.error(f"사용 가능한 providers: {ort.get_available_providers()}") + raise RuntimeError(f"MIGAN 모델 초기화 실패: {e}") + + return self._session + async def load_model(self): """모델을 비동기적으로 로드합니다.""" if self.loaded: return try: - logger.info("Loading MIGAN model...") + logger.info("Loading MIGAN ONNX model...") - # 실제 구현에서는 MIGAN 모델을 로드 - # 여기서는 플레이스홀더로 구현 - await asyncio.sleep(0.1) # 모델 로딩 시뮬레이션 + self.session = await self._get_or_create_session() + ins = self.session.get_inputs() + outs = self.session.get_outputs() + self.in_image = ins[0].name + self.in_mask = ins[1].name + self.out_name = outs[0].name + + for i, inp in enumerate(ins): + logger.debug(f"MIGAN 입력 {i}: {inp.name}, 형태: {inp.shape}, 타입: {inp.type}") + for i, out in enumerate(outs): + logger.debug(f"MIGAN 출력 {i}: {out.name}, 형태: {out.shape}, 타입: {out.type}") + + logger.debug(f"MIGAN 세션 준비 완료. providers={self.session.get_providers()}") - # TODO: 실제 모델 로딩 로직 - # self.model = load_migan_model(self.model_path, device=self.device) - - self.model = {"type": "migan", "device": self.device, "fp16": self.fp16} self.loaded = True - - logger.info("MIGAN model loaded successfully") + logger.info("MIGAN ONNX model loaded successfully") except Exception as e: - logger.error(f"Failed to load MIGAN model: {e}") + logger.error(f"Failed to load MIGAN model: {e}", exc_info=True) raise - - def preprocess_image(self, image: Union[Image.Image, np.ndarray]) -> torch.Tensor: - """이미지를 전처리합니다.""" - if isinstance(image, Image.Image): - image = np.array(image) - - # RGB로 변환 - if image.shape[2] == 4: # RGBA - image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB) - elif image.shape[2] == 3 and image.dtype == np.uint8: - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - - # 크기 조정 (MIGAN은 특정 크기를 선호할 수 있음) - height, width = image.shape[:2] - if height != 512 or width != 512: - image = cv2.resize(image, (512, 512), interpolation=cv2.INTER_LANCZOS4) - - # 정규화 (-1 to 1, MIGAN 스타일) - image = image.astype(np.float32) / 127.5 - 1.0 - - # 텐서로 변환 (B, C, H, W) - tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0) - - if self.fp16: - tensor = tensor.half() - - return tensor.to(self.device) - - def preprocess_mask(self, mask: Union[Image.Image, np.ndarray]) -> torch.Tensor: - """마스크를 전처리합니다.""" - if isinstance(mask, Image.Image): - mask = np.array(mask) - - # 그레이스케일로 변환 - if len(mask.shape) == 3: - mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY) - - # 크기 조정 - if mask.shape[0] != 512 or mask.shape[1] != 512: - mask = cv2.resize(mask, (512, 512), interpolation=cv2.INTER_NEAREST) - - # 이진화 (0 또는 1) - mask = (mask > 127).astype(np.float32) - - # 텐서로 변환 (B, 1, H, W) - tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0) - - if self.fp16: - tensor = tensor.half() - - return tensor.to(self.device) - - def postprocess_result(self, tensor: torch.Tensor, original_size: Tuple[int, int]) -> np.ndarray: - """결과를 후처리합니다.""" - # CPU로 이동하고 numpy로 변환 - if tensor.is_cuda: - tensor = tensor.cpu() - if tensor.dtype == torch.float16: - tensor = tensor.float() - - result = tensor.squeeze(0).permute(1, 2, 0).numpy() - - # -1 to 1 범위에서 0-255로 변환 - result = ((result + 1.0) * 127.5).clip(0, 255).astype(np.uint8) - - # 원본 크기로 복원 - if result.shape[:2] != original_size: - result = cv2.resize(result, (original_size[1], original_size[0]), - interpolation=cv2.INTER_LANCZOS4) - - return result - - async def inpaint(self, image: Union[Image.Image, np.ndarray], + + async def inpaint(self, image: Union[str, Image.Image, np.ndarray], mask: Union[Image.Image, np.ndarray]) -> np.ndarray: - """인페인팅을 수행합니다.""" + """ + 인페인팅을 수행합니다. + + Args: + image: 원본 이미지 (파일 경로, PIL Image, 또는 numpy array) + mask: 마스크 (PIL Image 또는 numpy array, 텍스트영역=255) + + Returns: + 인페인팅된 이미지 (BGR numpy array) + """ if not self.loaded: await self.load_model() try: - # 원본 크기 저장 - if isinstance(image, Image.Image): - original_size = image.size[::-1] # (height, width) + # 1) 입력 이미지 로드 + if isinstance(image, str): + bgr = cv2.imread(image, cv2.IMREAD_COLOR) + if bgr is None: + logger.error(f"MIGAN 이미지 로드 실패: {image}") + return None + elif isinstance(image, Image.Image): + bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + elif isinstance(image, np.ndarray): + if image.shape[2] == 3: + bgr = image.copy() + else: + logger.error(f"MIGAN 지원하지 않는 이미지 형태: {image.shape}") + return None else: - original_size = image.shape[:2] + logger.error(f"MIGAN 지원하지 않는 이미지 타입: {type(image)}") + return None + + H, W = bgr.shape[:2] + + # 2) 마스크 정규화: (이진화 → 반전) 해서 255=known, 0=hole 맞추기 + if isinstance(mask, Image.Image): + mask_array = np.array(mask) + else: + mask_array = mask - # 전처리 - image_tensor = self.preprocess_image(image) - mask_tensor = self.preprocess_mask(mask) + mask_normalized = _np_uint8_2d(mask_array, name="mask") + if mask_normalized.shape != (H, W): + logger.error(f"MIGAN 마스크 크기 불일치: mask={mask_normalized.shape}, img={(H,W)}") + return None + + # 이진화: 128 threshold 기준 + _, mask_bin = cv2.threshold(mask_normalized, 128, 255, cv2.THRESH_BINARY) + # 마스크 반전: 텍스트영역 255 -> 0 (hole), 배경 0 -> 255 (known) + mask_known255 = 255 - mask_bin + + # 3) RGB 변환 (파이프라인 입력은 RGB uint8) + rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + + # 4) ONNX 추론 - 배치 차원 추가 및 차원 순서 변경 + start = time.time() + # ONNX 모델 입력 형태: + # - image: (1, 3, H, W) - 배치, 채널, 높이, 너비 순서 + # - mask: (1, 1, H, W) - 배치, 채널(1), 높이, 너비 순서 - # 추론 (실제 구현에서는 모델 추론) - with torch.no_grad(): - # TODO: 실제 모델 추론 로직 - # result = self.model(image_tensor, mask_tensor) - - # 플레이스홀더: 더 정교한 인페인팅 시뮬레이션 - result = await self._simulate_advanced_inpainting(image_tensor, mask_tensor) - - # 후처리 - result_np = self.postprocess_result(result, original_size) + # 이미지: (H, W, 3) -> (1, 3, H, W) + rgb_batch = np.expand_dims(rgb, 0).transpose(0, 3, 1, 2) - return result_np + # 마스크: (H, W) -> (1, 1, H, W) + mask_batch = np.expand_dims(mask_known255, (0, 1)) + logger.debug(f"MIGAN 입력 형태 - 이미지: {rgb_batch.shape}, 마스크: {mask_batch.shape}") + + out = self.session.run( + [self.out_name], + {self.in_image: rgb_batch, self.in_mask: mask_batch} + )[0] # expect RGB uint8(1,3,H,W) + + # 출력 차원 처리: (1,3,H,W) -> (H,W,3) + if out.ndim == 4 and out.shape[0] == 1: + out = out[0].transpose(1, 2, 0) # (1,3,H,W) -> (3,H,W) -> (H,W,3) + elif out.ndim == 3 and out.shape[0] == 3: # (3,H,W) -> (H,W,3) + out = out.transpose(1, 2, 0) + + logger.debug(f"MIGAN 출력 형태: {out.shape}, dtype: {out.dtype}") + + if not isinstance(out, np.ndarray) or out.ndim != 3 or out.dtype != np.uint8: + logger.error(f"MIGAN ONNX 출력 형식 오류: type={type(out)}, shape={getattr(out,'shape',None)}, dtype={getattr(out,'dtype',None)}") + return None + + elapsed = (time.time() - start) * 1000.0 + logger.debug(f"MIGAN 추론 완료: {elapsed:.2f} ms") + + # 5) BGR로 되돌려 반환 + bgr_out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR) + return bgr_out + except Exception as e: - logger.error(f"MIGAN inpainting failed: {e}") - raise - - async def _simulate_advanced_inpainting(self, image_tensor: torch.Tensor, - mask_tensor: torch.Tensor) -> torch.Tensor: - """고급 인페인팅 시뮬레이션 (실제 구현에서는 제거)""" - # 비동기 처리 시뮬레이션 - await asyncio.sleep(0.15) # MIGAN은 더 오래 걸린다고 가정 - - result = image_tensor.clone() - mask_bool = mask_tensor.bool() - - # 더 정교한 인페인팅 시뮬레이션: 주변 픽셀의 가중 평균 - if mask_bool.any(): - # 간단한 inpainting 시뮬레이션 - for c in range(3): - channel = result[0, c] - mask_2d = mask_bool[0, 0] - - # 마스크 영역의 경계에서 값을 가져와서 보간 - kernel = torch.ones(3, 3, device=self.device) / 9.0 - if self.fp16: - kernel = kernel.half() - - # 간단한 convolution 기반 인페인팅 - padded_channel = torch.nn.functional.pad(channel.unsqueeze(0).unsqueeze(0), (1, 1, 1, 1), mode='replicate') - smoothed = torch.nn.functional.conv2d(padded_channel, kernel.unsqueeze(0).unsqueeze(0), padding=0) - - result[0, c][mask_2d] = smoothed[0, 0][mask_2d] - - return result - + error_msg = str(e).lower() + if "invalid rank" in error_msg or "invalid argument" in error_msg: + logger.error(f"MIGAN ONNX 입력 차원 오류: {e}") + logger.error(f"MIGAN 입력 이미지 형태: {rgb_batch.shape if 'rgb_batch' in locals() else 'N/A'}") + logger.error(f"MIGAN 입력 마스크 형태: {mask_batch.shape if 'mask_batch' in locals() else 'N/A'}") + else: + logger.error(f"MIGAN inpaint 예외: {e}", exc_info=True) + return None + def get_model_info(self) -> dict: """모델 정보를 반환합니다.""" return { "model_type": "migan", + "model_path": self.model_path, "device": self.device, "fp16": self.fp16, + "use_cuda": self.use_cuda, "loaded": self.loaded, - "model_path": self.model_path, - "input_size": (512, 512) + "providers": self.session.get_providers() if self.session else None } + + +# 편의 함수: 설정으로부터 MIGAN 인스턴스 생성 +def build_migan_from_config(config: dict, logger: Optional[object] = None, gpu_manager: Optional[object] = None) -> MiganInpainter: + """ + 설정으로부터 MiganInpainter 인스턴스를 생성. + 필수 키: + - migan_onnx_path + 선택 키: + - migan_use_cuda (bool) + - migan_intra_threads (int) + - migan_inter_threads (int) + """ + onnx_path = config.get("migan_onnx_path", "") + if not onnx_path: + raise ValueError("config['migan_onnx_path'] 가 필요합니다.") + use_cuda = bool(config.get("migan_use_cuda", False)) + intra = int(config.get("migan_intra_threads", 0) or 0) + inter = int(config.get("migan_inter_threads", 0) or 0) + + inpainter = MiganInpainter( + model_path=onnx_path, + device="cuda" if use_cuda else "cpu", + fp16=False, # ONNX에서는 fp16 사용하지 않음 + use_cuda=use_cuda, + intra_threads=intra, + inter_threads=inter, + ) + + # GPU 관리자를 인페인터 객체에 연결 + if gpu_manager: + inpainter.gpu_manager = gpu_manager + if logger: + logger.log(f"MIGAN GPU 관리자 연결 완료: {type(gpu_manager).__name__}", level=logging.DEBUG) + else: + if logger: + logger.log(f"MIGAN GPU 관리자 없음: gpu_manager={gpu_manager}", level=logging.DEBUG) + + # 디버깅: gpu_manager 속성 확인 + if logger: + logger.log(f"MIGAN 인페인터 gpu_manager 속성: {hasattr(inpainter, 'gpu_manager')}, 값: {getattr(inpainter, 'gpu_manager', None)}", level=logging.DEBUG) + + return inpainter \ No newline at end of file diff --git a/app/models/rembg_model.py b/app/models/rembg_model.py index a7fe3fc..f834c99 100644 --- a/app/models/rembg_model.py +++ b/app/models/rembg_model.py @@ -1,25 +1,139 @@ """ -REMBG 배경 제거 모델 구현 +REMBG 배경 제거 모델 구현 (실제 rembg 라이브러리 사용) """ -import torch -import numpy as np +import os import cv2 from PIL import Image import logging -from typing import Union, Tuple +import numpy as np +import onnxruntime # ONNX 런타임 직접 사용을 위해 임포트 +from typing import Union, Tuple, Optional import asyncio logger = logging.getLogger(__name__) class RembgProcessor: - def __init__(self, model_name: str = "u2net", device: str = "cuda", fp16: bool = True): + """ + rembg 기반 배경제거 모듈 (안전한 의존성 처리) + """ + + # 사용하시려는 birefnet 모델을 지원 목록에 추가합니다. + SUPPORTED_MODELS = { + "u2net": "범용 배경제거 | 빠름 | 사람/사물 모두 양호 (기본값)", + "u2netp": "u2net 경량화 | 매우 빠름 | 실시간, 저사양PC", + "u2net_human_seg": "인물 전용 | 빠름 | 사람 경계 정밀", + "u2net_cloth_seg": "옷 전용 | 빠름 | 패션/의류 특화", + "isnet-general-use": "범용 고품질 | 느림 | 디테일 중시, 대용량", + "sam": "SAM 최고 품질 | 매우 느림 | 고성능PC 권장", + "sam-mobile": "SAM 경량화 | 보통 | 모바일, 중간성능", + "birefnet-general-lite": "BiRefNet 경량 모델 | 고품질 저용량 (로컬)" + } + + # SUPPORTED_MODELS 키와 실제 rembg sessions 키 간의 매핑 + MODEL_NAME_MAPPING = { + "u2net": "u2net", + "u2netp": "u2netp", + "u2net_human_seg": "u2net-human-seg", + "u2net_cloth_seg": "u2net-cloth-seg", + "isnet-general-use": "dis-general-use", + "sam": "sam", + "sam-mobile": "sam", # sam-mobile은 sam과 동일하게 처리 + "birefnet-general-lite": "birefnet-general-lite" + } + + def __init__(self, model_name: str = "u2net", device: str = "cuda", fp16: bool = True, + local_rembg_model_path: str = None): self.model_name = model_name self.device = device self.fp16 = fp16 - self.model = None + self.local_rembg_model_path = local_rembg_model_path + self.sessions = {} self.loaded = False + self._rembg_available = None + self._init_error = None + self._cuda_providers_tested = False + + def _check_rembg_availability(self): + """rembg 모듈 사용 가능 여부를 확인하고 캐시. + 세션을 생성하지 않아 모델 다운로드를 유발하지 않도록 함.""" + if self._rembg_available is not None: + return self._rembg_available + + try: + import rembg # noqa: F401 + self._rembg_available = True + logger.info("rembg 모듈 임포트 성공 (세션 생성은 지연 로딩)") + return True + except ImportError as e: + self._init_error = f"rembg 모듈이 설치되지 않음: {e}" + self._rembg_available = False + except Exception as e: + self._init_error = f"rembg 모듈 초기화 실패 (의존성/하드웨어 문제): {e}" + self._rembg_available = False + + logger.error(self._init_error) + return False + def get_session(self, model_name, timeout_seconds: int = 90): + """ + 모델별 세션을 캐싱하여 반환 (로컬 모델 경로 및 CUDA 지원 포함) + """ + if not self._check_rembg_availability(): + logger.error(f"rembg 사용 불가로 세션 생성 실패: {self._init_error}") + return None + + # device 설정에 따라 CUDA 사용 여부 결정 (간소화) + cuda_enabled = self.device == "cuda" + # 실제 모델명을 세션 키에 사용 + actual_model_name = self.MODEL_NAME_MAPPING.get(model_name, model_name) + session_key = f"{actual_model_name}_cuda_{cuda_enabled}" + + if session_key not in self.sessions: + logger.info(f"🔧 rembg 새 세션 생성 필요: {session_key}") + try: + import rembg + try: + from rembg.sessions import sessions + except ImportError: + # rembg 버전에 따라 import 경로가 다를 수 있음 + sessions = None + logger.warning("rembg.sessions import 실패, 기본 방식 사용") + + # Jetson 환경에서 TensorRT 충돌을 피하기 위해 프로바이더 명시 + providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] + logger.info(f"rembg 세션 생성 providers: {providers}") + + session = rembg.new_session( + model_name=actual_model_name, + providers=providers + ) + + self.sessions[session_key] = session + + # 실제 사용된 provider 확인 및 로깅 (가드 처리) + actual_providers = [] + try: + inner = getattr(session, 'inner_session', None) + if inner and hasattr(inner, 'get_providers'): + actual_providers = inner.get_providers() or [] + except Exception as prov_err: + logger.debug(f"rembg provider 확인 실패: {prov_err}") + + is_gpu = any(('CUDA' in p) or ('Tensorrt' in p) for p in actual_providers) + status = "GPU 가속" if is_gpu else "CPU 모드" + logger.info( + f"✅ rembg '{actual_model_name}' {status}로 동작 (providers: {actual_providers or '알 수 없음'})" + ) + + except Exception as e: + logger.error(f"rembg 세션 생성 실패 ('{actual_model_name}'): {e}", exc_info=True) + return None + else: + logger.debug(f"♻️ rembg 기존 세션 재사용: {session_key}") + + return self.sessions.get(session_key) + async def load_model(self): """모델을 비동기적으로 로드합니다.""" if self.loaded: @@ -28,141 +142,141 @@ class RembgProcessor: try: logger.info(f"Loading REMBG model ({self.model_name})...") - # 실제 구현에서는 rembg 라이브러리를 사용 - # 여기서는 플레이스홀더로 구현 - await asyncio.sleep(0.1) # 모델 로딩 시뮬레이션 + # rembg 사용 가능성 확인 + if not self._check_rembg_availability(): + raise RuntimeError(f"REMBG 사용 불가: {self._init_error}") - # TODO: 실제 모델 로딩 로직 - # from rembg import new_session - # self.model = new_session(self.model_name) + # 세션 생성 + session = self.get_session(self.model_name) + if session is None: + raise RuntimeError(f"REMBG 세션 생성 실패: {self.model_name}") - self.model = { - "type": "rembg", - "model_name": self.model_name, - "device": self.device, - "fp16": self.fp16 - } self.loaded = True - logger.info(f"REMBG model ({self.model_name}) loaded successfully") except Exception as e: logger.error(f"Failed to load REMBG model: {e}") raise - - def preprocess_image(self, image: Union[Image.Image, np.ndarray]) -> np.ndarray: - """이미지를 전처리합니다.""" - if isinstance(image, Image.Image): - image = np.array(image) - - # RGB로 변환 - if image.shape[2] == 4: # RGBA - image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB) - elif len(image.shape) == 3 and image.shape[2] == 3: - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - - return image + + def to_white_background(self, img: Image.Image) -> Image.Image: + """RGBA 이미지를 흰 배경으로 변환""" + if img.mode in ("RGBA", "BGRA"): + bg = Image.new("RGB", img.size, (255, 255, 255)) + bg.paste(img, mask=img.split()[-1]) + return bg + else: + return img.convert("RGB") + + async def remove_background(self, image: Union[str, Image.Image, np.ndarray], + model_name: str = None, **kwargs) -> Tuple[np.ndarray, np.ndarray]: + """ + 배경을 제거하고 결과 이미지와 마스크를 반환합니다. - def create_mask_from_alpha(self, rgba_image: np.ndarray) -> np.ndarray: - """RGBA 이미지에서 알파 채널을 마스크로 변환합니다.""" - if rgba_image.shape[2] != 4: - raise ValueError("Input image must have 4 channels (RGBA)") + Args: + image: 입력 이미지 (파일 경로, PIL Image, 또는 numpy array) + model_name: 사용할 모델명 (없으면 기본 모델 사용) + **kwargs: 추가 옵션 (alpha_matting 등) - # 알파 채널을 마스크로 사용 - alpha_channel = rgba_image[:, :, 3] - - # 0-255 범위의 마스크 생성 - mask = alpha_channel.astype(np.uint8) - - return mask - - async def remove_background(self, image: Union[Image.Image, np.ndarray]) -> Tuple[np.ndarray, np.ndarray]: - """배경을 제거하고 결과 이미지와 마스크를 반환합니다.""" + Returns: + (result_rgb, mask): 결과 이미지(RGB)와 마스크 + """ if not self.loaded: await self.load_model() try: - # 전처리 - processed_image = self.preprocess_image(image) - original_shape = processed_image.shape + # 이미지 로드 및 변환 + if isinstance(image, str): + if not os.path.exists(image): + logger.error(f"입력 이미지가 존재하지 않습니다: {image}") + return None, None + img = cv2.imread(image) + if img is None: + logger.error(f"이미지 로드 실패: {image}") + return None, None + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + elif isinstance(image, Image.Image): + img_rgb = np.array(image.convert('RGB')) + elif isinstance(image, np.ndarray): + if len(image.shape) == 3 and image.shape[2] == 3: + # BGR to RGB + img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + else: + img_rgb = image + else: + logger.error(f"지원하지 않는 이미지 타입: {type(image)}") + return None, None + + # 사용할 모델명 결정 + effective_model_name = model_name or self.model_name - # 배경 제거 (실제 구현에서는 rembg 사용) - # TODO: 실제 모델 추론 로직 - # from rembg import remove - # result_rgba = remove(self.model, processed_image) + if effective_model_name not in self.SUPPORTED_MODELS: + logger.warning(f"지원하지 않는 모델명: {effective_model_name}. u2net으로 대체 사용") + effective_model_name = "u2net" + + session = self.get_session(effective_model_name) + if session is None: + return None, None + + import rembg + import time - # 플레이스홀더: 배경 제거 시뮬레이션 - result_rgba = await self._simulate_background_removal(processed_image) + start_time = time.time() + result = rembg.remove(img_rgb, session=session, alpha_matting=kwargs.get("alpha_matting", False)) + end_time = time.time() - # 결과에서 RGB 이미지와 마스크 분리 - result_rgb = result_rgba[:, :, :3] - mask = self.create_mask_from_alpha(result_rgba) + if not isinstance(result, Image.Image): + result = Image.fromarray(result) + + # RGBA 이미지에서 RGB와 마스크 분리 + result_rgba = np.array(result) + if result_rgba.shape[2] == 4: + result_rgb = result_rgba[:, :, :3] + mask = result_rgba[:, :, 3] + else: + result_rgb = result_rgba + # 간단한 마스크 생성 (배경이 검은색이라고 가정) + gray = cv2.cvtColor(result_rgb, cv2.COLOR_RGB2GRAY) + mask = (gray > 10).astype(np.uint8) * 255 + + processing_time = end_time - start_time + # provider 기반 상태 로깅 (세션에서 확인 시도) + try: + sess = self.sessions.get(f"{self.MODEL_NAME_MAPPING.get(effective_model_name, effective_model_name)}_cuda_{self.device == 'cuda'}") + providers = [] + if sess and getattr(sess, 'inner_session', None) and hasattr(sess.inner_session, 'get_providers'): + providers = sess.inner_session.get_providers() or [] + cuda_status = "CUDA" if any('CUDA' in p or 'Tensorrt' in p for p in providers) else "CPU" + except Exception: + cuda_status = "알 수 없음" + logger.info(f"✅ 배경 제거 성공: {effective_model_name} ({cuda_status}, {processing_time:.2f}초)") return result_rgb, mask except Exception as e: - logger.error(f"Background removal failed: {e}") - raise - - async def _simulate_background_removal(self, image: np.ndarray) -> np.ndarray: - """배경 제거 시뮬레이션 (실제 구현에서는 제거)""" - # 비동기 처리 시뮬레이션 - await asyncio.sleep(0.08) # REMBG는 상대적으로 빠르다고 가정 - - height, width = image.shape[:2] - - # 간단한 전경/배경 분리 시뮬레이션 - # 중앙 영역을 전경으로, 가장자리를 배경으로 가정 - center_x, center_y = width // 2, height // 2 - - # 타원형 마스크 생성 - y, x = np.ogrid[:height, :width] - mask = ((x - center_x) ** 2 / (width * 0.3) ** 2 + - (y - center_y) ** 2 / (height * 0.4) ** 2) <= 1 - - # 부드러운 가장자리를 위한 가우시안 블러 - mask_float = mask.astype(np.float32) - mask_blurred = cv2.GaussianBlur(mask_float, (51, 51), 20) - - # RGBA 이미지 생성 - result_rgba = np.zeros((height, width, 4), dtype=np.uint8) - result_rgba[:, :, :3] = image # RGB 채널 - result_rgba[:, :, 3] = (mask_blurred * 255).astype(np.uint8) # 알파 채널 - - return result_rgba - - async def apply_new_background(self, foreground: np.ndarray, mask: np.ndarray, - background: Union[np.ndarray, tuple]) -> np.ndarray: - """새로운 배경을 적용합니다.""" - try: - height, width = foreground.shape[:2] - - # 배경 준비 - if isinstance(background, tuple): - # 단색 배경 - bg = np.full((height, width, 3), background, dtype=np.uint8) - else: - # 이미지 배경 - if isinstance(background, Image.Image): - background = np.array(background) - bg = cv2.resize(background, (width, height)) - if len(bg.shape) == 3 and bg.shape[2] == 4: - bg = bg[:, :, :3] # RGBA에서 RGB로 - - # 마스크를 0-1 범위로 정규화 - mask_norm = mask.astype(np.float32) / 255.0 - mask_3ch = np.stack([mask_norm] * 3, axis=-1) - - # 알파 블렌딩 - result = (foreground.astype(np.float32) * mask_3ch + - bg.astype(np.float32) * (1 - mask_3ch)) - - return result.astype(np.uint8) - - except Exception as e: - logger.error(f"Background application failed: {e}") - raise - + logger.error(f"배경 제거 처리 중 오류 ({model_name}): {e}", exc_info=True) + return None, None + + def set_default_model(self, model_name): + if model_name not in self.SUPPORTED_MODELS: + raise ValueError(f"지원하지 않는 모델명: {model_name}") + self.model_name = model_name + logger.info(f"rembg 기본 모델이 '{model_name}'(으)로 변경됨") + + def get_default_model(self): + return self.model_name + + def get_supported_models(self): + return self.SUPPORTED_MODELS.copy() + + def get_model_description(self, model_name): + return self.SUPPORTED_MODELS.get(model_name, "모델 설명 없음") + + def is_available(self): + return self._check_rembg_availability() + + def get_init_error(self): + return self._init_error + def get_model_info(self) -> dict: """모델 정보를 반환합니다.""" return { @@ -170,5 +284,8 @@ class RembgProcessor: "model_name": self.model_name, "device": self.device, "fp16": self.fp16, - "loaded": self.loaded - } + "loaded": self.loaded, + "available": self.is_available(), + "supported_models": list(self.SUPPORTED_MODELS.keys()), + "local_model_path": self.local_rembg_model_path + } \ No newline at end of file diff --git a/app/models/simple_lama.py b/app/models/simple_lama.py index 10cca56..1eee3c0 100644 --- a/app/models/simple_lama.py +++ b/app/models/simple_lama.py @@ -29,17 +29,22 @@ class SimpleLamaInpainter: try: logger.info("Loading Simple LAMA model...") - # 실제 구현에서는 simple-lama-inpainting 라이브러리를 사용 - # 여기서는 플레이스홀더로 구현 - await asyncio.sleep(0.1) # 모델 로딩 시뮬레이션 + # 실제 simple-lama-inpainting 라이브러리 사용 + try: + from simple_lama_inpainting import SimpleLama + self.model = SimpleLama(device=self.device) + logger.info("실제 SimpleLama 모델 로딩 완료") + except ImportError as e: + logger.warning(f"SimpleLama 라이브러리 import 실패: {e}") + logger.info("fallback 모드로 전환합니다...") + # fallback으로 시뮬레이션 모드 사용 + self.model = {"type": "simple_lama_fallback", "device": self.device, "fp16": self.fp16} + except Exception as e: + logger.error(f"SimpleLama 모델 초기화 실패: {e}") + logger.info("fallback 모드로 전환합니다...") + self.model = {"type": "simple_lama_fallback", "device": self.device, "fp16": self.fp16} - # TODO: 실제 모델 로딩 로직 - # from simple_lama_inpainting import SimpleLama - # self.model = SimpleLama(device=self.device) - - self.model = {"type": "simple_lama", "device": self.device, "fp16": self.fp16} self.loaded = True - logger.info("Simple LAMA model loaded successfully") except Exception as e: @@ -114,18 +119,34 @@ class SimpleLamaInpainter: image_tensor = self.preprocess_image(image) mask_tensor = self.preprocess_mask(mask) - # 추론 (실제 구현에서는 모델 추론) + # 실제 모델 추론 with torch.no_grad(): - # TODO: 실제 모델 추론 로직 - # result = self.model(image_tensor, mask_tensor) - - # 플레이스홀더: 마스크 영역을 평균 색상으로 채우기 - result = await self._simulate_inpainting(image_tensor, mask_tensor) - - # 후처리 - result_np = self.postprocess_result(result) - - return result_np + if hasattr(self.model, '__call__') and not isinstance(self.model, dict): + # 실제 SimpleLama 모델 사용 + logger.info("실제 SimpleLama 모델로 인페인팅 수행") + + # SimpleLama는 PIL Image를 받으므로 변환 + if isinstance(image, np.ndarray): + pil_image = Image.fromarray(image) + else: + pil_image = image + + if isinstance(mask, np.ndarray): + pil_mask = Image.fromarray(mask) + else: + pil_mask = mask + + # 실제 추론 수행 + result_pil = self.model(pil_image, pil_mask) + result_np = np.array(result_pil) + + return result_np + else: + # Fallback: 시뮬레이션 모드 + logger.warning("Fallback 모드: 시뮬레이션 인페인팅 사용") + result = await self._simulate_inpainting(image_tensor, mask_tensor) + result_np = self.postprocess_result(result) + return result_np except Exception as e: logger.error(f"Inpainting failed: {e}") diff --git a/app/monitoring/dashboard.py b/app/monitoring/dashboard.py index ba6890e..871c0dd 100644 --- a/app/monitoring/dashboard.py +++ b/app/monitoring/dashboard.py @@ -109,28 +109,21 @@ class MonitoringData: logger.info("워커 상태가 비어있어 기본값 사용") worker_status = self._get_default_worker_status() - # 실제 세션 풀 상태 가져오기 (status.json보다 우선) + # 항상 실제 세션 풀 상태 사용 (동적 데이터) try: - logger.info("실제 세션 풀 상태 수집 시작") real_session_status = session_pool.get_status() + session_status = real_session_status + logger.debug(f"실시간 세션 풀 상태 사용: {real_session_status}") - # 세션 풀이 초기화되었는지 확인 (모든 total이 0이 아닌지 체크) - total_sessions = sum(pool_info.get("total", 0) for pool_info in real_session_status.values()) - - if real_session_status and total_sessions > 0: - session_status = real_session_status - logger.info(f"실제 세션 풀 상태 사용: {real_session_status}") - else: - logger.info(f"세션 풀이 아직 초기화되지 않음 (총 세션: {total_sessions}), status.json 사용") - # status.json에서 가져온 값을 그대로 사용 - if not session_status: - logger.info("세션 상태가 비어있어 기본값 사용") - session_status = self._get_default_session_status() - except Exception as e: - logger.warning(f"실제 세션 풀 상태 조회 실패: {e}") if not session_status: logger.info("세션 상태가 비어있어 기본값 사용") session_status = self._get_default_session_status() + except Exception as e: + logger.warning(f"실제 세션 풀 상태 조회 실패: {e}") + session_status = status.get("session_status", {}) + if not session_status: + logger.info("status.json 세션 상태도 비어있어 기본값 사용") + session_status = self._get_default_session_status() # GPU 정보 (안전하게 가져오기) gpu_info = {} @@ -895,6 +888,44 @@ HTML_TEMPLATE = """ + +