diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..e2d9729aae670eb88273b99ec1e813b08ea59162 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..715cf8f27f20188facb12525b5573efe83956e88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Git LFS tracks all binary files +# See .gitattributes for tracked file types diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..8af2f2399ad120faacc12fb33d166112b65496f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,46 @@ +POLYGOM PROPRIETARY SOFTWARE LICENSE + +Copyright (c) 2025 Polygom. All Rights Reserved. + +This software and associated documentation files (the "Software") are the +proprietary property of Polygom and are protected by copyright law. + +NO PERMISSION IS GRANTED TO: +- Use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software +- Use the Software for any commercial or non-commercial purpose +- Reverse engineer, decompile, or disassemble the Software + +This Software contains proprietary AI technology developed by Polygom. +Any unauthorized use, reproduction, or distribution is strictly prohibited +and may result in severe civil and criminal penalties. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED. IN NO EVENT SHALL POLYGOM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE. + +For licensing inquiries, please contact Polygom directly. + +--- + +POLYGOM 독점 소프트웨어 라이선스 + +Copyright (c) 2025 Polygom. 모든 권리 보유. + +이 소프트웨어와 관련 문서 파일("소프트웨어")은 Polygom의 독점 자산이며 +저작권법으로 보호됩니다. + +다음 행위는 허가되지 않습니다: +- 소프트웨어의 사용, 복사, 수정, 병합, 게시, 배포, 재라이선스, 판매 +- 상업적 또는 비상업적 목적으로의 소프트웨어 사용 +- 소프트웨어의 역공학, 디컴파일, 역어셈블 + +이 소프트웨어는 Polygom이 개발한 독점 AI 기술을 포함하고 있습니다. +무단 사용, 복제 또는 배포는 엄격히 금지되며 심각한 민형사상 처벌을 +받을 수 있습니다. + +소프트웨어는 "있는 그대로" 제공되며, 어떠한 명시적 또는 묵시적 보증도 +없습니다. Polygom은 소프트웨어와 관련하여 발생하는 어떠한 청구, 손해 +또는 기타 책임에 대해서도 책임지지 않습니다. + +라이선스 문의는 Polygom에 직접 연락하시기 바랍니다. \ No newline at end of file diff --git a/README.md b/README.md index 2e9aab659697eb258a81453ea8a5e92b4da09743..6e11090e4d661ac3939268b0b2eb7c842501c3a5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,51 @@ --- -title: PixelFree -emoji: 💻 -colorFrom: blue -colorTo: purple +title: PixelFree - AI Image Processing Platform +emoji: 🎨 +colorFrom: purple +colorTo: pink sdk: gradio -sdk_version: 5.49.0 +sdk_version: 5.45.0 app_file: app.py pinned: false -short_description: AI Image Processing --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# PixelFree - AI 이미지 처리 플랫폼 + +

+ Python + Gradio +

+ +## 🎯 소개 + +PixelFree는 **Polygom**의 자체 AI 기술을 활용한 이미지 처리 플랫폼입니다. 배경 제거, 배경 생성, 얼굴 교체 등의 고급 기능을 원클릭으로 제공합니다. + +## 📋 주요 기능 + +### 1. 배경 제거 +- 입력 이미지에서 배경을 자동으로 제거 +- 투명한 PNG 형식으로 결과 반환 + +### 2. 배경 생성 +- **2단계 프로세스**: + 1. 자동 배경 제거 + 2. 새로운 배경 생성 및 합성 +- 배경 선택 옵션: + - Indoor (실내 배경) + - Outdoor (실외 배경) + - Custom (사용자 업로드) + +### 3. 얼굴 교체 +- 참조 얼굴 선택 옵션: + - 남자 + - 여자 + - 전체 + - Custom (사용자 업로드) + +⚠️ **참고**: 이 API는 Polygom의 독점 기술이며, 별도의 라이선스 계약이 필요할 수 있습니다. + +자세한 내용은 [LICENSE](./LICENSE) 파일을 참조하세요. + +--- + +**Powered by Polygom** \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..190e235deb51768682abc59e0efc5a398129401c --- /dev/null +++ b/app.py @@ -0,0 +1,1219 @@ +""" +PixelFree - AI Image Processing Service +HuggingFace Spaces용 Gradio 애플리케이션 +Polygom API를 활용한 이미지 처리 서비스 + +Features: +- 배경 제거 (Background Removal) +- 배경 생성 (Background Generation) +- 얼굴 교체 (Face Swap) + +Version: 3.0.0 +Version 3: Simplified Multi-Mode Workflow +Author: PixelFree Team +""" + +import gradio as gr +import requests +import base64 +import io +import logging +import tempfile +from PIL import Image +from typing import Optional, Tuple +from datetime import datetime +from collections import OrderedDict +import time +import os + +# ===== Configuration ===== +API_BASE_URL = os.environ.get("API_BASE_URL") +API_ENDPOINTS = { + "background_removal": os.environ.get("BACKGROUND_REMOVAL_ENDPOINT"), + "background_generation": os.environ.get("BACKGROUND_GENERATION_ENDPOINT"), + "face_swap": os.environ.get("FACE_SWAP_ENDPOINT"), + "video_generation": os.environ.get("VIDEO_GEN"), +} +VIDEO_PROMPT = os.environ.get("VIDEO_PROMPT", "환하게 웃으며 눈을 깜빡인다.") + +# 허용된 사용자 목록 (Hugging Face Secrets에서 가져오기) +ALLOWED_USERS = os.environ.get("ALLOWED_USERS", "").split(",") +ALLOWED_USERS = [user.strip() for user in ALLOWED_USERS if user.strip()] + +IMAGE_HEIGHT = 500 +GALLERY_COLUMNS = 3 +GALLERY_ROWS = 3 +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger(__name__) + +IMAGE_CACHE_LIMIT = 24 +image_cache: "OrderedDict[str, Image.Image]" = OrderedDict() + + +def cache_image(path: str, image: Image.Image) -> None: + """Add image to global cache with simple LRU eviction.""" + if path in image_cache: + image_cache.move_to_end(path) + image_cache[path] = image + while len(image_cache) > IMAGE_CACHE_LIMIT: + image_cache.popitem(last=False) + +output_history = [] + + +def ensure_png_image(image: Optional[Image.Image]) -> Optional[Image.Image]: + """Ensure provided PIL image is stored as a PNG image (returns copy).""" + if image is None: + return None + + working_image = image + if working_image.mode not in ("RGB", "RGBA"): + working_image = working_image.convert("RGBA") + + buffer = io.BytesIO() + # Convert to PNG while preserving alpha when present + target_mode = "RGBA" if working_image.mode == "RGBA" else "RGB" + working_image.convert(target_mode).save(buffer, format="PNG") + buffer.seek(0) + with Image.open(buffer) as png_image: + return png_image.convert("RGBA").copy() + + +def make_thumbnail_image(image: Optional[Image.Image], size: Tuple[int, int] = (256, 256), *, fill_color=(255, 255, 255, 0)) -> Optional[Image.Image]: + """Create a thumbnail copy of the provided image for gallery previews.""" + base_image = ensure_png_image(image) + if base_image is None: + return None + + preview = base_image.copy() + try: + preview.thumbnail(size, Image.Resampling.LANCZOS) + except AttributeError: + preview.thumbnail(size) + canvas = Image.new("RGBA", size, fill_color) + offset = ((size[0] - preview.width) // 2, (size[1] - preview.height) // 2) + canvas.paste(preview, offset, preview if preview.mode == "RGBA" else None) + return canvas + + +def add_to_history(image: Optional[Image.Image], workspace: str) -> Optional[str]: + """Append processed image to history gallery with descriptive PNG name.""" + if image is None: + return None + + png_image = ensure_png_image(image) + if png_image is None: + return None + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + prefix_map = { + "bg_remove": "background_removed", + "bg_gen": "background_generated", + "face_swap": "face_swap", + "video": "video_generated", + } + prefix = prefix_map.get(workspace, "result") + file_name = f"{prefix}_{timestamp}.png" + + output_history.append((png_image, file_name)) + if len(output_history) > 20: + del output_history[:-20] + + return file_name + + +def get_history_items(limit: Optional[int] = None): + """Return history items (image, filename) with newest first.""" + if limit is None: + data = output_history[:] + elif limit <= 0: + return [] + else: + data = output_history[-limit:] + return list(reversed(data)) + + + +class ImageProcessor: + """Wrapper around PixelFree backend APIs.""" + + def __init__(self) -> None: + self.session = requests.Session() + adapter = requests.adapters.HTTPAdapter( + pool_connections=10, + pool_maxsize=20, + max_retries=3, + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + self.session.headers.update({"accept": "application/json"}) + + def image_to_bytes(self, image: Image.Image, format: str = "PNG", preserve_alpha: bool = False) -> bytes: + buffer = io.BytesIO() + img = image + if not preserve_alpha and img.mode in ("RGBA", "P"): + img = img.convert("RGB") + elif preserve_alpha and img.mode != "RGBA": + img = img.convert("RGBA") + img.save(buffer, format=format) + buffer.seek(0) + return buffer.getvalue() + + def base64_to_image(self, base64_str: str) -> Optional[Image.Image]: + try: + if base64_str.startswith("http"): + logger.error("Refusing to download image from URL: %s", base64_str) + return None + if "," in base64_str: + base64_str = base64_str.split(",", 1)[1] + img_data = base64.b64decode(base64_str) + return Image.open(io.BytesIO(img_data)) + except Exception as exc: + logger.error("Failed to decode image: %s", exc) + return None + + def call_api(self, endpoint: Optional[str], files: dict, data: dict) -> Optional[dict]: + if not endpoint or not API_BASE_URL: + logger.error("API endpoint or base URL missing") + return None + url = f"{API_BASE_URL}{endpoint}" + try: + response = self.session.post(url, files=files, data=data, timeout=300) + logger.info("API response %s: %s", endpoint, response.status_code) + response.raise_for_status() + return response.json() + except Exception as exc: + logger.error("API call failed (%s): %s", endpoint, exc) + return None + + def remove_background(self, image: Image.Image) -> Tuple[Optional[Image.Image], str]: + start_time = time.time() + files = { + "file": ("input.png", self.image_to_bytes(image, preserve_alpha=True), "image/png") + } + result = self.call_api(API_ENDPOINTS.get("background_removal"), files, {}) + total = time.time() - start_time + if result and result.get("success") and result.get("data"): + b64 = result["data"].get("result_base64") + image_obj = self.base64_to_image(b64) if b64 else None + if image_obj: + return image_obj, f"✅ 배경 제거 완료! (처리 시간: {total:.1f}초)" + return None, f"❌ 배경 제거 실패 (시도 시간: {total:.1f}초)" + + def generate_background( + self, + image: Image.Image, + bg_image: Image.Image, + hq_mode: bool = False, + ) -> Tuple[Optional[Image.Image], str]: + start_time = time.time() + files = { + "image": ("input.png", self.image_to_bytes(image, preserve_alpha=True), "image/png"), + "bg_image": ("background.png", self.image_to_bytes(bg_image), "image/png"), + } + data = { + "jobId": f"job_{int(time.time() * 1000)}", + "hq_mode": "true" if hq_mode else "false", + } + result = self.call_api(API_ENDPOINTS.get("background_generation"), files, data) + total = time.time() - start_time + mode_text = "고품질" if hq_mode else "일반" + if result and result.get("success") and result.get("data"): + payload = result["data"] + if payload.get("imageData"): + image_obj = self.base64_to_image(payload["imageData"]) + if image_obj: + return image_obj, f"✅ 배경 생성 완료! (처리 시간: {total:.1f}초, 모드: {mode_text})" + if payload.get("rawDataUrl"): + logger.error("Ignoring rawDataUrl field for security reasons.") + return None, f"❌ 배경 생성 실패 (시도 시간: {total:.1f}초, 모드: {mode_text})" + + def swap_face(self, source: Image.Image, target: Image.Image) -> Tuple[Optional[Image.Image], str]: + start_time = time.time() + files = { + "image": ("image.png", self.image_to_bytes(source), "image/png"), + "targetFaceImage": ("targetFace.png", self.image_to_bytes(target), "image/png"), + } + data = { + "jobId": f"face_swap_{int(time.time() * 1000)}" + } + result = self.call_api(API_ENDPOINTS.get("face_swap"), files, data) + total = time.time() - start_time + if result and result.get("success") and result.get("data"): + payload = result["data"] + if payload.get("imageData"): + image_obj = self.base64_to_image(payload["imageData"]) + if image_obj: + api_time = payload.get("processingTime", 0) + return image_obj, f"✅ 얼굴 교체 완료! (전체: {total:.1f}초, API: {api_time:.1f}초)" + if payload.get("rawDataUrl"): + logger.error("Ignoring rawDataUrl field for security reasons.") + return None, f"❌ 얼굴 교체 실패 (시도 시간: {total:.1f}초)" + + def generate_video(self, image: Optional[Image.Image]) -> Tuple[Optional[str], str]: + if image is None: + return None, "입력 이미지가 필요합니다." + + start_time = time.time() + files = { + "image": ("image.png", self.image_to_bytes(image, preserve_alpha=True), "image/png"), + } + data = { + "jobId": f"video_{int(time.time() * 1000)}", + "prompt": VIDEO_PROMPT, + "duration": "5", + } + result = self.call_api(API_ENDPOINTS.get("video_generation"), files, data) + total = time.time() - start_time + if result and result.get("success") and result.get("data"): + payload = result["data"] + video_base64 = payload.get("videoData") + if video_base64: + if "," in video_base64: + video_base64 = video_base64.split(",", 1)[1] + try: + video_bytes = base64.b64decode(video_base64) + with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video: + temp_video.write(video_bytes) + temp_path = temp_video.name + except Exception as exc: + logger.error("Failed to decode video: %s", exc) + else: + api_time = payload.get("processingTime", 0) + return temp_path, f"✅ 영상 생성 완료! (전체: {total:.1f}초, API: {api_time:.1f}초)" + if payload.get("rawDataUrl"): + logger.error("Ignoring rawDataUrl field for security reasons.") + return None, f"❌ 영상 생성 실패 (시도 시간: {total:.1f}초)" + +processor = ImageProcessor() + +# 인증 함수 +def auth_fn(username: str, password: str) -> bool: + """ + 사용자 인증 함수 + - username이 허용된 사용자 목록에 있고 + - username과 password가 같으면 인증 성공 + """ + return username == password and username in ALLOWED_USERS + +# 배경 이미지 로드 +def load_background_images(): + """배경 이미지를 assets 폴더에서 로드 (Lazy loading)""" + backgrounds = { + "indoor": [], + "outdoor": [], + "indoor_paths": [], + "outdoor_paths": [], + "indoor_pil": [], + "outdoor_pil": [] + } + + cwd = os.getcwd() + + # Indoor 이미지 경로 저장 + indoor_path = os.path.join(cwd, "assets", "indoor") + if os.path.exists(indoor_path): + files = sorted([f for f in os.listdir(indoor_path) if f.endswith('.png')]) + for img_file in files: + img_path = os.path.join(indoor_path, img_file) + try: + backgrounds["indoor_paths"].append(img_path) + # 갤러리용 썸네일만 생성 + with Image.open(img_path) as img: + img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB') + thumb = img.copy() + thumb.thumbnail((240, 240), Image.Resampling.LANCZOS) + backgrounds["indoor"].append(thumb) + backgrounds["indoor_pil"].append(img_path) # 경로 저장 + except Exception: + pass + + # Outdoor 이미지 로드 + outdoor_path = os.path.join(cwd, "assets", "outdoor") + if os.path.exists(outdoor_path): + files = sorted([f for f in os.listdir(outdoor_path) if f.endswith('.png')]) + for img_file in files: + img_path = os.path.join(outdoor_path, img_file) + try: + backgrounds["outdoor_paths"].append(img_path) + with Image.open(img_path) as img: + img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB') + thumb = img.copy() + thumb.thumbnail((240, 240), Image.Resampling.LANCZOS) + backgrounds["outdoor"].append(thumb) + backgrounds["outdoor_pil"].append(img_path) + except Exception: + pass + + # 이미지가 없는 경우 더미 이미지 생성 + if len(backgrounds['indoor']) == 0: + dummy = Image.new('RGB', (240, 240), color=(200, 200, 200)) + backgrounds["indoor"].append(dummy) + backgrounds["indoor_pil"].append(None) + + if len(backgrounds['outdoor']) == 0: + dummy = Image.new('RGB', (240, 240), color=(150, 200, 150)) + backgrounds["outdoor"].append(dummy) + backgrounds["outdoor_pil"].append(None) + + return backgrounds + +bg_images = load_background_images() + +# 얼굴 이미지 로드 +def load_face_images(): + """얼굴 이미지들을 로드 - PIL 이미지로 직접 로드하여 갤러리에 전달""" + faces = { + "man_pil": [], + "woman_pil": [], + "all_pil": [], + "man_paths": [], + "woman_paths": [], + "all_paths": [] + } + + cwd = os.getcwd() + + # Man 이미지 로드 + man_path = os.path.join(cwd, "assets", "faces", "man") + if os.path.exists(man_path): + files = sorted([f for f in os.listdir(man_path) if f.endswith(('.png', '.jpg', '.jpeg'))]) + for img_file in files: + img_path = os.path.join(man_path, img_file) + try: + with Image.open(img_path) as img: + img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB') + thumb = img.copy() + thumb.thumbnail((240, 240), Image.Resampling.LANCZOS) + faces["man_pil"].append(thumb) + faces["man_paths"].append(img_path) + except Exception: + pass + + # Woman 이미지 로드 + woman_path = os.path.join(cwd, "assets", "faces", "woman") + if os.path.exists(woman_path): + files = sorted([f for f in os.listdir(woman_path) if f.endswith(('.png', '.jpg', '.jpeg'))]) + for img_file in files: + img_path = os.path.join(woman_path, img_file) + try: + with Image.open(img_path) as img: + img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB') + thumb = img.copy() + thumb.thumbnail((240, 240), Image.Resampling.LANCZOS) + faces["woman_pil"].append(thumb) + faces["woman_paths"].append(img_path) + except Exception: + pass + + # 이미지가 없는 경우 더미 이미지 생성 + if len(faces['man_pil']) == 0: + for _ in range(3): + dummy = Image.new('RGB', (240, 240), color=(200, 200, 250)) + faces["man_pil"].append(dummy) + faces["man_paths"].append(None) + + if len(faces['woman_pil']) == 0: + for _ in range(3): + dummy = Image.new('RGB', (240, 240), color=(250, 200, 200)) + faces["woman_pil"].append(dummy) + faces["woman_paths"].append(None) + + # all 리스트 재구성 + faces["all_pil"] = faces["man_pil"] + faces["woman_pil"] + faces["all_paths"] = faces["man_paths"] + faces["woman_paths"] + + return faces + +face_images = load_face_images() +logger.info(f"Face images loaded - Total: {len(face_images.get('all_pil', []))}, Man: {len(face_images.get('man_pil', []))}, Woman: {len(face_images.get('woman_pil', []))}") + +# 이미지를 실제로 로드하는 헬퍼 함수 +def load_full_image(path_or_image): + """경로에서 전체 크기 이미지를 로드 (캐싱 포함)""" + if isinstance(path_or_image, str): + cached = image_cache.get(path_or_image) + if cached is not None: + image_cache.move_to_end(path_or_image) + return cached + + try: + with Image.open(path_or_image) as img: + mode = 'RGBA' if 'A' in img.getbands() else 'RGB' + loaded = img.convert(mode).copy() + except Exception as exc: + logger.error(f"Failed to load image {path_or_image}: {exc}") + return None + + cache_image(path_or_image, loaded) + return loaded + else: + return path_or_image + + +def create_app(): + """PixelFree Gradio UI (v3.0.0).""" + + # CSS for history gallery image display + custom_css = """ + #history-gallery img { + object-fit: contain !important; + height: 100% !important; + width: 100% !important; + } + + #history-gallery .thumbnail-item { + height: 150px !important; + } + + #history-gallery .grid-wrap { + gap: 10px !important; + } + """ + + with gr.Blocks(title="PixelFree v3.0.0", theme=gr.themes.Soft(), css=custom_css) as demo: + + # 메인 헤더 + gr.Markdown("# 🎨 PixelFree Studio") + + # 입력 섹션 + gr.Markdown("## 📸 입력 이미지") + + input_state = gr.State(None) + bg_removed_state = gr.State(None) + selected_bg_state = gr.State(None) + selected_face_state = gr.State(None) + last_output_state = gr.State(None) + + input_image = gr.Image(label="입력 이미지", type="pil", height=540) + + # 결과 섹션 + gr.Markdown("## ✨ 결과 이미지") + output_image = gr.Image(label="처리된 이미지", type="pil", height=500, interactive=False) + download_btn = gr.DownloadButton( + "📥 사진 다운로드", + value=None, + interactive=False + ) + + # 비디오 생성 섹션 + gr.Markdown("## 🎬 AI 비디오 생성") + with gr.Row(): + video_output = gr.Video( + label="생성된 영상", + height=360, + interactive=False, + autoplay=True, + ) + with gr.Column(): + gr.Markdown("최근 이미지 결과를 기반으로 짧은 클립을 생성합니다.") + generate_video_btn = gr.Button( + "🎬 영상 생성하기", + variant="primary", + size="lg", + interactive=False, + ) + + # 배경 제거 섹션 + gr.Markdown("## 🎯 AI 배경 제거") + remove_bg_btn = gr.Button("🚀 배경 제거 실행", variant="primary", size="lg", interactive=False) + + # AI 생성 기능 섹션 + gr.Markdown("## 🎨 AI 생성 기능") + with gr.Row(): + with gr.Column(): + gr.Markdown("### 🏞️ 배경 생성") + with gr.Tabs(): + with gr.Tab("🏠 실내"): + indoor_gallery = gr.Gallery( + value=bg_images["indoor"], + columns=4, + show_label=False, interactive=False, + allow_preview=False, + object_fit="contain" + ) + with gr.Tab("🌳 아웃도어"): + outdoor_gallery = gr.Gallery( + value=bg_images["outdoor"], + columns=4, + rows=4, show_label=False, interactive=False, + allow_preview=False, + object_fit="contain" + ) + hq_checkbox = gr.Checkbox(label="✨ 고품질 모드", value=False) + generate_bg_btn = gr.Button("🎨 배경 생성하기", variant="secondary", interactive=False, size="lg") + with gr.Column(): + gr.Markdown("### 👤 얼굴 교체") + with gr.Tabs(): + with gr.Tab("👨 남성"): + man_gallery = gr.Gallery( + value=face_images["man_pil"], + columns=4, + rows=2, show_label=False, interactive=False, + allow_preview=False, + object_fit="contain" + ) + with gr.Tab("👩 여성"): + woman_gallery = gr.Gallery( + value=face_images["woman_pil"], + columns=4, + rows=2, show_label=False, interactive=False, + allow_preview=False, + object_fit="contain" + ) + swap_face_btn = gr.Button("🔄 얼굴 교체 적용", variant="secondary", interactive=False, size="lg") + # Collapsible history section with image grid + with gr.Accordion("📜 작업 히스토리", open=False): + # History gallery with preview enabled + history_gallery = gr.Gallery( + value=[img for img, _ in get_history_items(20)], + label=None, + show_label=False, + columns=5, + rows=4, + height=500, + object_fit="contain", + allow_preview=True, + elem_id="history-gallery", + interactive=False + ) + + def make_download_update(image: Optional[Image.Image], prefix: str = "pixelfree_output"): + if image is None: + return gr.update(value=None, interactive=False) + filename = f"{prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + temp_path = f"/tmp/{filename}" + image.save(temp_path, "PNG") + return gr.update(value=temp_path, interactive=True) + + def history_update(): + """Update history gallery with original images.""" + items = get_history_items(20) + images = [img for img, _ in items] + return gr.update(value=images) + + def disable_all_buttons(): + disabled = gr.update(interactive=False) + return disabled, disabled, disabled, disabled + + def compute_button_states( + input_img: Optional[Image.Image], + last_output: Optional[Image.Image], + bg_removed: Optional[Image.Image], + selected_bg: Optional[Image.Image], + selected_face: Optional[Image.Image], + ): + remove_ready = gr.update(interactive=input_img is not None) + video_ready = gr.update(interactive=input_img is not None) + bg_ready = gr.update(interactive=(bg_removed is not None and selected_bg is not None)) + swap_ready = gr.update( + interactive=(selected_face is not None and ((last_output is not None) or (input_img is not None))) + ) + return remove_ready, video_ready, bg_ready, swap_ready + + + def on_upload(new_image: Optional[Image.Image]): + if new_image is None: + gr.Warning("입력 이미지를 업로드해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = disable_all_buttons() + return ( + None, + None, + None, + None, + None, + gr.update(value=None), + make_download_update(None), + gr.update(value=None), + remove_ready, + video_ready, + bg_ready, + swap_ready, + history_update(), + ) + + gr.Info("입력 이미지를 불러왔습니다.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + new_image, + None, + None, + None, + None, + ) + return ( + new_image, + None, + None, + None, + None, + gr.update(value=None), + make_download_update(None), + gr.update(value=None), + remove_ready, + video_ready, + bg_ready, + swap_ready, + history_update(), + ) + + def handle_remove_background( + input_img: Optional[Image.Image], + current_bg_removed: Optional[Image.Image], + selected_bg: Optional[Image.Image], + current_output: Optional[Image.Image], + selected_face: Optional[Image.Image], + ): + if input_img is None: + gr.Warning("입력 이미지를 먼저 업로드해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + current_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + current_bg_removed, + current_output, + gr.update(), + make_download_update(current_output), + bg_ready, + swap_ready, + remove_ready, + video_ready, + history_update(), + ) + return + + remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons() + yield ( + current_bg_removed, + current_output, + gr.update(), + make_download_update(current_output), + bg_disabled, + swap_disabled, + remove_disabled, + video_disabled, + history_update(), + ) + + removed, message = processor.remove_background(input_img) + if removed is None: + gr.Warning(message) + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + current_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + current_bg_removed, + current_output, + gr.update(value=current_output), + make_download_update(current_output), + bg_ready, + swap_ready, + remove_ready, + video_ready, + history_update(), + ) + return + + gr.Info(message) + add_to_history(removed, "bg_remove") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + removed, + removed, + selected_bg, + selected_face, + ) + yield ( + removed, + removed, + gr.update(value=removed), + make_download_update(removed, "background_removed"), + bg_ready, + swap_ready, + remove_ready, + video_ready, + history_update(), + ) + + def on_generate_video( + last_output: Optional[Image.Image], + input_img: Optional[Image.Image], + current_bg_removed: Optional[Image.Image], + selected_bg: Optional[Image.Image], + selected_face: Optional[Image.Image], + ): + base_image = last_output or input_img + if base_image is None: + gr.Warning("입력 이미지를 먼저 업로드해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + gr.update(value=None), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons() + yield ( + gr.update(), + history_update(), + remove_disabled, + video_disabled, + bg_disabled, + swap_disabled, + ) + + video_path, message = processor.generate_video(base_image) + if video_path is None: + gr.Warning(message) + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + gr.update(value=None), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + gr.Info(message) + add_to_history(base_image, "video") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + gr.update(value=video_path), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + + def load_background_image(category: str, index: int) -> Optional[Image.Image]: + key = f"{category}_pil" + paths = bg_images.get(key, []) + if 0 <= index < len(paths): + path = paths[index] + return load_full_image(path) + return None + + def on_select_background( + evt: gr.SelectData, + category: str, + bg_removed: Optional[Image.Image], + ): + index = getattr(evt, "index", None) + if index is None: + gr.Warning("선택 정보를 확인할 수 없습니다.") + return None, gr.update(interactive=False) + image = load_background_image(category, index) + if image is None: + gr.Warning("선택한 배경 이미지를 불러오지 못했습니다.") + return None, gr.update(interactive=False) + gr.Info("배경 이미지를 선택했습니다.") + ready = gr.update(interactive=bg_removed is not None) + return image, ready + + def on_generate_background( + last_output: Optional[Image.Image], + selected_bg: Optional[Image.Image], + hq_mode: bool, + input_img: Optional[Image.Image], + current_bg_removed: Optional[Image.Image], + selected_face: Optional[Image.Image], + ): + if last_output is None: + gr.Warning("먼저 배경 제거를 실행해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + last_output, + gr.update(value=last_output), + make_download_update(last_output), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + if selected_bg is None: + gr.Warning("사용할 배경 이미지를 선택해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + last_output, + gr.update(value=last_output), + make_download_update(last_output), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons() + yield ( + last_output, + gr.update(value=last_output), + make_download_update(last_output), + history_update(), + remove_disabled, + video_disabled, + bg_disabled, + swap_disabled, + ) + + generated, message = processor.generate_background(last_output, selected_bg, hq_mode) + if generated is None: + gr.Warning(message) + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + last_output, + gr.update(value=last_output), + make_download_update(last_output), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + gr.Info(message) + add_to_history(generated, "bg_gen") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + generated, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + generated, + gr.update(value=generated), + make_download_update(generated, "background_generated"), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + + def load_face_image(category: str, index: int) -> Optional[Image.Image]: + key = f"{category}_paths" + paths = face_images.get(key, []) + if 0 <= index < len(paths): + path = paths[index] + return load_full_image(path) + return None + + def on_select_face( + evt: gr.SelectData, + category: str, + last_output: Optional[Image.Image], + input_img: Optional[Image.Image], + ): + base_image = last_output or input_img + index = getattr(evt, "index", None) + if index is None: + gr.Warning("선택 정보를 확인할 수 없습니다.") + return None, gr.update(interactive=False) + image = load_face_image(category, index) + if image is None: + gr.Warning("선택한 얼굴 이미지를 불러오지 못했습니다.") + return None, gr.update(interactive=False) + gr.Info("얼굴 이미지를 선택했습니다.") + ready = gr.update(interactive=base_image is not None) + return image, ready + + def on_swap_face( + last_output: Optional[Image.Image], + input_img: Optional[Image.Image], + selected_face: Optional[Image.Image], + current_bg_removed: Optional[Image.Image], + selected_bg: Optional[Image.Image], + ): + base_image = last_output or input_img + if base_image is None: + gr.Warning("입력 이미지를 먼저 업로드해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + last_output, + gr.update(), + make_download_update(last_output), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + if selected_face is None: + gr.Warning("사용할 얼굴 이미지를 선택해주세요.") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + last_output, + gr.update(value=base_image), + make_download_update(last_output), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons() + yield ( + last_output, + gr.update(value=base_image), + make_download_update(last_output), + history_update(), + remove_disabled, + video_disabled, + bg_disabled, + swap_disabled, + ) + + swapped, message = processor.swap_face(base_image, selected_face) + if swapped is None: + gr.Warning(message) + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + last_output, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + last_output, + gr.update(value=base_image), + make_download_update(last_output), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + return + + gr.Info(message) + add_to_history(swapped, "face_swap") + remove_ready, video_ready, bg_ready, swap_ready = compute_button_states( + input_img, + swapped, + current_bg_removed, + selected_bg, + selected_face, + ) + yield ( + swapped, + gr.update(value=swapped), + make_download_update(swapped, "face_swap"), + history_update(), + remove_ready, + video_ready, + bg_ready, + swap_ready, + ) + + input_image.change( + on_upload, + inputs=[input_image], + outputs=[ + input_state, + bg_removed_state, + selected_bg_state, + selected_face_state, + last_output_state, + output_image, + download_btn, + video_output, + remove_bg_btn, + generate_video_btn, + generate_bg_btn, + swap_face_btn, + history_gallery, + ], + ) + + remove_bg_btn.click( + handle_remove_background, + inputs=[ + input_state, + bg_removed_state, + selected_bg_state, + last_output_state, + selected_face_state, + ], + outputs=[ + bg_removed_state, + last_output_state, + output_image, + download_btn, + generate_bg_btn, + swap_face_btn, + remove_bg_btn, + generate_video_btn, + history_gallery, + ], + ) + + generate_video_btn.click( + on_generate_video, + inputs=[ + last_output_state, + input_state, + bg_removed_state, + selected_bg_state, + selected_face_state, + ], + outputs=[ + video_output, + history_gallery, + remove_bg_btn, + generate_video_btn, + generate_bg_btn, + swap_face_btn, + ], + ) + + indoor_gallery.select( + on_select_background, + inputs=[gr.State("indoor"), bg_removed_state], + outputs=[selected_bg_state, generate_bg_btn], + ) + + outdoor_gallery.select( + on_select_background, + inputs=[gr.State("outdoor"), bg_removed_state], + outputs=[selected_bg_state, generate_bg_btn], + ) + + generate_bg_btn.click( + on_generate_background, + inputs=[ + last_output_state, + selected_bg_state, + hq_checkbox, + input_state, + bg_removed_state, + selected_face_state, + ], + outputs=[ + last_output_state, + output_image, + download_btn, + history_gallery, + remove_bg_btn, + generate_video_btn, + generate_bg_btn, + swap_face_btn, + ], + ) + + man_gallery.select( + on_select_face, + inputs=[gr.State("man"), last_output_state, input_state], + outputs=[selected_face_state, swap_face_btn], + ) + + woman_gallery.select( + on_select_face, + inputs=[gr.State("woman"), last_output_state, input_state], + outputs=[selected_face_state, swap_face_btn], + ) + + swap_face_btn.click( + on_swap_face, + inputs=[ + last_output_state, + input_state, + selected_face_state, + bg_removed_state, + selected_bg_state, + ], + outputs=[ + last_output_state, + output_image, + download_btn, + history_gallery, + remove_bg_btn, + generate_video_btn, + generate_bg_btn, + swap_face_btn, + ], + ) + + + return demo + +if __name__ == "__main__": + app = create_app() + app.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + show_error=True, + auth=auth_fn + ) diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..50c9f712bc6f431580c3da911f13152186ed07f9 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/POLYGOM_VIDEO_ver2_original.mp4 b/assets/POLYGOM_VIDEO_ver2_original.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..ec6bb083c68bb0bf03f051f1edc17f8f009c52dc --- /dev/null +++ b/assets/POLYGOM_VIDEO_ver2_original.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d042e834dac52034113fd56d3e9d950bfb3a85c1386dccfd84d6b3e88c80957 +size 14848608 diff --git a/assets/faces/.DS_Store b/assets/faces/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c1c123f113de0e79c8ee73666c69fd1a973121df Binary files /dev/null and b/assets/faces/.DS_Store differ diff --git a/assets/faces/man/001.png b/assets/faces/man/001.png new file mode 100644 index 0000000000000000000000000000000000000000..16592afc61fd9fa03a39225d7a9d76f246166d5c --- /dev/null +++ b/assets/faces/man/001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33cec6e6305c5361a2d82338d71e0f68497c522eaf642cedb1a684b8585a652d +size 611034 diff --git a/assets/faces/man/002.png b/assets/faces/man/002.png new file mode 100644 index 0000000000000000000000000000000000000000..bc07f737d15069bf1057babc39fb7d3a48318bf9 --- /dev/null +++ b/assets/faces/man/002.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1da5a08e127140e40b2ae6dc379690c56996632b75bc9cb5aef4b422e77e73c +size 1110727 diff --git a/assets/faces/man/003.png b/assets/faces/man/003.png new file mode 100644 index 0000000000000000000000000000000000000000..b9477ebb90e94198be879a7b2d40adb39ab5a348 --- /dev/null +++ b/assets/faces/man/003.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efa2386f3bb1380f01fd5956db70fecd6118ac1f7a223975a190dbe91c0582fc +size 1330906 diff --git a/assets/faces/man/004.png b/assets/faces/man/004.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bd7f1b959e5ff19b6cce9c3106fd910781b985 --- /dev/null +++ b/assets/faces/man/004.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cc05b4094c08e0c78a2c9404193c890e9192dc0185b508803ab6d041817427d +size 1295404 diff --git a/assets/faces/man/005.png b/assets/faces/man/005.png new file mode 100644 index 0000000000000000000000000000000000000000..62c63baff85b23ad392e533c5925df1248e76474 --- /dev/null +++ b/assets/faces/man/005.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52b74f683dbd867094a3d61d4defd34e731619cad190fde9d97aad01a5d461e0 +size 1306470 diff --git a/assets/faces/man/006.png b/assets/faces/man/006.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2e5c8c0e6b3fc7b35179e50cbf6c620e1b5e81 --- /dev/null +++ b/assets/faces/man/006.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e09a5409705d48d4fbaa37981ff3ac4aff98b4ae2efe501b88df7b5894472eea +size 1320689 diff --git a/assets/faces/man/007.png b/assets/faces/man/007.png new file mode 100644 index 0000000000000000000000000000000000000000..3f084b4c23afbe149154d234509b92e4a645d698 --- /dev/null +++ b/assets/faces/man/007.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00c008c2334712baaa40bbfd17b324884322a153b8844716f78952aa23facdee +size 1072793 diff --git a/assets/faces/man/008.png b/assets/faces/man/008.png new file mode 100644 index 0000000000000000000000000000000000000000..5d0fcc26826c7ceaf2af2a42da81178ce4ff89d5 --- /dev/null +++ b/assets/faces/man/008.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cf8ee93858c559ff8ac1e9f20d1beae4fcef2f9f6b62141edefdf20616d7516 +size 1711648 diff --git a/assets/faces/woman/001.png b/assets/faces/woman/001.png new file mode 100644 index 0000000000000000000000000000000000000000..d27b5439689d8537b45a028a95c619bf7099f440 --- /dev/null +++ b/assets/faces/woman/001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ba2de2df8f5ab9cb741a857bf2b2eaee10e34067fd81e89009e81ccff12b011 +size 1166268 diff --git a/assets/faces/woman/002.png b/assets/faces/woman/002.png new file mode 100644 index 0000000000000000000000000000000000000000..c597099f0bab301313b88c70316b9e6d84edda98 --- /dev/null +++ b/assets/faces/woman/002.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14ce72d69eb7b16dbf9d09167fe80f02747eee08199039c4eaf9e8695d9e7481 +size 1121384 diff --git a/assets/faces/woman/003.png b/assets/faces/woman/003.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce3387dd175da0235332a9248ba5dd91dcb0c7c --- /dev/null +++ b/assets/faces/woman/003.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b704f984433b5da3e61d73d840432c204792828ec9e611f98d27ef06ae772ce +size 1557225 diff --git a/assets/faces/woman/004.png b/assets/faces/woman/004.png new file mode 100644 index 0000000000000000000000000000000000000000..be1bc3d8fee50cb6ffbfda41581efaeb0199d1a3 --- /dev/null +++ b/assets/faces/woman/004.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e30ed1a55089a8e0b1a91471858edfff47cce2a2696bcfed2bec1ba1223ce8a6 +size 1085956 diff --git a/assets/faces/woman/005.png b/assets/faces/woman/005.png new file mode 100644 index 0000000000000000000000000000000000000000..d7bc5b8e0077173097c373d92b9ee3ccbe7aa665 --- /dev/null +++ b/assets/faces/woman/005.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bc6782310cfc93bf5ba905e8a50de93418535f994e7f4df0935b028bc348aa4 +size 1174661 diff --git a/assets/faces/woman/006.png b/assets/faces/woman/006.png new file mode 100644 index 0000000000000000000000000000000000000000..bf31e8961259a5574bbabceb8ac956d734370c84 --- /dev/null +++ b/assets/faces/woman/006.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5765c0da746e4b00ed357fc6158aad4880dcc8935e29e589e4b131b5b2cdb840 +size 1410455 diff --git a/assets/faces/woman/007.png b/assets/faces/woman/007.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8d5f5b3959067baf5f1248b917ada777de9f00 --- /dev/null +++ b/assets/faces/woman/007.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f870c70f38f8d8387821adcda60ee922c144163e34444ae313cf7b812807945 +size 1593121 diff --git a/assets/faces/woman/008.png b/assets/faces/woman/008.png new file mode 100644 index 0000000000000000000000000000000000000000..ade50d9bbd25dfeb3155aa80bec799fc1beaea89 --- /dev/null +++ b/assets/faces/woman/008.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f13c4cbb89ff372eff01bc589930c8f0919c6543691c0f5819cc6b64581940cc +size 1683328 diff --git a/assets/indoor/001.png b/assets/indoor/001.png new file mode 100644 index 0000000000000000000000000000000000000000..4b0ded551955da64a5f80ba8dee5dfe3f34a102f --- /dev/null +++ b/assets/indoor/001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e86be7c59d59a1e11e4fc0df8271944c6ef8cc9384069f3db245edd857ef11a2 +size 746759 diff --git a/assets/indoor/003.png b/assets/indoor/003.png new file mode 100644 index 0000000000000000000000000000000000000000..dc63b751817e59fed0d44935f6c6b45b0b113497 --- /dev/null +++ b/assets/indoor/003.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dab739162520e710ec74e1a848e02d4dc33a76808190496320de1bd99bc0e61 +size 838981 diff --git a/assets/indoor/004.png b/assets/indoor/004.png new file mode 100644 index 0000000000000000000000000000000000000000..b4670f97106c089cf4af560475da2d443672a3b6 --- /dev/null +++ b/assets/indoor/004.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06bdd47916183ab371d324a5786967f4d75b320e80e4ad2c814e303ed7867b94 +size 2993568 diff --git a/assets/indoor/005.png b/assets/indoor/005.png new file mode 100644 index 0000000000000000000000000000000000000000..52e25c3b439d61c7b6428822535c4d91d88aa0fb --- /dev/null +++ b/assets/indoor/005.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8d984cee48f2f12af6bbc5248fee7d4cd209071a5deb4dc32cf25a00bb0605a +size 2010555 diff --git a/assets/indoor/006.png b/assets/indoor/006.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4578901e345d9b3e744b8ad2ff63ad57ded4a0 --- /dev/null +++ b/assets/indoor/006.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cabbfe0546a772325f536cb38be24628fb9eded81bd7fe33d8e705cfbb488ed5 +size 1837065 diff --git a/assets/indoor/007.png b/assets/indoor/007.png new file mode 100644 index 0000000000000000000000000000000000000000..72e9e0be99d939c8cc1e94d3b79d3204353fd8b4 --- /dev/null +++ b/assets/indoor/007.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:644ffe8138c36261afa0d49d3e596992b4a7372d6ee76236902e887d2e14adb7 +size 608348 diff --git a/assets/indoor/008.png b/assets/indoor/008.png new file mode 100644 index 0000000000000000000000000000000000000000..7100112a10f3a96eceb2026e2c86a540e5889d82 --- /dev/null +++ b/assets/indoor/008.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:125536abd0eae586cd66604e56fef8932e482ae7d40fd6d1e5defbf4bde922fd +size 605223 diff --git a/assets/indoor/009.png b/assets/indoor/009.png new file mode 100644 index 0000000000000000000000000000000000000000..635a33283fc71450cd42f37015fcb9fee25d9153 --- /dev/null +++ b/assets/indoor/009.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31c10cc449cd49e420acf46dbbd3b89d2ed62986ac521420521a18ad2c7c42ad +size 2595931 diff --git a/assets/indoor/010.png b/assets/indoor/010.png new file mode 100644 index 0000000000000000000000000000000000000000..b392d6d00ba69f82d9e3fb90390c0214308af4eb --- /dev/null +++ b/assets/indoor/010.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11ce561a723ff4c73ff1efbc1eddbabdb27e7743f5d7a2848f5492471ec94dd4 +size 811846 diff --git a/assets/indoor/011.png b/assets/indoor/011.png new file mode 100644 index 0000000000000000000000000000000000000000..6ee54597836955715987e521c2dc1a4fa98a9820 --- /dev/null +++ b/assets/indoor/011.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0491a348dfb9221028f087e20df8107b7c3b9229ad8c07e9c7befaa4b6f60a1 +size 4475668 diff --git a/assets/indoor/012.png b/assets/indoor/012.png new file mode 100644 index 0000000000000000000000000000000000000000..c4a0c5303c4f18ab4fe7fcdcd5f54e7adf074cb6 --- /dev/null +++ b/assets/indoor/012.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2d556e9a3934b6e89ac987624ebefc58e639ff60af58bcf05bde5956861dd8e +size 996212 diff --git a/assets/outdoor/001.png b/assets/outdoor/001.png new file mode 100644 index 0000000000000000000000000000000000000000..879f4a2d206532f1755559a778cf94c24ace6c14 --- /dev/null +++ b/assets/outdoor/001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97d7a17e2ac12a8bb1c03b6da93e946b35f43f829fa0220420006416ce121071 +size 1102947 diff --git a/assets/outdoor/002.png b/assets/outdoor/002.png new file mode 100644 index 0000000000000000000000000000000000000000..a9625ebbefc60e1e4ad4de6cc0d9b35d37551da8 --- /dev/null +++ b/assets/outdoor/002.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be0ec40dcb88bdf714274213671656e9bdad8f02782eff5c9cdc00b6f2a0c7e0 +size 1185045 diff --git a/assets/outdoor/003.png b/assets/outdoor/003.png new file mode 100644 index 0000000000000000000000000000000000000000..abba4aeab16e87e76d020bb0c7e4d5ef13e34121 --- /dev/null +++ b/assets/outdoor/003.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2636192cbb8bf83d5679306e7a41693a09c5d9abb2916cd9c8585da057cf3024 +size 480306 diff --git a/assets/outdoor/004.png b/assets/outdoor/004.png new file mode 100644 index 0000000000000000000000000000000000000000..27cb1094c18e39401dee528c3940738016d889eb --- /dev/null +++ b/assets/outdoor/004.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:133c92ee439d10cee08187cbe10dd82d8b96c725a38224473cb2988f559eca6d +size 6038945 diff --git a/assets/outdoor/005.png b/assets/outdoor/005.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a63d0e791543ca7c4a41b2dfe87c969abf86cc --- /dev/null +++ b/assets/outdoor/005.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a3fa1a5c3d343069141772fb11713931d519b7d1622a8abc6cd0ee2f31a6e5a +size 1548042 diff --git a/assets/outdoor/006.png b/assets/outdoor/006.png new file mode 100644 index 0000000000000000000000000000000000000000..f54c0c4eb1284db9ec8ddb205b8a4df3766b0a0d --- /dev/null +++ b/assets/outdoor/006.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bfc4fc06db1d570c0e40ca438d1d2b13d0b5b292ae36d247f71439bf232ca4e +size 1424646 diff --git a/assets/outdoor/007.png b/assets/outdoor/007.png new file mode 100644 index 0000000000000000000000000000000000000000..363133eaebc3b9fab40a1f4b82a35d4e34d6e24b --- /dev/null +++ b/assets/outdoor/007.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77fd0afe2bc58b51a8d90932653ab79fa57e4515ea8ee46e8e99ccc0c999392b +size 1561655 diff --git a/assets/outdoor/008.png b/assets/outdoor/008.png new file mode 100644 index 0000000000000000000000000000000000000000..4b7fafbbbac064b54dd0a49ca2f2195eb9e36f56 --- /dev/null +++ b/assets/outdoor/008.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e09295e6e45926d00ef0daf690b210bc903c74727063650a5f8916028f1d69 +size 1737899 diff --git a/assets/outdoor/009.png b/assets/outdoor/009.png new file mode 100644 index 0000000000000000000000000000000000000000..0169eeb66cc4a89ac030377e52ade976c20ab385 --- /dev/null +++ b/assets/outdoor/009.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2de3552341d13faccfbccf0e1e2810f54bc197cf8c62db9a003a5f5b9f89e84d +size 1253017 diff --git a/assets/outdoor/010.png b/assets/outdoor/010.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd86aeb6378efec9f37b67b04256c53d234a6e8 --- /dev/null +++ b/assets/outdoor/010.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2964dbea528fdc10b044e51754bce6ac155a9e9edf83e05f9e403e90a8cc6adb +size 1303435 diff --git a/assets/outdoor/011.png b/assets/outdoor/011.png new file mode 100644 index 0000000000000000000000000000000000000000..9e0d666e894dcd9a28126b3d548e56be66398e8d --- /dev/null +++ b/assets/outdoor/011.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb7ff6156c13a1620384108bb410e14391c5f6c54f64aa984162fb8e040c09e +size 1413134 diff --git a/assets/outdoor/012.png b/assets/outdoor/012.png new file mode 100644 index 0000000000000000000000000000000000000000..9086b7658c511470ba1e831094c9b714667d3826 --- /dev/null +++ b/assets/outdoor/012.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f66cabade7f37b3f7ed5223a2149fd616b70ea11d0acee3d20a9d3441d4ffd +size 1013801 diff --git a/assets/outdoor/013.png b/assets/outdoor/013.png new file mode 100644 index 0000000000000000000000000000000000000000..2f3cd1985682e1f27ccbc3d9e6cc0c846de6794c --- /dev/null +++ b/assets/outdoor/013.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c0136bf396eba40cfb7ab00ae3ad6377703bad1f7d249120d3b7e5a80bf1d93 +size 1325034 diff --git a/assets/outdoor/014.png b/assets/outdoor/014.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4cc24e1d0c18db540179e0a0e694f6224abc4d --- /dev/null +++ b/assets/outdoor/014.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fee389d93c8903f70526acdfd41f4fc7edd301765fb1247fa42876882d32fccd +size 1103513 diff --git a/assets/outdoor/015.png b/assets/outdoor/015.png new file mode 100644 index 0000000000000000000000000000000000000000..a87cd953b307370e2b65be1611a5a3999efdeccd --- /dev/null +++ b/assets/outdoor/015.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fba97edd4f55735a58c4e8a0de49b87453c70d011e0fc59558e31fda39197782 +size 6354729 diff --git a/assets/outdoor/016.png b/assets/outdoor/016.png new file mode 100644 index 0000000000000000000000000000000000000000..eb2c287dab94ad73c788491a4e10a86ce29aa807 --- /dev/null +++ b/assets/outdoor/016.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:000051055b8b10761a194808c9106f11bec18d35c5c1b27bbd86bb999162086d +size 1596132 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..178333be418e7fc100b8318ee43ea070c38c6890 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +gradio==5.45.0 +Pillow>=10.0.0 +numpy>=1.24.0 +requests>=2.31.0 \ No newline at end of file