325 lines
10 KiB
Python
325 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
간단한 iopaint 호환성 테스트 서버
|
|
기본적인 API 엔드포인트들을 테스트할 수 있습니다.
|
|
"""
|
|
import base64
|
|
import time
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import List
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
import cv2
|
|
import numpy as np
|
|
from PIL import Image
|
|
import io
|
|
|
|
# 로깅 설정
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# FastAPI 앱 생성
|
|
app = FastAPI(title="iopaint 호환성 테스트 서버", version="1.0.0")
|
|
|
|
# CORS 미들웨어 추가
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# 시작 시간 기록
|
|
start_time = time.time()
|
|
|
|
# Pydantic 모델들
|
|
class InpaintRequest(BaseModel):
|
|
image: str # base64 인코딩된 이미지
|
|
mask: str # base64 인코딩된 마스크
|
|
model_name: str = "simple-lama"
|
|
prompt: str = ""
|
|
negative_prompt: str = ""
|
|
sd_seed: int = -1
|
|
num_inference_steps: int = 20
|
|
guidance_scale: float = 7.5
|
|
strength: float = 1.0
|
|
|
|
class RemoveBGRequest(BaseModel):
|
|
image: str # base64 인코딩된 이미지
|
|
model_name: str = "rembg"
|
|
|
|
class InpaintResponse(BaseModel):
|
|
success: bool
|
|
image: str = None # base64 인코딩된 결과 이미지
|
|
error: str = None
|
|
processing_time: float = None
|
|
seed: int = None
|
|
|
|
class RemoveBGResponse(BaseModel):
|
|
success: bool
|
|
image: str = None # base64 인코딩된 결과 이미지
|
|
mask: str = None # base64 인코딩된 마스크
|
|
error: str = None
|
|
processing_time: float = None
|
|
|
|
class HealthResponse(BaseModel):
|
|
status: str
|
|
timestamp: str
|
|
version: str
|
|
uptime: float
|
|
|
|
class ModelInfo(BaseModel):
|
|
name: str
|
|
type: str
|
|
description: str = None
|
|
supported_formats: List[str] = []
|
|
max_image_size: int = None
|
|
|
|
class ServerConfigResponse(BaseModel):
|
|
models: List[ModelInfo]
|
|
max_file_size: int
|
|
supported_formats: List[str]
|
|
device: str
|
|
is_jetson: bool
|
|
|
|
# 유틸리티 함수들
|
|
def decode_base64_to_image(base64_string: str, gray: bool = False):
|
|
"""base64 문자열을 이미지로 디코딩 (iopaint 호환)"""
|
|
try:
|
|
# base64 디코딩
|
|
image_data = base64.b64decode(base64_string)
|
|
|
|
# PIL Image로 변환
|
|
image = Image.open(io.BytesIO(image_data))
|
|
|
|
# numpy 배열로 변환
|
|
if gray:
|
|
image = image.convert('L')
|
|
image_array = np.array(image)
|
|
else:
|
|
image_array = np.array(image.convert('RGB'))
|
|
|
|
return image_array
|
|
except Exception as e:
|
|
logger.error(f"이미지 디코딩 실패: {e}")
|
|
raise HTTPException(status_code=400, detail=f"이미지 디코딩 실패: {str(e)}")
|
|
|
|
def encode_image_to_base64(image_array: np.ndarray, format: str = "PNG"):
|
|
"""이미지를 base64로 인코딩 (iopaint 호환)"""
|
|
try:
|
|
# PIL Image로 변환
|
|
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
|
image = Image.fromarray(image_array, 'RGB')
|
|
else:
|
|
image = Image.fromarray(image_array)
|
|
|
|
# 바이트로 변환
|
|
buffer = io.BytesIO()
|
|
image.save(buffer, format=format)
|
|
image_bytes = buffer.getvalue()
|
|
|
|
# base64로 인코딩
|
|
base64_string = base64.b64encode(image_bytes).decode('utf-8')
|
|
return base64_string
|
|
except Exception as e:
|
|
logger.error(f"이미지 인코딩 실패: {e}")
|
|
raise HTTPException(status_code=500, detail=f"이미지 인코딩 실패: {str(e)}")
|
|
|
|
def simulate_inpainting(image: np.ndarray, mask: np.ndarray, prompt: str = ""):
|
|
"""인페인팅 시뮬레이션 (실제 구현에서는 실제 모델 사용)"""
|
|
# 마스크 영역을 약간 수정하여 시뮬레이션
|
|
result = image.copy()
|
|
|
|
# 마스크 영역을 약간 밝게 만들기
|
|
mask_bool = mask > 127
|
|
result[mask_bool] = np.clip(result[mask_bool] * 1.2, 0, 255).astype(np.uint8)
|
|
|
|
# 약간의 노이즈 추가
|
|
noise = np.random.randint(-20, 20, result.shape, dtype=np.int16)
|
|
result = np.clip(result.astype(np.int16) + noise, 0, 255).astype(np.uint8)
|
|
|
|
return result
|
|
|
|
def simulate_background_removal(image: np.ndarray):
|
|
"""배경 제거 시뮬레이션 (실제 구현에서는 실제 모델 사용)"""
|
|
# 중앙 영역을 전경으로, 가장자리를 배경으로 가정
|
|
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
|
|
|
|
# 마스크를 0-255 범위로 변환
|
|
mask_uint8 = (mask * 255).astype(np.uint8)
|
|
|
|
# 결과 이미지 (마스크 영역만 유지)
|
|
result = image.copy()
|
|
result[~mask] = [255, 255, 255] # 배경을 흰색으로
|
|
|
|
return result, mask_uint8
|
|
|
|
# API 엔드포인트들
|
|
@app.get("/health", response_model=HealthResponse)
|
|
async def health_check():
|
|
"""서버 상태 확인 (iopaint 호환)"""
|
|
uptime = time.time() - start_time
|
|
return HealthResponse(
|
|
status="healthy",
|
|
timestamp=datetime.now().isoformat(),
|
|
version="1.0.0",
|
|
uptime=uptime
|
|
)
|
|
|
|
@app.get("/api/v1/server-config", response_model=ServerConfigResponse)
|
|
async def get_server_config():
|
|
"""서버 설정 정보 반환 (iopaint 호환)"""
|
|
models = [
|
|
ModelInfo(
|
|
name="simple-lama",
|
|
type="inpainting",
|
|
description="Simple LAMA 인페인팅 모델 (테스트용)",
|
|
supported_formats=["png", "jpg", "jpeg"],
|
|
max_image_size=2048
|
|
),
|
|
ModelInfo(
|
|
name="migan",
|
|
type="inpainting",
|
|
description="MIGAN 인페인팅 모델 (테스트용)",
|
|
supported_formats=["png", "jpg", "jpeg"],
|
|
max_image_size=2048
|
|
),
|
|
ModelInfo(
|
|
name="rembg",
|
|
type="rembg",
|
|
description="Rembg 배경 제거 모델 (테스트용)",
|
|
supported_formats=["png", "jpg", "jpeg"],
|
|
max_image_size=2048
|
|
)
|
|
]
|
|
|
|
return ServerConfigResponse(
|
|
models=models,
|
|
max_file_size=25,
|
|
supported_formats=["png", "jpg", "jpeg"],
|
|
device="cuda",
|
|
is_jetson=True
|
|
)
|
|
|
|
@app.post("/api/v1/inpaint", response_model=InpaintResponse)
|
|
async def inpaint_image(request: InpaintRequest):
|
|
"""인페인팅 API (iopaint 호환)"""
|
|
start_time_process = time.time()
|
|
|
|
try:
|
|
# base64 이미지 디코딩
|
|
image = decode_base64_to_image(request.image)
|
|
mask = decode_base64_to_image(request.mask, gray=True)
|
|
|
|
# 이미지와 마스크 크기 검증
|
|
if image.shape[:2] != mask.shape[:2]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"이미지 크기({image.shape[:2]})와 마스크 크기({mask.shape[:2]})가 일치하지 않습니다."
|
|
)
|
|
|
|
# 인페인팅 시뮬레이션
|
|
result = simulate_inpainting(image, mask, request.prompt)
|
|
|
|
# 결과를 base64로 인코딩
|
|
result_base64 = encode_image_to_base64(result)
|
|
|
|
processing_time = time.time() - start_time_process
|
|
|
|
logger.info(f"인페인팅 성공: {image.shape}, 처리시간: {processing_time:.2f}초")
|
|
|
|
return InpaintResponse(
|
|
success=True,
|
|
image=result_base64,
|
|
processing_time=processing_time,
|
|
seed=request.sd_seed
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"인페인팅 처리 실패: {e}")
|
|
processing_time = time.time() - start_time_process
|
|
|
|
return InpaintResponse(
|
|
success=False,
|
|
error=f"인페인팅 처리 실패: {str(e)}",
|
|
processing_time=processing_time
|
|
)
|
|
|
|
@app.post("/api/v1/remove_bg", response_model=RemoveBGResponse)
|
|
async def remove_background(request: RemoveBGRequest):
|
|
"""배경 제거 API (iopaint 호환)"""
|
|
start_time_process = time.time()
|
|
|
|
try:
|
|
# base64 이미지 디코딩
|
|
image = decode_base64_to_image(request.image)
|
|
|
|
# 배경 제거 시뮬레이션
|
|
result_image, result_mask = simulate_background_removal(image)
|
|
|
|
# 결과를 base64로 인코딩
|
|
result_base64 = encode_image_to_base64(result_image)
|
|
mask_base64 = encode_image_to_base64(result_mask)
|
|
|
|
processing_time = time.time() - start_time_process
|
|
|
|
logger.info(f"배경 제거 성공: {image.shape}, 처리시간: {processing_time:.2f}초")
|
|
|
|
return RemoveBGResponse(
|
|
success=True,
|
|
image=result_base64,
|
|
mask=mask_base64,
|
|
processing_time=processing_time
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"배경 제거 처리 실패: {e}")
|
|
processing_time = time.time() - start_time_process
|
|
|
|
return RemoveBGResponse(
|
|
success=False,
|
|
error=f"배경 제거 처리 실패: {str(e)}",
|
|
processing_time=processing_time
|
|
)
|
|
|
|
@app.get("/api/v1/samplers")
|
|
async def get_samplers():
|
|
"""사용 가능한 샘플러 목록 반환 (iopaint 호환)"""
|
|
return [
|
|
"euler", "euler_a", "heun", "dpm_2", "dpm_2_a",
|
|
"lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_a",
|
|
"dpmpp_sde", "dpmpp_2m", "ddim", "uni_pc", "uni_pc_bh2"
|
|
]
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""루트 엔드포인트"""
|
|
return {
|
|
"message": "iopaint 호환성 테스트 서버",
|
|
"version": "1.0.0",
|
|
"docs": "/docs",
|
|
"health": "/health"
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
print("🚀 iopaint 호환성 테스트 서버 시작 중...")
|
|
print(" 📖 API 문서: http://localhost:8002/docs")
|
|
print(" 🏥 헬스 체크: http://localhost:8002/health")
|
|
print(" 🧪 테스트: http://localhost:8002/api/v1/server-config")
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=8002, log_level="info")
|