Spaces:
Runtime error
Runtime error
Commit
Β·
965e09e
0
Parent(s):
Developed Virtual try on
Browse files- .gitattributes +35 -0
- .gitignore +7 -0
- Dockerfile +31 -0
- README.md +11 -0
- app.py +117 -0
- requirements.txt +11 -0
- runtime.txt +1 -0
- static/outputs/.gitkeep +0 -0
- static/uploads/.gitkeep +0 -0
- templates/index.html +225 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
.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,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Set cache directories
|
| 6 |
+
ENV HF_HOME=/app/.cache
|
| 7 |
+
ENV MPLCONFIGDIR=/app/.cache
|
| 8 |
+
|
| 9 |
+
# Install dependencies
|
| 10 |
+
RUN apt-get update && apt-get install -y libgl1-mesa-glx libglib2.0-0 && apt-get clean
|
| 11 |
+
|
| 12 |
+
# Create cache directory
|
| 13 |
+
RUN mkdir -p /app/.cache && chmod -R 777 /app/.cache
|
| 14 |
+
|
| 15 |
+
# Install Python dependencies
|
| 16 |
+
COPY requirements.txt .
|
| 17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Pre-download model
|
| 20 |
+
RUN python -c "from transformers import SamModel, SamProcessor; \
|
| 21 |
+
SamModel.from_pretrained('Zigeng/SlimSAM-uniform-50', cache_dir='/app/.cache'); \
|
| 22 |
+
SamProcessor.from_pretrained('Zigeng/SlimSAM-uniform-50', cache_dir='/app/.cache')"
|
| 23 |
+
|
| 24 |
+
# Copy app code
|
| 25 |
+
COPY . .
|
| 26 |
+
|
| 27 |
+
# Set port
|
| 28 |
+
ENV PORT=7860
|
| 29 |
+
|
| 30 |
+
# Run with Gunicorn
|
| 31 |
+
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:7860", "--workers", "2"]
|
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,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, send_from_directory
|
| 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 |
+
|
| 9 |
+
app= Flask(__name__)
|
| 10 |
+
CORS(app)
|
| 11 |
+
|
| 12 |
+
# Enable Hugging Face detailed logs (shows model download progress)
|
| 13 |
+
hf_logging.set_verbosity_info()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
UPLOAD_FOLDER = '/tmp/uploads'
|
| 17 |
+
OUTPUT_FOLDER = '/tmp/outputs'
|
| 18 |
+
|
| 19 |
+
if not os.path.exists(UPLOAD_FOLDER):
|
| 20 |
+
print(f"[WARN] {UPLOAD_FOLDER} does not exist. Creating...")
|
| 21 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 22 |
+
|
| 23 |
+
if not os.path.exists(OUTPUT_FOLDER):
|
| 24 |
+
print(f"[WARN] {OUTPUT_FOLDER} does not exist. Creating...")
|
| 25 |
+
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Lazy-load model
|
| 29 |
+
model, processor = None, None
|
| 30 |
+
|
| 31 |
+
def load_model():
|
| 32 |
+
global model, processor
|
| 33 |
+
if model is None or processor is None:
|
| 34 |
+
print("[INFO] Loading SAM model and processor...")
|
| 35 |
+
model = SamModel.from_pretrained("Zigeng/SlimSAM-uniform-50", cache_dir="/app/.cache")
|
| 36 |
+
processor = SamProcessor.from_pretrained("Zigeng/SlimSAM-uniform-50", cache_dir="/app/.cache")
|
| 37 |
+
print("[INFO] Model and processor loaded successfully!")
|
| 38 |
+
|
| 39 |
+
@app.before_request
|
| 40 |
+
def log_request_info():
|
| 41 |
+
print(f"[INFO] Incoming request: {request.method} {request.path}")
|
| 42 |
+
|
| 43 |
+
@app.route('/health')
|
| 44 |
+
def health():
|
| 45 |
+
return "OK", 200
|
| 46 |
+
|
| 47 |
+
# Route to serve outputs dynamically
|
| 48 |
+
@app.route('/outputs/<filename>')
|
| 49 |
+
def serve_output(filename):
|
| 50 |
+
return send_from_directory(OUTPUT_FOLDER, filename)
|
| 51 |
+
|
| 52 |
+
@app.route('/', methods=['GET', 'POST'])
|
| 53 |
+
def index():
|
| 54 |
+
print(f"[INFO] Handling {request.method} on /")
|
| 55 |
+
if request.method == 'POST':
|
| 56 |
+
try:
|
| 57 |
+
load_model()
|
| 58 |
+
|
| 59 |
+
# Save uploaded images
|
| 60 |
+
person_file = request.files['person_image']
|
| 61 |
+
tshirt_file = request.files['tshirt_image']
|
| 62 |
+
person_path = os.path.join(UPLOAD_FOLDER, 'person.jpg')
|
| 63 |
+
tshirt_path = os.path.join(UPLOAD_FOLDER, 'tshirt.png')
|
| 64 |
+
person_file.save(person_path)
|
| 65 |
+
tshirt_file.save(tshirt_path)
|
| 66 |
+
print(f"[INFO] Saved files to {UPLOAD_FOLDER}")
|
| 67 |
+
|
| 68 |
+
# Pose detection
|
| 69 |
+
mp_pose = mp.solutions.pose
|
| 70 |
+
pose = mp_pose.Pose()
|
| 71 |
+
image = cv2.imread(person_path)
|
| 72 |
+
if image is None:
|
| 73 |
+
return "No image detected."
|
| 74 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 75 |
+
results = pose.process(image_rgb)
|
| 76 |
+
if not results.pose_landmarks:
|
| 77 |
+
return "No pose detected."
|
| 78 |
+
height, width, _ = image.shape
|
| 79 |
+
landmarks = results.pose_landmarks.landmark
|
| 80 |
+
left_shoulder = (int(landmarks[11].x * width), int(landmarks[11].y * height))
|
| 81 |
+
right_shoulder = (int(landmarks[12].x * width), int(landmarks[12].y * height))
|
| 82 |
+
print(f"[INFO] Shoulder coordinates: {left_shoulder}, {right_shoulder}")
|
| 83 |
+
|
| 84 |
+
# SAM model inference
|
| 85 |
+
img = load_image(person_path)
|
| 86 |
+
new_tshirt = load_image(tshirt_path)
|
| 87 |
+
input_points = [[[left_shoulder[0], left_shoulder[1]], [right_shoulder[0], right_shoulder[1]]]]
|
| 88 |
+
inputs = processor(img, input_points=input_points, return_tensors="pt")
|
| 89 |
+
outputs = model(**inputs)
|
| 90 |
+
masks = processor.image_processor.post_process_masks(
|
| 91 |
+
outputs.pred_masks.cpu(),
|
| 92 |
+
inputs["original_sizes"].cpu(),
|
| 93 |
+
inputs["reshaped_input_sizes"].cpu()
|
| 94 |
+
)
|
| 95 |
+
mask_tensor = masks[0][0][2].to(dtype=torch.uint8)
|
| 96 |
+
mask = transforms.ToPILImage()(mask_tensor * 255)
|
| 97 |
+
|
| 98 |
+
# Combine images
|
| 99 |
+
new_tshirt = new_tshirt.resize(img.size, Image.LANCZOS)
|
| 100 |
+
img_with_new_tshirt = Image.composite(new_tshirt, img, mask)
|
| 101 |
+
result_path = os.path.join(OUTPUT_FOLDER, 'result.jpg')
|
| 102 |
+
img_with_new_tshirt.save(result_path)
|
| 103 |
+
print(f"[INFO] Result saved to {result_path}")
|
| 104 |
+
|
| 105 |
+
# Serve via dynamic route
|
| 106 |
+
return render_template('index.html', result_img='/outputs/result.jpg')
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"[ERROR] {e}")
|
| 110 |
+
return f"Error: {e}"
|
| 111 |
+
|
| 112 |
+
return render_template('index.html')
|
| 113 |
+
|
| 114 |
+
if __name__ == '__main__':
|
| 115 |
+
|
| 116 |
+
print("[INFO] Starting Flask server...")
|
| 117 |
+
app.run(debug=True, host='0.0.0.0')
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask
|
| 2 |
+
gunicorn
|
| 3 |
+
Pillow
|
| 4 |
+
opencv-python
|
| 5 |
+
torch
|
| 6 |
+
torchvision
|
| 7 |
+
mediapipe
|
| 8 |
+
transformers
|
| 9 |
+
diffusers
|
| 10 |
+
safetensors
|
| 11 |
+
flask-cors
|
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,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
<h2 class="text-lg font-semibold mb-2">Upload your photo</h2>
|
| 25 |
+
<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">
|
| 26 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mb-2" fill="none"
|
| 27 |
+
viewBox="0 0 24 24" stroke="currentColor">
|
| 28 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 29 |
+
d="M7 16v4m0 0h10m-10 0v-4m0 0h10m-10 0V5m0 0h10m-10 0H5m14 0h-2" />
|
| 30 |
+
</svg>
|
| 31 |
+
<p class="text-gray-400">Drag & drop or click to upload</p>
|
| 32 |
+
<input id="person_image" type="file" name="person_image" class="hidden" required
|
| 33 |
+
onchange="showFileName('person_image', 'person_filename', 'person_preview')">
|
| 34 |
+
</label>
|
| 35 |
+
<p id="person_filename" class="text-green-400 text-sm mt-2 text-center"></p>
|
| 36 |
+
<div class="mt-3 flex justify-center">
|
| 37 |
+
<img id="person_preview" class="hidden max-h-32 rounded-lg border border-gray-600">
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<!-- Garment Image Upload with Cropper -->
|
| 42 |
+
<div>
|
| 43 |
+
<h2 class="text-lg font-semibold mb-2">Upload garment image</h2>
|
| 44 |
+
<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">
|
| 45 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mb-2" fill="none"
|
| 46 |
+
viewBox="0 0 24 24" stroke="currentColor">
|
| 47 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 48 |
+
d="M7 16v4m0 0h10m-10 0v-4m0 0h10m-10 0V5m0 0h10m-10 0H5m14 0h-2" />
|
| 49 |
+
</svg>
|
| 50 |
+
<p class="text-gray-400">Drag & drop or click to upload</p>
|
| 51 |
+
<input id="tshirt_image" type="file" name="tshirt_image" class="hidden" required>
|
| 52 |
+
</label>
|
| 53 |
+
<p id="tshirt_filename" class="text-green-400 text-sm mt-2 text-center"></p>
|
| 54 |
+
|
| 55 |
+
<!-- Cropping Container -->
|
| 56 |
+
<div class="mt-3 flex justify-center">
|
| 57 |
+
<img id="tshirt_preview" class="hidden max-h-64 rounded-lg border border-gray-600">
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<!-- Submit Button -->
|
| 63 |
+
<div class="flex justify-center">
|
| 64 |
+
<button type="submit" class="bg-pink-500 hover:bg-pink-600 text-white font-semibold py-3 px-8 rounded-xl shadow-md transition">
|
| 65 |
+
π Perform Virtual Try-On
|
| 66 |
+
</button>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
</form>
|
| 70 |
+
|
| 71 |
+
<!-- RIGHT: Output -->
|
| 72 |
+
<div class="w-full md:w-1/2 bg-gray-800 rounded-2xl shadow-lg p-8 flex items-center justify-center text-center">
|
| 73 |
+
{% if result_img %}
|
| 74 |
+
<div>
|
| 75 |
+
<h2 class="text-2xl font-bold mb-6 text-center">π Your Virtual Try-On Result</h2>
|
| 76 |
+
<div class="flex justify-center mb-6">
|
| 77 |
+
<img id="result-image" src="{{ result_img }}" alt="Result Image" class="rounded-xl">
|
| 78 |
+
</div>
|
| 79 |
+
<div class="flex justify-center">
|
| 80 |
+
<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">
|
| 81 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
| 82 |
+
stroke="currentColor">
|
| 83 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 84 |
+
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" />
|
| 85 |
+
</svg>
|
| 86 |
+
Download Image
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
{% else %}
|
| 91 |
+
<div id="output-container">
|
| 92 |
+
<div id="loading-spinner" style="display: none;" class="flex flex-col items-center">
|
| 93 |
+
<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">
|
| 94 |
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
|
| 95 |
+
</circle>
|
| 96 |
+
<path class="opacity-75" fill="currentColor"
|
| 97 |
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014
|
| 98 |
+
12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 99 |
+
</svg>
|
| 100 |
+
<p class="mt-4 text-lg">Processing... Please wait.</p>
|
| 101 |
+
</div>
|
| 102 |
+
<div id="placeholder-text">
|
| 103 |
+
<h2 class="text-2xl font-bold text-center text-gray-500">Your result will appear here</h2>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
{% endif %}
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<!-- Cropper.js -->
|
| 112 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
| 113 |
+
|
| 114 |
+
<script>
|
| 115 |
+
let cropper;
|
| 116 |
+
|
| 117 |
+
// Spinner
|
| 118 |
+
document.getElementById('tryon-form').addEventListener('submit', function(e) {
|
| 119 |
+
// If garment cropper is active β replace original file with cropped version
|
| 120 |
+
if (cropper) {
|
| 121 |
+
e.preventDefault();
|
| 122 |
+
|
| 123 |
+
cropper.getCroppedCanvas().toBlob((blob) => {
|
| 124 |
+
const file = new File([blob], "cropped_garment.png", {
|
| 125 |
+
type: "image/png"
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
// Replace original input
|
| 129 |
+
const dataTransfer = new DataTransfer();
|
| 130 |
+
dataTransfer.items.add(file);
|
| 131 |
+
document.getElementById('tshirt_image').files = dataTransfer.files;
|
| 132 |
+
|
| 133 |
+
// Now submit form
|
| 134 |
+
e.target.submit();
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
document.getElementById('placeholder-text').style.display = 'none';
|
| 139 |
+
document.getElementById('loading-spinner').style.display = 'flex';
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
// Show file name + preview (person only)
|
| 143 |
+
function showFileName(inputId, filenameId, previewId) {
|
| 144 |
+
const input = document.getElementById(inputId);
|
| 145 |
+
const filename = document.getElementById(filenameId);
|
| 146 |
+
const preview = document.getElementById(previewId);
|
| 147 |
+
|
| 148 |
+
if (input.files.length > 0) {
|
| 149 |
+
const file = input.files[0];
|
| 150 |
+
filename.textContent = "βοΈ " + file.name + " uploaded";
|
| 151 |
+
|
| 152 |
+
const reader = new FileReader();
|
| 153 |
+
reader.onload = function(e) {
|
| 154 |
+
preview.src = e.target.result;
|
| 155 |
+
preview.classList.remove("hidden");
|
| 156 |
+
};
|
| 157 |
+
reader.readAsDataURL(file);
|
| 158 |
+
} else {
|
| 159 |
+
filename.textContent = "";
|
| 160 |
+
preview.classList.add("hidden");
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Garment upload with Cropper
|
| 165 |
+
document.getElementById('tshirt_image').addEventListener('change', function() {
|
| 166 |
+
const input = this;
|
| 167 |
+
const filename = document.getElementById('tshirt_filename');
|
| 168 |
+
const preview = document.getElementById('tshirt_preview');
|
| 169 |
+
|
| 170 |
+
if (input.files.length > 0) {
|
| 171 |
+
const file = input.files[0];
|
| 172 |
+
filename.textContent = "βοΈ " + file.name + " uploaded";
|
| 173 |
+
|
| 174 |
+
const reader = new FileReader();
|
| 175 |
+
reader.onload = function(e) {
|
| 176 |
+
preview.src = e.target.result;
|
| 177 |
+
preview.classList.remove("hidden");
|
| 178 |
+
|
| 179 |
+
// Destroy old cropper if exists
|
| 180 |
+
if (cropper) {
|
| 181 |
+
cropper.destroy();
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Initialize Cropper.js with square ratio
|
| 185 |
+
cropper = new Cropper(preview, {
|
| 186 |
+
aspectRatio: 1,
|
| 187 |
+
viewMode: 1,
|
| 188 |
+
autoCropArea: 1,
|
| 189 |
+
});
|
| 190 |
+
};
|
| 191 |
+
reader.readAsDataURL(file);
|
| 192 |
+
} else {
|
| 193 |
+
filename.textContent = "";
|
| 194 |
+
preview.classList.add("hidden");
|
| 195 |
+
if (cropper) {
|
| 196 |
+
cropper.destroy();
|
| 197 |
+
cropper = null;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
});
|
| 201 |
+
|
| 202 |
+
// Download result
|
| 203 |
+
function downloadImage() {
|
| 204 |
+
const img = document.getElementById('result-image');
|
| 205 |
+
const canvas = document.createElement('canvas');
|
| 206 |
+
const ctx = canvas.getContext('2d');
|
| 207 |
+
canvas.width = img.naturalWidth;
|
| 208 |
+
canvas.height = img.naturalHeight;
|
| 209 |
+
ctx.drawImage(img, 0, 0);
|
| 210 |
+
canvas.toBlob(function(blob) {
|
| 211 |
+
const url = URL.createObjectURL(blob);
|
| 212 |
+
const a = document.createElement('a');
|
| 213 |
+
a.href = url;
|
| 214 |
+
a.download = 'virtual-try-on-result.png';
|
| 215 |
+
document.body.appendChild(a);
|
| 216 |
+
a.click();
|
| 217 |
+
document.body.removeChild(a);
|
| 218 |
+
URL.revokeObjectURL(url);
|
| 219 |
+
}, 'image/png');
|
| 220 |
+
}
|
| 221 |
+
</script>
|
| 222 |
+
|
| 223 |
+
</body>
|
| 224 |
+
|
| 225 |
+
</html>
|