ritz26 commited on
Commit
019b718
·
0 Parent(s):

improve the model and fix girls try on issue

Browse files
Files changed (10) hide show
  1. .gitattributes +36 -0
  2. .gitignore +7 -0
  3. Dockerfile +36 -0
  4. README.md +11 -0
  5. app.py +321 -0
  6. requirements.txt +12 -0
  7. runtime.txt +1 -0
  8. static/outputs/.gitkeep +0 -0
  9. static/uploads/.gitkeep +0 -0
  10. 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>