Spaces:
Sleeping
Sleeping
Commit
·
019b718
0
Parent(s):
improve the model and fix girls try on issue
Browse files- .gitattributes +36 -0
- .gitignore +7 -0
- Dockerfile +36 -0
- README.md +11 -0
- app.py +321 -0
- requirements.txt +12 -0
- runtime.txt +1 -0
- static/outputs/.gitkeep +0 -0
- static/uploads/.gitkeep +0 -0
- templates/index.html +339 -0
.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
img/young-male-entrepreneur-making-eye-contact-against-blue-background.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ignore user uploads and generated output
|
| 2 |
+
static/uploads/*
|
| 3 |
+
static/outputs/*
|
| 4 |
+
|
| 5 |
+
# But keep the directories themselves
|
| 6 |
+
!static/uploads/.gitkeep
|
| 7 |
+
!static/outputs/.gitkeep
|
Dockerfile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Set cache directories and performance optimizations
|
| 6 |
+
ENV HF_HOME=/tmp/.cache
|
| 7 |
+
ENV MPLCONFIGDIR=/tmp/.cache
|
| 8 |
+
ENV OMP_NUM_THREADS=4
|
| 9 |
+
ENV TOKENIZERS_PARALLELISM=false
|
| 10 |
+
|
| 11 |
+
# Install dependencies including CUDA support
|
| 12 |
+
RUN apt-get update && apt-get install -y \
|
| 13 |
+
libgl1-mesa-glx \
|
| 14 |
+
libglib2.0-0 \
|
| 15 |
+
libgomp1 \
|
| 16 |
+
&& apt-get clean \
|
| 17 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
+
|
| 19 |
+
# Create upload and output directories
|
| 20 |
+
RUN mkdir -p /tmp/uploads /tmp/outputs && chmod -R 777 /tmp/uploads /tmp/outputs
|
| 21 |
+
|
| 22 |
+
# Install Python dependencies
|
| 23 |
+
COPY requirements.txt .
|
| 24 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Create temporary cache directory (will be created at runtime)
|
| 27 |
+
RUN mkdir -p /tmp/.cache && chmod -R 777 /tmp/.cache
|
| 28 |
+
|
| 29 |
+
# Copy app code
|
| 30 |
+
COPY . .
|
| 31 |
+
|
| 32 |
+
# Set port
|
| 33 |
+
ENV PORT=7860
|
| 34 |
+
|
| 35 |
+
# Run with Gunicorn with optimized settings
|
| 36 |
+
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "4", "--worker-class", "gthread", "--timeout", "300"]
|
README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: V Try On
|
| 3 |
+
emoji: 👁
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
short_description: virtual try on
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, send_from_directory, session, redirect, url_for
|
| 2 |
+
from PIL import Image
|
| 3 |
+
import os, torch, cv2, mediapipe as mp
|
| 4 |
+
from transformers import SamModel, SamProcessor, logging as hf_logging
|
| 5 |
+
from torchvision import transforms
|
| 6 |
+
from diffusers.utils import load_image
|
| 7 |
+
from flask_cors import CORS
|
| 8 |
+
import json
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
app = Flask(__name__)
|
| 12 |
+
app.secret_key = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
|
| 13 |
+
CORS(app)
|
| 14 |
+
|
| 15 |
+
# Enable Hugging Face logs
|
| 16 |
+
hf_logging.set_verbosity_info()
|
| 17 |
+
|
| 18 |
+
UPLOAD_FOLDER = '/tmp/uploads'
|
| 19 |
+
OUTPUT_FOLDER = '/tmp/outputs'
|
| 20 |
+
|
| 21 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 22 |
+
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
| 23 |
+
|
| 24 |
+
# Global model vars
|
| 25 |
+
model, processor, device = None, None, None
|
| 26 |
+
|
| 27 |
+
def load_model():
|
| 28 |
+
"""Load SAM model (CPU only)."""
|
| 29 |
+
global model, processor, device
|
| 30 |
+
device = "cpu"
|
| 31 |
+
print(f"[INFO] Using device: {device}")
|
| 32 |
+
model = SamModel.from_pretrained(
|
| 33 |
+
"Zigeng/SlimSAM-uniform-50",
|
| 34 |
+
cache_dir="/tmp/.cache",
|
| 35 |
+
torch_dtype=torch.float32,
|
| 36 |
+
)
|
| 37 |
+
processor = SamProcessor.from_pretrained("Zigeng/SlimSAM-uniform-50", cache_dir="/tmp/.cache")
|
| 38 |
+
print("[INFO] Model loaded successfully!")
|
| 39 |
+
|
| 40 |
+
def cleanup_temp_files():
|
| 41 |
+
import shutil
|
| 42 |
+
try:
|
| 43 |
+
if os.path.exists("/tmp/.cache"):
|
| 44 |
+
shutil.rmtree("/tmp/.cache")
|
| 45 |
+
print("[INFO] Cleaned up cache")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"[WARNING] Cleanup failed: {e}")
|
| 48 |
+
|
| 49 |
+
def cleanup_old_outputs():
|
| 50 |
+
try:
|
| 51 |
+
if os.path.exists(OUTPUT_FOLDER):
|
| 52 |
+
for file in os.listdir(OUTPUT_FOLDER):
|
| 53 |
+
file_path = os.path.join(OUTPUT_FOLDER, file)
|
| 54 |
+
if os.path.isfile(file_path) and (time.time() - os.path.getctime(file_path) > 3600):
|
| 55 |
+
os.remove(file_path)
|
| 56 |
+
print(f"[INFO] Removed old file: {file}")
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"[WARNING] Cleanup outputs failed: {e}")
|
| 59 |
+
|
| 60 |
+
@app.before_request
|
| 61 |
+
def log_request_info():
|
| 62 |
+
print(f"[INFO] Incoming request: {request.method} {request.path}")
|
| 63 |
+
|
| 64 |
+
@app.route('/health')
|
| 65 |
+
def health():
|
| 66 |
+
return "OK", 200
|
| 67 |
+
|
| 68 |
+
@app.route('/outputs/<filename>')
|
| 69 |
+
def serve_output(filename):
|
| 70 |
+
file_path = os.path.join(OUTPUT_FOLDER, filename)
|
| 71 |
+
if not os.path.exists(file_path):
|
| 72 |
+
return "File not found", 404
|
| 73 |
+
if filename.lower().endswith(('.jpg', '.jpeg')):
|
| 74 |
+
mimetype = 'image/jpeg'
|
| 75 |
+
elif filename.lower().endswith('.png'):
|
| 76 |
+
mimetype = 'image/png'
|
| 77 |
+
else:
|
| 78 |
+
mimetype = 'application/octet-stream'
|
| 79 |
+
return send_from_directory(OUTPUT_FOLDER, filename, mimetype=mimetype)
|
| 80 |
+
|
| 81 |
+
@app.route('/uploads/<filename>')
|
| 82 |
+
def serve_upload(filename):
|
| 83 |
+
return send_from_directory(UPLOAD_FOLDER, filename)
|
| 84 |
+
|
| 85 |
+
def detect_upper_body_coordinates(person_path, is_female=False):
|
| 86 |
+
mp_pose = mp.solutions.pose
|
| 87 |
+
pose = mp_pose.Pose()
|
| 88 |
+
image = cv2.imread(person_path)
|
| 89 |
+
if image is None:
|
| 90 |
+
raise Exception("No image detected.")
|
| 91 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 92 |
+
results = pose.process(image_rgb)
|
| 93 |
+
if not results.pose_landmarks:
|
| 94 |
+
raise Exception("No pose detected.")
|
| 95 |
+
h, w, _ = image.shape
|
| 96 |
+
lm = results.pose_landmarks.landmark
|
| 97 |
+
|
| 98 |
+
# Get key body landmarks
|
| 99 |
+
left_shoulder = (int(lm[11].x * w), int(lm[11].y * h))
|
| 100 |
+
right_shoulder = (int(lm[12].x * w), int(lm[12].y * h))
|
| 101 |
+
left_hip = (int(lm[23].x * w), int(lm[23].y * h))
|
| 102 |
+
right_hip = (int(lm[24].x * w), int(lm[24].y * h))
|
| 103 |
+
|
| 104 |
+
# Calculate centers
|
| 105 |
+
shoulder_center_x = (left_shoulder[0] + right_shoulder[0]) // 2
|
| 106 |
+
shoulder_center_y = (left_shoulder[1] + right_shoulder[1]) // 2
|
| 107 |
+
hip_center_x = (left_hip[0] + right_hip[0]) // 2
|
| 108 |
+
hip_center_y = (left_hip[1] + right_hip[1]) // 2
|
| 109 |
+
|
| 110 |
+
if is_female:
|
| 111 |
+
# For girls: Focus on chest-to-hip area to avoid hair on shoulders
|
| 112 |
+
# Calculate chest points (below shoulders to avoid hair)
|
| 113 |
+
chest_offset = 80 # pixels below shoulders
|
| 114 |
+
left_chest_x = left_shoulder[0]
|
| 115 |
+
left_chest_y = left_shoulder[1] + chest_offset
|
| 116 |
+
right_chest_x = right_shoulder[0]
|
| 117 |
+
right_chest_y = right_shoulder[1] + chest_offset
|
| 118 |
+
|
| 119 |
+
# Chest center point
|
| 120 |
+
chest_center_x = (left_chest_x + right_chest_x) // 2
|
| 121 |
+
chest_center_y = (left_chest_y + right_chest_y) // 2
|
| 122 |
+
|
| 123 |
+
# Waist center point (between chest and hips)
|
| 124 |
+
waist_center_x = chest_center_x
|
| 125 |
+
waist_center_y = chest_center_y + (hip_center_y - chest_center_y) // 2
|
| 126 |
+
|
| 127 |
+
coords = {
|
| 128 |
+
"left_chest": (left_chest_x, left_chest_y),
|
| 129 |
+
"right_chest": (right_chest_x, right_chest_y),
|
| 130 |
+
"left_hip": left_hip,
|
| 131 |
+
"right_hip": right_hip,
|
| 132 |
+
"chest_center": (chest_center_x, chest_center_y),
|
| 133 |
+
"waist_center": (waist_center_x, waist_center_y),
|
| 134 |
+
"detection_type": "female_chest_to_hip"
|
| 135 |
+
}
|
| 136 |
+
else:
|
| 137 |
+
# For boys: Use shoulder-based detection (original approach)
|
| 138 |
+
chest_center_x = shoulder_center_x
|
| 139 |
+
chest_center_y = shoulder_center_y + (hip_center_y - shoulder_center_y) // 3
|
| 140 |
+
waist_center_x = shoulder_center_x
|
| 141 |
+
waist_center_y = shoulder_center_y + 2 * (hip_center_y - shoulder_center_y) // 3
|
| 142 |
+
|
| 143 |
+
coords = {
|
| 144 |
+
"left_shoulder": left_shoulder,
|
| 145 |
+
"right_shoulder": right_shoulder,
|
| 146 |
+
"left_hip": left_hip,
|
| 147 |
+
"right_hip": right_hip,
|
| 148 |
+
"chest_center": (chest_center_x, chest_center_y),
|
| 149 |
+
"waist_center": (waist_center_x, waist_center_y),
|
| 150 |
+
"shoulder_center": (shoulder_center_x, shoulder_center_y),
|
| 151 |
+
"detection_type": "male_shoulder_based"
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return coords
|
| 155 |
+
|
| 156 |
+
@app.route('/', methods=['GET', 'POST'])
|
| 157 |
+
def index():
|
| 158 |
+
start_time = time.time()
|
| 159 |
+
if request.method == 'POST':
|
| 160 |
+
try:
|
| 161 |
+
load_model()
|
| 162 |
+
|
| 163 |
+
# Person image handling
|
| 164 |
+
use_cached_person = 'person_coordinates' in session and 'person_image_path' in session
|
| 165 |
+
cached_person_flag = use_cached_person
|
| 166 |
+
person_path = None
|
| 167 |
+
person_disk_path = os.path.join(UPLOAD_FOLDER, 'person.jpg')
|
| 168 |
+
|
| 169 |
+
# Get gender selection from form
|
| 170 |
+
is_female = request.form.get('gender') == 'female'
|
| 171 |
+
print(f"[INFO] Gender selection: {'Female' if is_female else 'Male'}")
|
| 172 |
+
|
| 173 |
+
if use_cached_person:
|
| 174 |
+
person_path = session['person_image_path']
|
| 175 |
+
person_coordinates = session['person_coordinates']
|
| 176 |
+
print(f"[INFO] Using cached person {person_path}")
|
| 177 |
+
else:
|
| 178 |
+
person_file = request.files.get('person_image')
|
| 179 |
+
if person_file and person_file.filename != '':
|
| 180 |
+
person_path = person_disk_path
|
| 181 |
+
person_file.save(person_path)
|
| 182 |
+
print(f"[INFO] Saved new person {person_path}")
|
| 183 |
+
elif os.path.exists(person_disk_path):
|
| 184 |
+
person_path = person_disk_path
|
| 185 |
+
print(f"[INFO] Reusing person {person_path}")
|
| 186 |
+
else:
|
| 187 |
+
return "No person image provided."
|
| 188 |
+
|
| 189 |
+
# Detect pose with gender-aware coordinates
|
| 190 |
+
coords = detect_upper_body_coordinates(person_path, is_female=is_female)
|
| 191 |
+
person_coordinates = coords.copy() # Use all coordinates from detection
|
| 192 |
+
|
| 193 |
+
# Cache
|
| 194 |
+
session['person_image_path'] = person_path
|
| 195 |
+
session['person_coordinates'] = person_coordinates
|
| 196 |
+
cached_person_flag = True
|
| 197 |
+
|
| 198 |
+
# Garment handling
|
| 199 |
+
tshirt_file = request.files['tshirt_image']
|
| 200 |
+
tshirt_path = os.path.join(UPLOAD_FOLDER, 'tshirt.png')
|
| 201 |
+
tshirt_file.save(tshirt_path)
|
| 202 |
+
|
| 203 |
+
# Inference
|
| 204 |
+
img = load_image(person_path)
|
| 205 |
+
new_tshirt = load_image(tshirt_path)
|
| 206 |
+
|
| 207 |
+
# Use cached or fresh coordinates
|
| 208 |
+
coords = person_coordinates
|
| 209 |
+
detection_type = coords.get('detection_type', 'male_shoulder_based')
|
| 210 |
+
|
| 211 |
+
# Create input points based on detection type
|
| 212 |
+
if detection_type == 'female_chest_to_hip':
|
| 213 |
+
# For girls: Use chest-to-hip points (avoiding shoulders/hair)
|
| 214 |
+
input_points = [[
|
| 215 |
+
list(coords['left_chest']),
|
| 216 |
+
list(coords['right_chest']),
|
| 217 |
+
list(coords['chest_center']),
|
| 218 |
+
list(coords['waist_center']),
|
| 219 |
+
list(coords['left_hip']),
|
| 220 |
+
list(coords['right_hip'])
|
| 221 |
+
]]
|
| 222 |
+
print("[INFO] Using female chest-to-hip detection (avoiding hair on shoulders)")
|
| 223 |
+
else:
|
| 224 |
+
# For boys: Use shoulder-based points
|
| 225 |
+
input_points = [[
|
| 226 |
+
list(coords['left_shoulder']),
|
| 227 |
+
list(coords['right_shoulder']),
|
| 228 |
+
list(coords['chest_center']),
|
| 229 |
+
list(coords['waist_center']),
|
| 230 |
+
list(coords['left_hip']),
|
| 231 |
+
list(coords['right_hip']),
|
| 232 |
+
list(coords['shoulder_center'])
|
| 233 |
+
]]
|
| 234 |
+
print("[INFO] Using male shoulder-based detection")
|
| 235 |
+
|
| 236 |
+
inputs = processor(img, input_points=input_points, return_tensors="pt")
|
| 237 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 238 |
+
|
| 239 |
+
with torch.no_grad():
|
| 240 |
+
outputs = model(**inputs)
|
| 241 |
+
|
| 242 |
+
masks = processor.image_processor.post_process_masks(
|
| 243 |
+
outputs.pred_masks.cpu(),
|
| 244 |
+
inputs["original_sizes"].cpu(),
|
| 245 |
+
inputs["reshaped_input_sizes"].cpu()
|
| 246 |
+
)
|
| 247 |
+
mask_tensor = masks[0][0][2].to(dtype=torch.uint8)
|
| 248 |
+
mask = transforms.ToPILImage()(mask_tensor * 255)
|
| 249 |
+
|
| 250 |
+
# Merge
|
| 251 |
+
new_tshirt = new_tshirt.resize(img.size, Image.LANCZOS)
|
| 252 |
+
img_with_new_tshirt = Image.composite(new_tshirt, img, mask)
|
| 253 |
+
result_path = os.path.join(OUTPUT_FOLDER, 'result.jpg')
|
| 254 |
+
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
| 255 |
+
img_with_new_tshirt.save(result_path)
|
| 256 |
+
|
| 257 |
+
# Unique result name
|
| 258 |
+
import uuid, shutil
|
| 259 |
+
unique_filename = f"result_{uuid.uuid4().hex[:8]}.jpg"
|
| 260 |
+
unique_result_path = os.path.join(OUTPUT_FOLDER, unique_filename)
|
| 261 |
+
shutil.copy2(result_path, unique_result_path)
|
| 262 |
+
|
| 263 |
+
processing_time = time.time() - start_time
|
| 264 |
+
cleanup_old_outputs()
|
| 265 |
+
|
| 266 |
+
return render_template(
|
| 267 |
+
'index.html',
|
| 268 |
+
result_img=f'/outputs/{unique_filename}',
|
| 269 |
+
cached_person=cached_person_flag,
|
| 270 |
+
person_image_path=person_path,
|
| 271 |
+
processing_time=f"{processing_time:.2f}s"
|
| 272 |
+
)
|
| 273 |
+
except Exception as e:
|
| 274 |
+
print(f"[ERROR] {e}")
|
| 275 |
+
return f"Error: {e}"
|
| 276 |
+
|
| 277 |
+
# GET
|
| 278 |
+
has_cached = 'person_coordinates' in session and 'person_image_path' in session
|
| 279 |
+
return render_template(
|
| 280 |
+
'index.html',
|
| 281 |
+
cached_person=has_cached,
|
| 282 |
+
person_image_path=session.get('person_image_path') if has_cached else None
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
@app.route('/change_person', methods=['POST'])
|
| 286 |
+
def change_person():
|
| 287 |
+
session.pop('person_coordinates', None)
|
| 288 |
+
session.pop('person_image_path', None)
|
| 289 |
+
try:
|
| 290 |
+
for f in ['person.jpg', 'tshirt.png']:
|
| 291 |
+
path = os.path.join(UPLOAD_FOLDER, f)
|
| 292 |
+
if os.path.exists(path):
|
| 293 |
+
os.remove(path)
|
| 294 |
+
for f in os.listdir(OUTPUT_FOLDER):
|
| 295 |
+
fp = os.path.join(OUTPUT_FOLDER, f)
|
| 296 |
+
if os.path.isfile(fp):
|
| 297 |
+
os.remove(fp)
|
| 298 |
+
print("[INFO] Cleared person data")
|
| 299 |
+
except Exception as e:
|
| 300 |
+
print(f"[WARNING] Failed to clear: {e}")
|
| 301 |
+
return redirect(url_for('index'))
|
| 302 |
+
|
| 303 |
+
@app.route('/cleanup', methods=['POST'])
|
| 304 |
+
def cleanup():
|
| 305 |
+
cleanup_temp_files()
|
| 306 |
+
cleanup_old_outputs()
|
| 307 |
+
return "Cleanup completed", 200
|
| 308 |
+
|
| 309 |
+
@app.route('/test-image')
|
| 310 |
+
def test_image():
|
| 311 |
+
from PIL import ImageDraw
|
| 312 |
+
img = Image.new('RGB', (200, 200), color='red')
|
| 313 |
+
draw = ImageDraw.Draw(img)
|
| 314 |
+
draw.text((50, 100), "TEST", fill='white')
|
| 315 |
+
test_path = os.path.join(OUTPUT_FOLDER, 'test.jpg')
|
| 316 |
+
img.save(test_path)
|
| 317 |
+
return f'<img src="/outputs/test.jpg" alt="Test">'
|
| 318 |
+
|
| 319 |
+
if __name__ == '__main__':
|
| 320 |
+
print("[INFO] Starting Flask server...")
|
| 321 |
+
app.run(debug=True, host='0.0.0.0')
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask
|
| 2 |
+
gunicorn
|
| 3 |
+
Pillow
|
| 4 |
+
opencv-python
|
| 5 |
+
torch
|
| 6 |
+
torchvision
|
| 7 |
+
mediapipe
|
| 8 |
+
transformers
|
| 9 |
+
diffusers
|
| 10 |
+
safetensors
|
| 11 |
+
flask-cors
|
| 12 |
+
accelerate
|
runtime.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
python-3.10.12
|
static/outputs/.gitkeep
ADDED
|
File without changes
|
static/uploads/.gitkeep
ADDED
|
File without changes
|
templates/index.html
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<title>Virtual Fashion Try-On</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<!-- Cropper.js CSS -->
|
| 9 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet">
|
| 10 |
+
</head>
|
| 11 |
+
|
| 12 |
+
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center py-10">
|
| 13 |
+
|
| 14 |
+
<h1 class="text-4xl font-bold text-blue-400 mb-10">Virtual Fashion Try-On</h1>
|
| 15 |
+
|
| 16 |
+
<div class="flex flex-col md:flex-row gap-10 w-full max-w-6xl">
|
| 17 |
+
|
| 18 |
+
<!-- LEFT: Input Form -->
|
| 19 |
+
<form id="tryon-form" action="/" method="post" enctype="multipart/form-data" class="w-full md:w-1/2 bg-gray-800 rounded-2xl shadow-lg p-8 space-y-6">
|
| 20 |
+
|
| 21 |
+
<div class="grid grid-cols-1 gap-8">
|
| 22 |
+
<!-- Person Image Upload -->
|
| 23 |
+
<div>
|
| 24 |
+
<div class="flex justify-between items-center mb-2">
|
| 25 |
+
<h2 class="text-lg font-semibold">Upload your photo</h2>
|
| 26 |
+
{% if cached_person %}
|
| 27 |
+
<button type="button" onclick="changePerson()" class="bg-yellow-500 hover:bg-yellow-600 text-white text-sm px-3 py-1 rounded-lg transition">
|
| 28 |
+
Change Person
|
| 29 |
+
</button>
|
| 30 |
+
{% endif %}
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
{% if cached_person %}
|
| 34 |
+
<!-- Show cached person image -->
|
| 35 |
+
<div class="border-2 border-green-500 rounded-xl p-4 bg-green-900/20">
|
| 36 |
+
<div class="flex flex-col items-center">
|
| 37 |
+
<div class="flex items-center gap-2 mb-2">
|
| 38 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 39 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
| 40 |
+
</svg>
|
| 41 |
+
<p class="text-green-400 text-sm font-medium">Person image cached</p>
|
| 42 |
+
</div>
|
| 43 |
+
<img src="/uploads/person.jpg" alt="Cached Person" class="max-h-32 rounded-lg border border-gray-600">
|
| 44 |
+
<p class="text-gray-400 text-xs mt-2">Coordinates saved - no need to re-upload!</p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
{% else %}
|
| 48 |
+
<label for="person_image" class="flex flex-col items-center justify-center border-2 border-dashed border-gray-600 rounded-xl p-6 hover:bg-gray-700 cursor-pointer">
|
| 49 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mb-2" fill="none"
|
| 50 |
+
viewBox="0 0 24 24" stroke="currentColor">
|
| 51 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 52 |
+
d="M7 16v4m0 0h10m-10 0v-4m0 0h10m-10 0V5m0 0h10m-10 0H5m14 0h-2" />
|
| 53 |
+
</svg>
|
| 54 |
+
<p class="text-gray-400">Drag & drop or click to upload</p>
|
| 55 |
+
<input id="person_image" type="file" name="person_image" class="hidden"
|
| 56 |
+
onchange="showFileName('person_image', 'person_filename', 'person_preview')">
|
| 57 |
+
</label>
|
| 58 |
+
<p id="person_filename" class="text-green-400 text-sm mt-2 text-center"></p>
|
| 59 |
+
<div class="mt-3 flex justify-center">
|
| 60 |
+
<img id="person_preview" class="hidden max-h-32 rounded-lg border border-gray-600">
|
| 61 |
+
</div>
|
| 62 |
+
{% endif %}
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<!-- Gender Selection -->
|
| 66 |
+
<div>
|
| 67 |
+
<h2 class="text-lg font-semibold mb-3">Select Gender</h2>
|
| 68 |
+
<div class="flex gap-4">
|
| 69 |
+
<label class="flex items-center cursor-pointer">
|
| 70 |
+
<input type="radio" name="gender" value="male" checked class="sr-only">
|
| 71 |
+
<div class="gender-option flex items-center gap-2 px-4 py-2 rounded-lg border-2 border-blue-500 bg-blue-500/20 text-blue-400">
|
| 72 |
+
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 73 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
| 74 |
+
</svg> -->
|
| 75 |
+
<span>Male</span>
|
| 76 |
+
</div>
|
| 77 |
+
</label>
|
| 78 |
+
<label class="flex items-center cursor-pointer">
|
| 79 |
+
<input type="radio" name="gender" value="female" class="sr-only">
|
| 80 |
+
<div class="gender-option flex items-center gap-2 px-4 py-2 rounded-lg border-2 border-gray-600 text-gray-400">
|
| 81 |
+
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 82 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
| 83 |
+
</svg> -->
|
| 84 |
+
<span>Female</span>
|
| 85 |
+
</div>
|
| 86 |
+
</label>
|
| 87 |
+
</div>
|
| 88 |
+
<!-- <p class="text-xs text-gray-400 mt-2">
|
| 89 |
+
<span class="text-pink-400">Female mode:</span> Uses chest-to-hip detection to avoid hair on shoulders
|
| 90 |
+
</p> -->
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<!-- Garment Image Upload with Cropper -->
|
| 94 |
+
<div>
|
| 95 |
+
<div class="flex justify-between items-center mb-2">
|
| 96 |
+
<h2 class="text-lg font-semibold">Upload garment image</h2>
|
| 97 |
+
{% if cached_person %}
|
| 98 |
+
<div class="flex items-center gap-1 text-xs text-green-400">
|
| 99 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 100 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
| 101 |
+
</svg>
|
| 102 |
+
<span>Fast mode</span>
|
| 103 |
+
</div>
|
| 104 |
+
{% endif %}
|
| 105 |
+
</div>
|
| 106 |
+
<label for="tshirt_image" class="flex flex-col items-center justify-center border-2 border-dashed border-gray-600 rounded-xl p-6 hover:bg-gray-700 cursor-pointer">
|
| 107 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mb-2" fill="none"
|
| 108 |
+
viewBox="0 0 24 24" stroke="currentColor">
|
| 109 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 110 |
+
d="M7 16v4m0 0h10m-10 0v-4m0 0h10m-10 0V5m0 0h10m-10 0H5m14 0h-2" />
|
| 111 |
+
</svg>
|
| 112 |
+
<p class="text-gray-400">Drag & drop or click to upload</p>
|
| 113 |
+
<input id="tshirt_image" type="file" name="tshirt_image" class="hidden" required>
|
| 114 |
+
</label>
|
| 115 |
+
<p id="tshirt_filename" class="text-green-400 text-sm mt-2 text-center"></p>
|
| 116 |
+
|
| 117 |
+
<!-- Cropping Container -->
|
| 118 |
+
<div class="mt-3 flex justify-center">
|
| 119 |
+
<img id="tshirt_preview" class="hidden max-h-64 rounded-lg border border-gray-600">
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<!-- Submit Button -->
|
| 125 |
+
<div class="flex justify-center">
|
| 126 |
+
<button type="submit" class="bg-pink-500 hover:bg-pink-600 text-white font-semibold py-3 px-8 rounded-xl shadow-md transition">
|
| 127 |
+
🚀 Perform Virtual Try-On
|
| 128 |
+
</button>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
</form>
|
| 132 |
+
|
| 133 |
+
<!-- RIGHT: Output -->
|
| 134 |
+
<div class="w-full md:w-1/2 bg-gray-800 rounded-2xl shadow-lg p-8 flex items-center justify-center text-center">
|
| 135 |
+
{% if result_img %}
|
| 136 |
+
<div>
|
| 137 |
+
<h2 class="text-2xl font-bold mb-6 text-center">🎉 Your Virtual Try-On Result</h2>
|
| 138 |
+
{% if processing_time %}
|
| 139 |
+
<div class="text-center mb-4">
|
| 140 |
+
<span class="bg-blue-500 text-white px-3 py-1 rounded-full text-sm">
|
| 141 |
+
⚡ Processed in {{ processing_time }}
|
| 142 |
+
</span>
|
| 143 |
+
</div>
|
| 144 |
+
{% endif %}
|
| 145 |
+
<div class="flex justify-center mb-6">
|
| 146 |
+
<img id="result-image" src="{{ result_img }}" alt="Result Image" class="rounded-xl"
|
| 147 |
+
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
| 148 |
+
<div style="display:none;" class="text-red-500 p-4 border border-red-500 rounded-xl">
|
| 149 |
+
<p>❌ Image failed to load</p>
|
| 150 |
+
<p class="text-sm">Please try again or check the server logs</p>
|
| 151 |
+
<p class="text-xs">Image URL: {{ result_img }}</p>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="flex justify-center">
|
| 155 |
+
<button onclick="downloadImage()" class="bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-6 rounded-xl shadow-md transition flex items-center gap-2">
|
| 156 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
| 157 |
+
stroke="currentColor">
|
| 158 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 159 |
+
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 160 |
+
</svg>
|
| 161 |
+
Download Image
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
{% else %}
|
| 166 |
+
<div id="output-container">
|
| 167 |
+
<div id="loading-spinner" style="display: none;" class="flex flex-col items-center">
|
| 168 |
+
<svg class="animate-spin h-16 w-16 text-pink-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 169 |
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
|
| 170 |
+
</circle>
|
| 171 |
+
<path class="opacity-75" fill="currentColor"
|
| 172 |
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014
|
| 173 |
+
12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 174 |
+
</svg>
|
| 175 |
+
<p class="mt-4 text-lg">Processing... Please wait.</p>
|
| 176 |
+
</div>
|
| 177 |
+
<div id="placeholder-text">
|
| 178 |
+
<h2 class="text-2xl font-bold text-center text-gray-500">Your result will appear here</h2>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
{% endif %}
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<!-- Global Full-screen Loader Overlay -->
|
| 187 |
+
<div id="global-loader" style="display:none;" class="fixed inset-0 z-50 bg-black/70 flex items-center justify-center">
|
| 188 |
+
<div class="flex flex-col items-center">
|
| 189 |
+
<svg class="animate-spin h-16 w-16 text-pink-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 190 |
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
| 191 |
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 192 |
+
</svg>
|
| 193 |
+
<p class="mt-4 text-lg">Processing... Please wait.</p>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
|
| 197 |
+
<!-- Cropper.js -->
|
| 198 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
| 199 |
+
|
| 200 |
+
<script>
|
| 201 |
+
let cropper;
|
| 202 |
+
|
| 203 |
+
// Global loader on every submit
|
| 204 |
+
document.getElementById('tryon-form').addEventListener('submit', function(e) {
|
| 205 |
+
const overlay = document.getElementById('global-loader');
|
| 206 |
+
if (overlay) overlay.style.display = 'flex';
|
| 207 |
+
|
| 208 |
+
// If garment cropper is active → replace original file with cropped version
|
| 209 |
+
if (cropper) {
|
| 210 |
+
e.preventDefault();
|
| 211 |
+
|
| 212 |
+
cropper.getCroppedCanvas().toBlob((blob) => {
|
| 213 |
+
const file = new File([blob], "cropped_garment.png", { type: "image/png" });
|
| 214 |
+
|
| 215 |
+
// Replace original input
|
| 216 |
+
const dataTransfer = new DataTransfer();
|
| 217 |
+
dataTransfer.items.add(file);
|
| 218 |
+
document.getElementById('tshirt_image').files = dataTransfer.files;
|
| 219 |
+
|
| 220 |
+
// Now submit form
|
| 221 |
+
e.target.submit();
|
| 222 |
+
});
|
| 223 |
+
return;
|
| 224 |
+
}
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
// Change Person: full refresh via POST + server redirect
|
| 228 |
+
function changePerson() {
|
| 229 |
+
const form = document.createElement('form');
|
| 230 |
+
form.method = 'POST';
|
| 231 |
+
form.action = '/change_person';
|
| 232 |
+
document.body.appendChild(form);
|
| 233 |
+
form.submit();
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// Gender selection handling
|
| 237 |
+
document.querySelectorAll('input[name="gender"]').forEach(radio => {
|
| 238 |
+
radio.addEventListener('change', function() {
|
| 239 |
+
// Update visual styling
|
| 240 |
+
document.querySelectorAll('.gender-option').forEach(option => {
|
| 241 |
+
option.classList.remove('border-blue-500', 'bg-blue-500/20', 'text-blue-400', 'border-pink-500', 'bg-pink-500/20', 'text-pink-400');
|
| 242 |
+
option.classList.add('border-gray-600', 'text-gray-400');
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
const selectedOption = this.parentElement.querySelector('.gender-option');
|
| 246 |
+
if (this.value === 'male') {
|
| 247 |
+
selectedOption.classList.remove('border-gray-600', 'text-gray-400');
|
| 248 |
+
selectedOption.classList.add('border-blue-500', 'bg-blue-500/20', 'text-blue-400');
|
| 249 |
+
} else {
|
| 250 |
+
selectedOption.classList.remove('border-gray-600', 'text-gray-400');
|
| 251 |
+
selectedOption.classList.add('border-pink-500', 'bg-pink-500/20', 'text-pink-400');
|
| 252 |
+
}
|
| 253 |
+
});
|
| 254 |
+
});
|
| 255 |
+
|
| 256 |
+
// Show file name + preview (person only)
|
| 257 |
+
function showFileName(inputId, filenameId, previewId) {
|
| 258 |
+
const input = document.getElementById(inputId);
|
| 259 |
+
const filename = document.getElementById(filenameId);
|
| 260 |
+
const preview = document.getElementById(previewId);
|
| 261 |
+
|
| 262 |
+
if (input.files.length > 0) {
|
| 263 |
+
const file = input.files[0];
|
| 264 |
+
filename.textContent = "✔️ " + file.name + " uploaded";
|
| 265 |
+
|
| 266 |
+
const reader = new FileReader();
|
| 267 |
+
reader.onload = function(e) {
|
| 268 |
+
preview.src = e.target.result;
|
| 269 |
+
preview.classList.remove("hidden");
|
| 270 |
+
};
|
| 271 |
+
reader.readAsDataURL(file);
|
| 272 |
+
} else {
|
| 273 |
+
filename.textContent = "";
|
| 274 |
+
preview.classList.add("hidden");
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// Garment upload with Cropper
|
| 279 |
+
document.getElementById('tshirt_image').addEventListener('change', function() {
|
| 280 |
+
const input = this;
|
| 281 |
+
const filename = document.getElementById('tshirt_filename');
|
| 282 |
+
const preview = document.getElementById('tshirt_preview');
|
| 283 |
+
|
| 284 |
+
if (input.files.length > 0) {
|
| 285 |
+
const file = input.files[0];
|
| 286 |
+
filename.textContent = "✔️ " + file.name + " uploaded";
|
| 287 |
+
|
| 288 |
+
const reader = new FileReader();
|
| 289 |
+
reader.onload = function(e) {
|
| 290 |
+
preview.src = e.target.result;
|
| 291 |
+
preview.classList.remove("hidden");
|
| 292 |
+
|
| 293 |
+
// Destroy old cropper if exists
|
| 294 |
+
if (cropper) {
|
| 295 |
+
cropper.destroy();
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// Initialize Cropper.js with square ratio
|
| 299 |
+
cropper = new Cropper(preview, {
|
| 300 |
+
aspectRatio: 1,
|
| 301 |
+
viewMode: 1,
|
| 302 |
+
autoCropArea: 1,
|
| 303 |
+
});
|
| 304 |
+
};
|
| 305 |
+
reader.readAsDataURL(file);
|
| 306 |
+
} else {
|
| 307 |
+
filename.textContent = "";
|
| 308 |
+
preview.classList.add("hidden");
|
| 309 |
+
if (cropper) {
|
| 310 |
+
cropper.destroy();
|
| 311 |
+
cropper = null;
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
});
|
| 315 |
+
|
| 316 |
+
// Download result
|
| 317 |
+
function downloadImage() {
|
| 318 |
+
const img = document.getElementById('result-image');
|
| 319 |
+
const canvas = document.createElement('canvas');
|
| 320 |
+
const ctx = canvas.getContext('2d');
|
| 321 |
+
canvas.width = img.naturalWidth;
|
| 322 |
+
canvas.height = img.naturalHeight;
|
| 323 |
+
ctx.drawImage(img, 0, 0);
|
| 324 |
+
canvas.toBlob(function(blob) {
|
| 325 |
+
const url = URL.createObjectURL(blob);
|
| 326 |
+
const a = document.createElement('a');
|
| 327 |
+
a.href = url;
|
| 328 |
+
a.download = 'virtual-try-on-result.png';
|
| 329 |
+
document.body.appendChild(a);
|
| 330 |
+
a.click();
|
| 331 |
+
document.body.removeChild(a);
|
| 332 |
+
URL.revokeObjectURL(url);
|
| 333 |
+
}, 'image/png');
|
| 334 |
+
}
|
| 335 |
+
</script>
|
| 336 |
+
|
| 337 |
+
</body>
|
| 338 |
+
|
| 339 |
+
</html>
|