tayy786 commited on
Commit
2429c55
Β·
verified Β·
1 Parent(s): 624c434

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -117
app.py CHANGED
@@ -4,10 +4,10 @@ import torch.nn as nn
4
  import torchvision.transforms as transforms
5
  import numpy as np
6
  from PIL import Image
7
- import mediapipe as mp
8
  import os
9
 
10
- # ── Model Definition (must match training exactly) ────────────────────────────
11
  class ConvBlock(nn.Module):
12
  def __init__(self, in_ch, out_ch):
13
  super().__init__()
@@ -20,7 +20,8 @@ class ConvBlock(nn.Module):
20
  def forward(self, x): return self.block(x)
21
 
22
  class TryOnUNet(nn.Module):
23
- def __init__(self, in_ch=25, base_ch=32):
 
24
  super().__init__()
25
  self.enc1 = ConvBlock(in_ch, base_ch)
26
  self.enc2 = ConvBlock(base_ch, base_ch*2)
@@ -50,41 +51,77 @@ H, W = 256, 192
50
  DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
51
 
52
  # ── Load Model ────────────────────────────────────────────────────────────────
53
- def load_model(weights_path='tryon_model.pth'):
54
- model = TryOnUNet(in_ch=25, base_ch=32).to(DEVICE)
55
- if os.path.exists(weights_path):
56
- ckpt = torch.load(weights_path, map_location=DEVICE)
57
- # Handle both raw state_dict and checkpoint dict
58
- sd = ckpt.get('model_state_dict', ckpt)
59
- model.load_state_dict(sd)
60
- print(f"βœ… Loaded weights from {weights_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  else:
62
- print("⚠️ No weights file found β€” using random weights (demo only)")
63
- model.eval()
64
- return model
65
-
66
- model = load_model()
67
-
68
- # ── Pose Estimator ────────────────────────────────────────────────────────────
69
- mp_pose = mp.solutions.pose
70
- pose_estimator = mp_pose.Pose(
71
- static_image_mode=True, model_complexity=1,
72
- min_detection_confidence=0.5
73
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- def extract_pose_heatmap(img_pil):
76
- img_np = np.array(img_pil.convert('RGB'))
77
- results = pose_estimator.process(img_np)
78
- heatmap = np.zeros((18, H, W), dtype=np.float32)
79
- if results.pose_landmarks:
80
- lms = results.pose_landmarks.landmark
81
- for i in range(min(18, len(lms))):
82
- x = int(lms[i].x * W)
83
- y = int(lms[i].y * H)
84
- c = lms[i].visibility
85
- if c > 0.3 and 0 <= x < W and 0 <= y < H:
86
- yy, xx = np.ogrid[:H, :W]
87
- heatmap[i] = np.exp(-((xx-x)**2+(yy-y)**2)/(2*8**2)) * c
88
  return torch.from_numpy(heatmap)
89
 
90
  # ── Transforms ────────────────────────────────────────────────────────────────
@@ -99,32 +136,25 @@ def denorm(tensor):
99
  return transforms.ToPILImage()(t.clamp(0, 1))
100
 
101
  def make_agnostic(person_t):
 
102
  ag = person_t.clone()
103
  ag[:, H//4:3*H//4, W//6:5*W//6] = -0.5
104
  return ag
105
 
106
- # ── Main Inference Function ───────────────────────────────────────────────────
107
  def virtual_tryon(person_img, cloth_img):
108
- """
109
- Args:
110
- person_img : PIL Image β€” full body photo of person
111
- cloth_img : PIL Image β€” flat-lay garment photo
112
- Returns:
113
- result_img : PIL Image β€” person wearing the garment
114
- """
115
  if person_img is None or cloth_img is None:
116
- return None, "⚠️ Please upload both a person photo and a garment photo."
117
-
118
  try:
119
- # Preprocess
120
- person_t = img_transform(person_img.convert('RGB'))
121
- cloth_t = img_transform(cloth_img.convert('RGB'))
122
- agnostic = make_agnostic(person_t)
123
- pose_map = extract_pose_heatmap(person_img)
124
  cloth_mask = torch.zeros(1, H, W)
125
  cloth_mask[:, H//4:3*H//4, W//6:5*W//6] = 1.0
126
 
127
- # Stack inputs β†’ [1, 25, H, W]
128
  inp = torch.cat([
129
  agnostic.unsqueeze(0),
130
  cloth_t.unsqueeze(0),
@@ -132,86 +162,41 @@ def virtual_tryon(person_img, cloth_img):
132
  pose_map.unsqueeze(0)
133
  ], dim=1).to(DEVICE)
134
 
135
- # Inference
136
  with torch.no_grad():
137
  out = model(inp).squeeze(0)
138
 
139
- result = denorm(out)
140
- return result, "βœ… Try-on complete!"
141
 
142
  except Exception as e:
143
- return None, f"❌ Error: {str(e)}"
144
 
145
  # ── Gradio UI ─────────────────────────────────────────────────────────────────
146
- with gr.Blocks(
147
- title="Virtual Try-On",
148
- theme=gr.themes.Soft(),
149
- css="""
150
- .header { text-align: center; padding: 20px 0 10px; }
151
- .header h1 { font-size: 2rem; font-weight: 700; color: #1a1a2e; }
152
- .header p { color: #555; font-size: 1rem; }
153
- .result-box { border: 2px dashed #ccc; border-radius: 12px; }
154
- footer { display: none !important; }
155
- """
156
- ) as demo:
157
-
158
- gr.HTML("""
159
- <div class='header'>
160
- <h1>πŸ§₯ Virtual Try-On System</h1>
161
- <p>Upload a person photo and a garment β€” see it on them instantly</p>
162
- <p style='font-size:0.85rem; color:#888;'>
163
- FYP Project | BZU CASPAM | Post ADP Mathematics
164
- </p>
165
- </div>
166
  """)
167
 
168
  with gr.Row():
169
- with gr.Column(scale=1):
170
- person_input = gr.Image(
171
- label="πŸ‘€ Person Photo",
172
- type="pil",
173
- height=320
174
- )
175
- gr.Markdown("> Full-body frontal photo works best")
176
-
177
- with gr.Column(scale=1):
178
- cloth_input = gr.Image(
179
- label="πŸ‘• Garment Photo",
180
- type="pil",
181
- height=320
182
- )
183
- gr.Markdown("> Flat-lay product photo on white background")
184
-
185
- with gr.Column(scale=1):
186
- result_output = gr.Image(
187
- label="✨ Try-On Result",
188
- type="pil",
189
- height=320,
190
- elem_classes=["result-box"]
191
- )
192
- status_text = gr.Markdown("")
193
 
194
  run_btn = gr.Button("πŸš€ Try It On!", variant="primary", size="lg")
195
 
 
 
 
 
 
 
 
196
  run_btn.click(
197
  fn=virtual_tryon,
198
  inputs=[person_input, cloth_input],
199
- outputs=[result_output, status_text]
200
  )
201
 
202
- gr.Markdown("""
203
- ---
204
- ### πŸ“Œ Tips for best results
205
- - Use a **frontal, full-body** photo with clear background
206
- - Garment should be a **flat-lay on white background**
207
- - Works best with **tops and shirts** (not pants/shoes yet)
208
-
209
- ### πŸ”§ How it works
210
- 1. **Pose Estimation** β€” MediaPipe detects 18 body keypoints
211
- 2. **Agnostic Masking** β€” Original clothing region is masked out
212
- 3. **UNet Synthesis** β€” Model warps garment onto body shape
213
- """)
214
-
215
- if __name__ == "__main__":
216
- demo.launch()
217
-
 
4
  import torchvision.transforms as transforms
5
  import numpy as np
6
  from PIL import Image
7
+ import cv2
8
  import os
9
 
10
+ # ── Model Definition ──────────────────────────────────────────────────────────
11
  class ConvBlock(nn.Module):
12
  def __init__(self, in_ch, out_ch):
13
  super().__init__()
 
20
  def forward(self, x): return self.block(x)
21
 
22
  class TryOnUNet(nn.Module):
23
+ def __init__(self, in_ch=22, base_ch=32):
24
+ # in_ch = agnostic(3) + cloth(3) + cloth_mask(1) + pose_heatmap(15)
25
  super().__init__()
26
  self.enc1 = ConvBlock(in_ch, base_ch)
27
  self.enc2 = ConvBlock(base_ch, base_ch*2)
 
51
  DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
52
 
53
  # ── Load Model ────────────────────────────────────────────────────────────────
54
+ model = TryOnUNet(in_ch=22, base_ch=32).to(DEVICE)
55
+ if os.path.exists('tryon_model.pth'):
56
+ ckpt = torch.load('tryon_model.pth', map_location=DEVICE)
57
+ sd = ckpt.get('model_state_dict', ckpt)
58
+ model.load_state_dict(sd, strict=False)
59
+ print("βœ… Weights loaded")
60
+ else:
61
+ print("⚠️ No weights β€” using random weights (demo)")
62
+ model.eval()
63
+
64
+ # ── Pose: OpenCV-based body keypoint approximation ────────────────────────────
65
+ # Uses HOG person detector + simple torso geometry
66
+ # No external pose library needed β€” works on any Python 3.13 environment
67
+
68
+ def estimate_pose_heatmap(img_pil, h=H, w=W):
69
+ """
70
+ Lightweight pose heatmap using OpenCV HOG detector.
71
+ Approximates 15 keypoint locations from detected person bounding box.
72
+ Good enough for clothing region alignment in try-on.
73
+ """
74
+ img_np = np.array(img_pil.convert('RGB'))
75
+ img_resized = cv2.resize(img_np, (w, h))
76
+ gray = cv2.cvtColor(img_resized, cv2.COLOR_RGB2GRAY)
77
+
78
+ heatmap = np.zeros((15, h, w), dtype=np.float32)
79
+
80
+ # Try HOG person detection
81
+ hog = cv2.HOGDescriptor()
82
+ hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
83
+ img_bgr = cv2.cvtColor(img_resized, cv2.COLOR_RGB2BGR)
84
+ boxes, weights = hog.detectMultiScale(img_bgr, winStride=(8,8), padding=(4,4), scale=1.05)
85
+
86
+ if len(boxes) > 0:
87
+ # Use largest detected person box
88
+ idx = np.argmax([b[2]*b[3] for b in boxes])
89
+ x, y, bw, bh = boxes[idx]
90
+ cx = x + bw // 2 # center x
91
  else:
92
+ # Fallback: assume person is centered
93
+ x, y, bw, bh = w//4, 0, w//2, h
94
+ cx = w // 2
95
+
96
+ # Approximate keypoint positions from bounding box geometry
97
+ # Order: nose, neck, L-shoulder, R-shoulder, L-elbow, R-elbow,
98
+ # L-wrist, R-wrist, L-hip, R-hip, L-knee, R-knee,
99
+ # L-ankle, R-ankle, center-chest
100
+ keypoints = [
101
+ (cx, y + int(bh*0.05)), # 0 nose
102
+ (cx, y + int(bh*0.12)), # 1 neck
103
+ (cx - bw//4, y + int(bh*0.18)), # 2 left shoulder
104
+ (cx + bw//4, y + int(bh*0.18)), # 3 right shoulder
105
+ (cx - bw//3, y + int(bh*0.35)), # 4 left elbow
106
+ (cx + bw//3, y + int(bh*0.35)), # 5 right elbow
107
+ (cx - bw//3, y + int(bh*0.52)), # 6 left wrist
108
+ (cx + bw//3, y + int(bh*0.52)), # 7 right wrist
109
+ (cx - bw//5, y + int(bh*0.55)), # 8 left hip
110
+ (cx + bw//5, y + int(bh*0.55)), # 9 right hip
111
+ (cx - bw//5, y + int(bh*0.72)), # 10 left knee
112
+ (cx + bw//5, y + int(bh*0.72)), # 11 right knee
113
+ (cx - bw//5, y + int(bh*0.90)), # 12 left ankle
114
+ (cx + bw//5, y + int(bh*0.90)), # 13 right ankle
115
+ (cx, y + int(bh*0.30)), # 14 chest center
116
+ ]
117
+
118
+ sigma = 10
119
+ yy, xx = np.ogrid[:h, :w]
120
+ for i, (kx, ky) in enumerate(keypoints):
121
+ kx = int(np.clip(kx, 0, w-1))
122
+ ky = int(np.clip(ky, 0, h-1))
123
+ heatmap[i] = np.exp(-((xx-kx)**2 + (yy-ky)**2) / (2*sigma**2))
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  return torch.from_numpy(heatmap)
126
 
127
  # ── Transforms ────────────────────────────────────────────────────────────────
 
136
  return transforms.ToPILImage()(t.clamp(0, 1))
137
 
138
  def make_agnostic(person_t):
139
+ """Mask out torso clothing region."""
140
  ag = person_t.clone()
141
  ag[:, H//4:3*H//4, W//6:5*W//6] = -0.5
142
  return ag
143
 
144
+ # ── Inference ─────────────────────────────────────────────────────────────────
145
  def virtual_tryon(person_img, cloth_img):
 
 
 
 
 
 
 
146
  if person_img is None or cloth_img is None:
147
+ gr.Warning("Please upload both a person photo and a garment photo.")
148
+ return None
149
  try:
150
+ person_t = img_transform(person_img.convert('RGB'))
151
+ cloth_t = img_transform(cloth_img.convert('RGB'))
152
+ agnostic = make_agnostic(person_t)
153
+ pose_map = estimate_pose_heatmap(person_img) # [15, H, W]
 
154
  cloth_mask = torch.zeros(1, H, W)
155
  cloth_mask[:, H//4:3*H//4, W//6:5*W//6] = 1.0
156
 
157
+ # Stack: 3 + 3 + 1 + 15 = 22 channels
158
  inp = torch.cat([
159
  agnostic.unsqueeze(0),
160
  cloth_t.unsqueeze(0),
 
162
  pose_map.unsqueeze(0)
163
  ], dim=1).to(DEVICE)
164
 
 
165
  with torch.no_grad():
166
  out = model(inp).squeeze(0)
167
 
168
+ return denorm(out)
 
169
 
170
  except Exception as e:
171
+ raise gr.Error(f"Inference failed: {str(e)}")
172
 
173
  # ── Gradio UI ─────────────────────────────────────────────────────────────────
174
+ with gr.Blocks(title="Virtual Try-On", theme=gr.themes.Soft()) as demo:
175
+
176
+ gr.Markdown("""
177
+ # πŸ§₯ Virtual Try-On System
178
+ Upload a **person photo** and a **garment image** to see the outfit on them.
179
+ > FYP Project Β· BZU CASPAM Β· Post ADP Mathematics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  """)
181
 
182
  with gr.Row():
183
+ person_input = gr.Image(label="πŸ‘€ Person Photo", type="pil", height=320)
184
+ cloth_input = gr.Image(label="πŸ‘• Garment Photo", type="pil", height=320)
185
+ result_img = gr.Image(label="✨ Try-On Result", type="pil", height=320)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  run_btn = gr.Button("πŸš€ Try It On!", variant="primary", size="lg")
188
 
189
+ gr.Markdown("""
190
+ **Tips for best results:**
191
+ - Use a **frontal full-body** photo with plain background
192
+ - Garment should be **flat-lay on white background**
193
+ - Works best with **tops and shirts**
194
+ """)
195
+
196
  run_btn.click(
197
  fn=virtual_tryon,
198
  inputs=[person_input, cloth_input],
199
+ outputs=[result_img]
200
  )
201
 
202
+ demo.launch()