N_I / app.py
ssboost's picture
Update app.py
6e2dd20 verified
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 = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
"""
# 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('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/3097/3097412.png"> ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</div>')
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('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/1375/1375106.png"> ๋ณ€ํ˜•๋œ ์ด๋ฏธ์ง€</div>')
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('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/1022/1022293.png"> ๋ณ€ํ˜• ์˜ต์…˜</div>')
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('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/3153/3153376.png"> ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ</div>')
download_file = gr.File(label="ZIP ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ")
# ์ ์šฉ๋œ ๋ณ€ํ˜• ๋ฐฉ๋ฒ• ์ •๋ณด
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/4297/4297825.png"> ์ ์šฉ๋œ ๋ณ€ํ˜• ์ •๋ณด</div>')
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%")