ritz26 commited on
Commit
965e09e
Β·
0 Parent(s):

Developed Virtual try on

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