import gradio as gr import numpy as np from PIL import Image, ImageEnhance import random import cv2 import io import datetime import zipfile import tempfile import os import shutil import pytz import logging # ------------------- 로깅 설정 ------------------- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class ImageTransformer: def __init__(self): self.method_info = { "미세 노이즈 추가": { "function": self.add_micro_noise, "description": "이미지에 눈에 보이지 않는 작은 노이즈를 추가합니다. 픽셀값에 0.1~0.5 정도의 미세한 변화를 줍니다." }, "색상 미세 조정": { "function": self.adjust_color_slightly, "description": "이미지의 채도와 색조를 아주 조금 변경합니다. 사람 눈으로는 구분이 어려운 0.1% 이내의 변화입니다." }, "미세 회전": { "function": self.micro_rotate, "description": "이미지를 0.1도 이내로 회전시킵니다. 육안으로는 회전을 감지할 수 없는 수준입니다." }, "미세 크기 조정": { "function": self.micro_scale, "description": "이미지 크기를 0.01% 정도 변경했다가 다시 원래 크기로 복원합니다. 픽셀 보간으로 인한 미세한 차이가 발생합니다." }, "압축률 조정": { "function": self.adjust_compression, "description": "JPEG 압축 품질을 94-96 사이에서 랜덤하게 변경합니다. 화질 차이는 거의 없지만 파일 구조가 달라집니다." }, "보이지 않는 워터마크": { "function": self.add_invisible_watermark, "description": "눈에 보이지 않는 패턴을 이미지에 추가합니다. 0.1~0.3 정도의 매우 약한 격자 패턴입니다." }, "밝기/대비 미세 조정": { "function": self.micro_brightness_contrast, "description": "밝기와 대비를 0.1% 이내로 조정합니다. 모니터 설정 차이보다 작은 변화입니다." }, "픽셀 위치 미세 이동": { "function": self.pixel_shift, "description": "전체 이미지를 0.5픽셀 이내로 이동시킵니다. 서브픽셀 단위의 이동으로 육안 구분이 불가능합니다." }, "채널별 미세 조정": { "function": self.channel_adjustment, "description": "RGB 각 채널의 값을 ±1 정도 조정합니다. 색상 차이가 거의 없지만 데이터는 다릅니다." }, "미세 블러 효과": { "function": self.micro_blur, "description": "이미지에 아주 약한 블러 효과를 적용합니다. 선명도 차이를 거의 느낄 수 없는 수준입니다." } } def add_micro_noise(self, img_array, intensity=0.5): """극미량의 가우시안 노이즈 추가""" img_float = img_array.astype(np.float32) noise = np.random.normal(0, intensity * 5, img_array.shape) noisy_image = img_float + noise return np.clip(noisy_image, 0, 255).astype(np.uint8) def adjust_color_slightly(self, img_array, intensity=0.5): """색상을 미세하게 조정""" img_pil = Image.fromarray(img_array) enhancer = ImageEnhance.Color(img_pil) factor = 1.0 + (random.uniform(-0.02, 0.02) * intensity) img_pil = enhancer.enhance(factor) return np.array(img_pil) def micro_rotate(self, img_array, intensity=0.5): """미세한 회전 적용""" angle = random.uniform(-0.5, 0.5) * intensity rows, cols = img_array.shape[:2] M = cv2.getRotationMatrix2D((cols/2, rows/2), angle, 1) rotated = cv2.warpAffine(img_array, M, (cols, rows), borderMode=cv2.BORDER_REFLECT) return rotated def micro_scale(self, img_array, intensity=0.5): """미세한 크기 조정""" scale_factor = 1.0 + (random.uniform(-0.002, 0.002) * intensity) rows, cols = img_array.shape[:2] new_rows, new_cols = int(rows * scale_factor), int(cols * scale_factor) resized = cv2.resize(img_array, (new_cols, new_rows), interpolation=cv2.INTER_LANCZOS4) final = cv2.resize(resized, (cols, rows), interpolation=cv2.INTER_LANCZOS4) return final def adjust_compression(self, img_array, intensity=0.5): """압축률 미세 조정""" img_pil = Image.fromarray(img_array) buffer = io.BytesIO() quality = int(95 - (random.uniform(3, 8) * intensity)) img_pil.save(buffer, format='JPEG', quality=quality) buffer.seek(0) compressed_img = Image.open(buffer) return np.array(compressed_img) def add_invisible_watermark(self, img_array, intensity=0.5): """눈에 보이지 않는 미세한 워터마크 추가""" rows, cols = img_array.shape[:2] watermark = np.zeros((rows, cols), dtype=np.float32) for i in range(0, rows, 20): for j in range(0, cols, 20): if random.random() > 0.5: watermark[i, j] = 2.0 * intensity watermark = cv2.GaussianBlur(watermark, (5, 5), 0) if len(img_array.shape) == 3: watermark = np.dstack([watermark] * 3) img_float = img_array.astype(np.float32) watermarked = img_float + watermark return np.clip(watermarked, 0, 255).astype(np.uint8) def micro_brightness_contrast(self, img_array, intensity=0.5): """밝기와 대비 미세 조정""" alpha = 1.0 + (random.uniform(-0.01, 0.01) * intensity) beta = random.uniform(-3, 3) * intensity adjusted = cv2.convertScaleAbs(img_array, alpha=alpha, beta=beta) return adjusted def pixel_shift(self, img_array, intensity=0.5): """픽셀 위치 미세 이동""" rows, cols = img_array.shape[:2] dx = random.uniform(-2.0, 2.0) * intensity dy = random.uniform(-2.0, 2.0) * intensity M = np.float32([[1, 0, dx], [0, 1, dy]]) shifted = cv2.warpAffine(img_array, M, (cols, rows), borderMode=cv2.BORDER_REFLECT) return shifted def channel_adjustment(self, img_array, intensity=0.5): """채널별 미세 조정""" adjusted = img_array.astype(np.float32) for i in range(3): adjustment = random.randint(-4, 4) * intensity adjusted[:, :, i] = adjusted[:, :, i] + adjustment adjusted = np.clip(adjusted, 0, 255) return adjusted.astype(np.uint8) def micro_blur(self, img_array, intensity=0.5): """미세 블러 효과""" kernel_size = 3 sigma = 0.5 * intensity blurred = cv2.GaussianBlur(img_array, (kernel_size, kernel_size), sigma) return blurred def transform_image_with_details(self, image, selected_methods, intensity): """이미지 변형 및 상세 정보 반환""" if image is None: return None, {} img_array = np.array(image) details = {} for method_name in selected_methods: if method_name in self.method_info: if method_name == "미세 노이즈 추가": noise_level = intensity * 5 img_array = self.add_micro_noise(img_array, intensity) details[method_name] = f"가우시안 노이즈 강도: {noise_level:.3f} (표준편차)" elif method_name == "색상 미세 조정": factor_change = random.uniform(-0.02, 0.02) * intensity img_array = self.adjust_color_slightly(img_array, intensity) details[method_name] = f"채도 변화: {factor_change*100:.2f}%" elif method_name == "미세 회전": angle = random.uniform(-0.5, 0.5) * intensity img_array = self.micro_rotate(img_array, intensity) details[method_name] = f"회전 각도: {angle:.3f}도" elif method_name == "미세 크기 조정": scale_change = random.uniform(-0.002, 0.002) * intensity img_array = self.micro_scale(img_array, intensity) details[method_name] = f"크기 변화: {scale_change*100:.3f}%" elif method_name == "압축률 조정": quality_reduction = random.uniform(3, 8) * intensity quality = int(95 - quality_reduction) img_array = self.adjust_compression(img_array, intensity) details[method_name] = f"JPEG 품질: {quality} (원본 대비 -{quality_reduction:.1f})" elif method_name == "보이지 않는 워터마크": pattern_intensity = 2.0 * intensity img_array = self.add_invisible_watermark(img_array, intensity) details[method_name] = f"패턴 강도: {pattern_intensity:.2f} (20x20 그리드)" elif method_name == "밝기/대비 미세 조정": alpha_change = random.uniform(-0.01, 0.01) * intensity beta_change = random.uniform(-3, 3) * intensity img_array = self.micro_brightness_contrast(img_array, intensity) details[method_name] = f"대비 변화: {alpha_change*100:.2f}%, 밝기 변화: {beta_change:.2f}" elif method_name == "픽셀 위치 미세 이동": dx = random.uniform(-2.0, 2.0) * intensity dy = random.uniform(-2.0, 2.0) * intensity img_array = self.pixel_shift(img_array, intensity) details[method_name] = f"이동량: X축 {dx:.2f}px, Y축 {dy:.2f}px" elif method_name == "채널별 미세 조정": rgb_changes = [random.randint(-4, 4) * intensity for _ in range(3)] img_array = self.channel_adjustment(img_array, intensity) details[method_name] = f"RGB 조정: R{rgb_changes[0]:+.1f}, G{rgb_changes[1]:+.1f}, B{rgb_changes[2]:+.1f}" elif method_name == "미세 블러 효과": sigma = 0.5 * intensity img_array = self.micro_blur(img_array, intensity) details[method_name] = f"블러 강도(시그마): {sigma:.3f}" transformed_image = Image.fromarray(img_array) # EXIF 데이터 처리 (에러 방지) try: exif = transformed_image.getexif() exif[0x9003] = datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S") except: pass return transformed_image, details def calculate_similarity(self, original, transformed): """원본과 변형된 이미지의 유사도 계산""" if original is None or transformed is None: return 0.0 original_cv = cv2.cvtColor(np.array(original), cv2.COLOR_RGB2BGR) transformed_cv = cv2.cvtColor(np.array(transformed), cv2.COLOR_RGB2BGR) # MSE 기반 유사도 계산 mse = np.mean((original_cv - transformed_cv) ** 2) if mse == 0: return 100.0 # MSE를 더 민감하게 반영하도록 조정 max_mse = 255.0 * 255.0 similarity = 100 - (mse / max_mse * 100 * 5) return max(85, min(95, similarity)) # 다크모드 지원 커스텀 CSS 스타일 custom_css = """ /* ============================================ 1. CSS 변수 정의 (라이트모드 - 기본값) ============================================ */ :root { /* 메인 컬러 */ --primary-color: #FB7F0D; --secondary-color: #ff9a8b; --accent-color: #FF6B6B; /* 배경 컬러 */ --background-color: #FFFFFF; --card-bg: #ffffff; --input-bg: #ffffff; /* 텍스트 컬러 */ --text-color: #334155; --text-secondary: #64748b; /* 보더 및 구분선 */ --border-color: #dddddd; --border-light: #e5e5e5; /* 테이블 컬러 */ --table-even-bg: #f3f3f3; --table-hover-bg: #f0f0f0; /* 그림자 */ --shadow: 0 8px 30px rgba(251, 127, 13, 0.08); --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1); /* 기타 */ --border-radius: 18px; } /* ============================================ 2. 다크모드 색상 변수 (자동 감지) ============================================ */ @media (prefers-color-scheme: dark) { :root { /* 배경 컬러 */ --background-color: #1a1a1a; --card-bg: #2d2d2d; --input-bg: #2d2d2d; /* 텍스트 컬러 */ --text-color: #e5e5e5; --text-secondary: #a1a1aa; /* 보더 및 구분선 */ --border-color: #404040; --border-light: #525252; /* 테이블 컬러 */ --table-even-bg: #333333; --table-hover-bg: #404040; /* 그림자 */ --shadow: 0 8px 30px rgba(0, 0, 0, 0.3); --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2); } } /* ============================================ 3. 수동 다크모드 클래스 (Gradio 토글용) ============================================ */ [data-theme="dark"], .dark, .gr-theme-dark { /* 배경 컬러 */ --background-color: #1a1a1a; --card-bg: #2d2d2d; --input-bg: #2d2d2d; /* 텍스트 컬러 */ --text-color: #e5e5e5; --text-secondary: #a1a1aa; /* 보더 및 구분선 */ --border-color: #404040; --border-light: #525252; /* 테이블 컬러 */ --table-even-bg: #333333; --table-hover-bg: #404040; /* 그림자 */ --shadow: 0 8px 30px rgba(0, 0, 0, 0.3); --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2); } /* ============================================ 4. 기본 스타일 (원본 UI 유지) ============================================ */ body { font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background-color: var(--background-color) !important; color: var(--text-color) !important; line-height: 1.6; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; } /* 푸터 숨김 설정 */ footer { visibility: hidden; } /* ============================================ 5. Gradio 컨테이너 강제 적용 ============================================ */ .gradio-container, .gradio-container *, .gr-app, .gr-app *, .gr-interface { background-color: var(--background-color) !important; color: var(--text-color) !important; } .gradio-container { width: 100%; margin: 0 auto; padding: 20px; background-color: var(--background-color) !important; } /* ============================================ 6. 헤더 스타일 (원본 유지) ============================================ */ .custom-header { background: #FF7F00; padding: 2rem; border-radius: 15px; margin-bottom: 20px; box-shadow: var(--shadow); text-align: center; } .custom-header h1 { margin: 0; font-size: 2.5rem; font-weight: 700; color: black; } .custom-header p { margin: 10px 0 0; font-size: 1.2rem; color: black; } /* ============================================ 7. 카드 및 패널 스타일 (다크모드 적용) ============================================ */ .custom-frame, .gr-form, .gr-box, .gr-panel, [class*="frame"], [class*="card"], [class*="panel"] { background-color: var(--card-bg) !important; border: 1px solid var(--border-color) !important; border-radius: var(--border-radius); padding: 20px; margin: 10px 0; box-shadow: var(--shadow) !important; color: var(--text-color) !important; } .custom-section-group { margin-top: 20px; padding: 0; border: none; border-radius: 0; background-color: var(--background-color) !important; box-shadow: none !important; } /* ============================================ 8. 입력 필드 스타일 (다크모드 적용) ============================================ */ input[type="text"], input[type="number"], input[type="email"], input[type="password"], textarea, select, .gr-input, .gr-text-input, .gr-textarea, .gr-dropdown, .gr-sample-inputs { background-color: var(--input-bg) !important; color: var(--text-color) !important; border: 1px solid var(--border-color) !important; border-radius: var(--border-radius) !important; padding: 12px !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important; transition: all 0.3s ease !important; } input[type="text"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="password"]:focus, textarea:focus, select:focus, .gr-input:focus, .gr-text-input:focus, .gr-textarea:focus, .gr-dropdown:focus { border-color: var(--primary-color) !important; outline: none !important; box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important; } /* ============================================ 9. 버튼 스타일 (원본 유지) ============================================ */ .custom-button { border-radius: 30px !important; background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; color: white !important; font-size: 18px !important; padding: 10px 20px !important; border: none; box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25); transition: transform 0.3s ease; } .custom-button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3); } /* 일반 버튼 다크모드 적용 */ button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* ============================================ 10. 텍스트 및 라벨 스타일 (다크모드 적용) ============================================ */ label, .gr-label, .gr-checkbox label, .gr-radio label, p, span, div { color: var(--text-color) !important; } .custom-title { font-size: 28px; font-weight: bold; margin-bottom: 10px; color: var(--text-color) !important; border-bottom: 2px solid var(--primary-color); padding-bottom: 5px; } /* ============================================ 11. 섹션 제목 스타일 (다크모드 적용) ============================================ */ .section-title { display: flex; align-items: center; font-size: 20px; font-weight: 700; color: var(--text-color) !important; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 2px solid #FB7F0D; font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; } .section-title img { margin-right: 10px; width: 24px; height: 24px; } /* ============================================ 12. 이미지 컨테이너 스타일 (다크모드 적용) ============================================ */ .image-container { border-radius: var(--border-radius); overflow: hidden; border: 1px solid var(--border-color) !important; transition: all 0.3s ease; background-color: var(--card-bg) !important; aspect-ratio: 1 / 1; } .image-container:hover { box-shadow: var(--shadow-light); } .image-container img { width: 100%; height: 100%; object-fit: contain; } /* ============================================ 13. 테이블 스타일 (다크모드 적용) ============================================ */ table { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } table th { background-color: var(--primary-color) !important; color: white !important; border-color: var(--border-color) !important; } table td { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } table tbody tr:nth-child(even) { background-color: var(--table-even-bg) !important; } table tbody tr:hover { background-color: var(--table-hover-bg) !important; } /* ============================================ 14. 체크박스 및 라디오 버튼 (다크모드 적용) ============================================ */ input[type="checkbox"], input[type="radio"] { accent-color: var(--primary-color) !important; } /* ============================================ 15. 스크롤바 스타일 (다크모드 적용) ============================================ */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--card-bg); border-radius: 10px; } ::-webkit-scrollbar-thumb { background: var(--primary-color); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: var(--secondary-color); } /* ============================================ 16. 추가 Gradio 컴포넌트들 (다크모드 적용) ============================================ */ .gr-block, .gr-group, .gr-row, .gr-column { background-color: var(--background-color) !important; color: var(--text-color) !important; } /* ============================================ 17. 코드 블록 및 pre 태그 (다크모드 적용) ============================================ */ code, pre, .code-block { background-color: var(--table-even-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* ============================================ 18. 애니메이션 스타일 (원본 유지) ============================================ */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.5s ease-out; } /* ============================================ 19. 전환 애니메이션 (다크모드 적용) ============================================ */ * { transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important; } /* ============================================ 20. 그룹 래퍼 배경 완전 제거 ============================================ */ .custom-section-group, .gr-block.gr-group { background-color: var(--background-color) !important; box-shadow: none !important; } .custom-section-group::before, .custom-section-group::after, .gr-block.gr-group::before, .gr-block.gr-group::after { display: none !important; content: none !important; } /* ============================================ 21. 알림 및 메시지 (다크모드 적용) ============================================ */ .alert, .message, .notification, [class*="alert"], [class*="message"], [class*="notification"] { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* ============================================ 22. 툴팁 및 팝업 (다크모드 적용) ============================================ */ [data-tooltip]:hover::after, .tooltip, .popup { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; box-shadow: var(--shadow-light) !important; } /* ============================================ 23. 모달 및 오버레이 (다크모드 적용) ============================================ */ .modal, .overlay, [class*="modal"], [class*="overlay"] { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* ============================================ 24. 아코디언 및 드롭다운 (다크모드 적용) ============================================ */ details { background-color: var(--card-bg) !important; border-color: var(--border-color) !important; color: var(--text-color) !important; } details summary { background-color: var(--card-bg) !important; color: var(--text-color) !important; } """ # FontAwesome 아이콘 포함 fontawesome_link = """ """ # Gradio 인터페이스 생성 transformer = ImageTransformer() def process_images(images, mode): if not images: return None, None, None, "이미지를 업로드해주세요." results = [] all_selected_methods = [] all_similarities = [] method_details = [] # 각 변형의 실제 수치 저장 # 임시 디렉토리 생성 with tempfile.TemporaryDirectory() as temp_dir: for idx, image_data in enumerate(images): # Gallery에서 받은 데이터 처리 if isinstance(image_data, tuple): # Gallery가 튜플(파일 경로, 파일 이름) 형태로 반환하는 경우 if len(image_data) > 0: image_path = image_data[0] if isinstance(image_data[0], str) else image_data[1] image = Image.open(image_path) original_filename = os.path.basename(image_path) else: continue elif isinstance(image_data, str): # 파일 경로인 경우 image = Image.open(image_data) original_filename = os.path.basename(image_data) elif isinstance(image_data, dict) and 'name' in image_data: # 파일 정보 딕셔너리인 경우 image = Image.open(image_data['name']) original_filename = os.path.basename(image_data['name']) else: # PIL Image 객체인 경우 image = image_data original_filename = f"image_{idx+1}.jpg" # RGB로 변환 (RGBA 등 다른 모드 처리) if image.mode != 'RGB': image = image.convert('RGB') if mode == "랜덤 변형": # 95% 목표 - 강도 5 selected_methods = random.sample(list(transformer.method_info.keys()), 5) intensity = 0.5 target_similarity = 95 else: # 최대 변형 # 90% 목표 - 모든 방법 적용, 강도 10 selected_methods = list(transformer.method_info.keys()) intensity = 1.0 target_similarity = 90 transformed, details = transformer.transform_image_with_details(image, selected_methods, intensity) similarity = transformer.calculate_similarity(image, transformed) results.append(transformed) all_selected_methods.append(selected_methods) all_similarities.append(similarity) method_details.append(details) # 변형된 이미지 저장 - 변경_원래파일명 name_part, ext = os.path.splitext(original_filename) new_filename = f"변경_{name_part}{ext}" transformed.save(os.path.join(temp_dir, new_filename), "JPEG", quality=95) # ZIP 파일 생성 - 이미지변경_한국날짜_시간 형식 kst = pytz.timezone('Asia/Seoul') now_kst = datetime.datetime.now(kst) zip_filename = f"이미지변경_{now_kst.strftime('%y.%m.%d_%H.%M')}.zip" zip_path = os.path.join(temp_dir, zip_filename) with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for filename in os.listdir(temp_dir): if filename.endswith(('.jpg', '.jpeg', '.png')): zipf.write(os.path.join(temp_dir, filename), filename) # ZIP 파일을 실제로 저장 zip_output_path = os.path.join(tempfile.gettempdir(), zip_filename) shutil.copy2(zip_path, zip_output_path) # 변형 설명 생성 avg_similarity = sum(all_similarities) / len(all_similarities) # 적용된 방법 설명 생성 - 기본 설명과 수치를 함께 포함 if method_details: detail_text = "### 적용된 변형 방법\n\n" for method, detail in method_details[0].items(): # 기본 설명과 실제 적용 수치를 결합 base_description = transformer.method_info[method]['description'] detail_text += f"**{method}**\n{base_description}\n실제 적용 수치: {detail}\n\n" detail_text += f"\n**변형 결과**\n" detail_text += f"- 처리된 이미지 수: {len(images)}개\n" detail_text += f"- 평균 유사도: {avg_similarity:.2f}%\n" detail_text += f"- 목표 유사도: {target_similarity}%\n" else: detail_text = "변형 정보 없음" # 선택된 방법들 (첫 번째 이미지 기준) selected_methods_display = all_selected_methods[0] return results, selected_methods_display, detail_text, zip_output_path # Gradio 인터페이스 구성 def create_app(): with gr.Blocks(css=custom_css, title="네이버 블로그 유사 이미지 회피 도구", theme=gr.themes.Default( primary_hue="orange", secondary_hue="orange", font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"] )) as demo: gr.HTML(fontawesome_link) # 이미지 변형 기능 섹션 - 탭 없이 직접 구현 with gr.Row(): with gr.Column(scale=1): # 이미지 업로드 섹션 with gr.Column(elem_classes="custom-frame"): gr.HTML('
이미지 업로드
') input_images = gr.Gallery( label="원본 이미지", columns=3, height="400px", allow_preview=True, object_fit="contain", type="filepath", elem_id="input_gallery" ) with gr.Column(scale=1): # 변형된 이미지 섹션 with gr.Column(elem_classes="custom-frame"): gr.HTML('
변형된 이미지
') output_images = gr.Gallery( label="", columns=3, height="400px", allow_preview=True, object_fit="contain", show_download_button=True, elem_id="output_gallery" ) # 변형 옵션 및 결과 섹션 with gr.Row(): with gr.Column(scale=1): with gr.Column(elem_classes="custom-frame"): gr.HTML('
변형 옵션
') with gr.Row(): random_btn = gr.Button("🎲 랜덤 변형 (95% 목표)", elem_classes="custom-button") max_btn = gr.Button("⚡ 최대 변형 (90% 목표)", elem_classes="custom-button") with gr.Column(scale=1): with gr.Column(elem_classes="custom-frame"): gr.HTML('
결과 다운로드
') download_file = gr.File(label="ZIP 파일 다운로드") # 적용된 변형 방법 정보 with gr.Column(elem_classes="custom-frame"): gr.HTML('
적용된 변형 정보
') with gr.Row(): with gr.Column(scale=1): method_checkboxes = gr.CheckboxGroup( choices=list(transformer.method_info.keys()), label="적용된 변형 방법", interactive=False ) with gr.Column(scale=1): status_text = gr.Textbox( label="변형 상세 정보", interactive=False, lines=12 ) # 이벤트 핸들러 def random_transform(images): if not images: return None, None, "이미지를 먼저 업로드해주세요.", None return process_images(images, "랜덤 변형") def max_transform(images): if not images: return None, None, "이미지를 먼저 업로드해주세요.", None return process_images(images, "최대 변형") random_btn.click( fn=random_transform, inputs=[input_images], outputs=[output_images, method_checkboxes, status_text, download_file] ) max_btn.click( fn=max_transform, inputs=[input_images], outputs=[output_images, method_checkboxes, status_text, download_file] ) return demo if __name__ == "__main__": app = create_app() app.queue(max_size=10) # 요청을 순차적으로 처리하도록 큐 설정 app.launch(share=False, inbrowser=True, width="100%")