hdremover commited on
Commit
d6dc276
Β·
verified Β·
1 Parent(s): 10965f1

Update engine.py

Browse files
Files changed (1) hide show
  1. engine.py +83 -14
engine.py CHANGED
@@ -15,6 +15,7 @@ from __future__ import annotations
15
 
16
  import gc
17
  import io
 
18
  import os
19
  import threading
20
  from dataclasses import dataclass
@@ -1181,6 +1182,7 @@ def apply_outfit(
1181
  # Collar should sit at the shoulder line, not above/below it β€”
1182
  # use the shoulder keypoints' own y as the collar target.
1183
  target_collar_y = shoulders.center_y
 
1184
  else:
1185
  # Fallback: derive an approximate shoulder position from the
1186
  # already-known face box, since we always have that.
@@ -1190,6 +1192,7 @@ def apply_outfit(
1190
  # same order-of-magnitude approximation used elsewhere in this
1191
  # file for anatomy without a direct measurement.
1192
  target_collar_y = face.y + face.h * 1.9
 
1193
 
1194
  scale = target_width_px / garment.shoulder_width_px
1195
  new_w = max(1, int(round(garment.image.width * scale)))
@@ -1203,6 +1206,16 @@ def apply_outfit(
1203
  paste_x = int(round(target_cx - scaled_collar_x))
1204
  paste_y = int(round(target_collar_y - scaled_collar_y))
1205
 
 
 
 
 
 
 
 
 
 
 
1206
  # Composite: start from a copy of matted, paste garment on top (using
1207
  # its own alpha as the mask so transparent garment-PNG pixels don't
1208
  # overwrite the subject), THEN paste the original head/shoulders
@@ -1213,20 +1226,51 @@ def apply_outfit(
1213
  del scaled_garment
1214
  gc.collect()
1215
 
1216
- # Re-apply the original face region on top. Padding matches the
1217
- # face-thumbnail crop elsewhere in this file (0.6x face size margin)
1218
- # so the reinstated patch comfortably covers the whole head with a
1219
- # soft-enough boundary that a hard rectangle edge is unlikely to fall
1220
- # across a hairline in a way that reads as an obvious seam.
1221
- pad = int(max(face.w, face.h) * 0.6)
 
 
 
 
1222
  side = max(face.w, face.h) + 2 * pad
1223
  fx0 = max(0, int(face.cx - side / 2))
1224
  fy0 = max(0, int(face.cy - side / 2))
1225
  fx1 = min(matted.width, fx0 + side)
1226
  fy1 = min(matted.height, fy0 + side)
1227
  face_patch = matted.crop((fx0, fy0, fx1, fy1))
1228
- result.paste(face_patch, (fx0, fy0), face_patch)
1229
- del face_patch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1230
  gc.collect()
1231
 
1232
  return result
@@ -1245,10 +1289,10 @@ def process_photo(
1245
  y_offset: float = 0.0,
1246
  auto_straighten: bool = True,
1247
  outfit_label: Optional[str] = None,
1248
- ) -> tuple[Image.Image, Optional[Image.Image], list["ComplianceCheck"], Image.Image, Image.Image, float]:
1249
  """Full pipeline. Returns (single_photo, print_sheet_or_None,
1250
  compliance_checks, bg_removed_preview, face_only_thumbnail,
1251
- straighten_angle_applied).
1252
 
1253
  bg_removed_preview: the alpha-matted subject on transparent background,
1254
  at the same size as `bounded` β€” this is a display artifact for the UI's
@@ -1271,6 +1315,17 @@ def process_photo(
1271
  None/"" to skip outfit overlay entirely (default β€” the original
1272
  photo's clothing is used, exactly as before this feature existed).
1273
 
 
 
 
 
 
 
 
 
 
 
 
1274
  Raises ValueError with a user-facing message on any recoverable
1275
  failure (no face found, bad spec key, etc) β€” ui.py surfaces these via
1276
  gr.Error rather than letting a raw traceback reach the user.
@@ -1317,18 +1372,32 @@ def process_photo(
1317
  # requested, skip entirely: zero cost, zero behavior change from
1318
  # before this feature existed.
1319
  outfitted = None
 
 
1320
  if outfit_label:
1321
  try:
1322
  outfitted = apply_outfit(bounded, matted, face, outfit_label)
 
1323
  except ValueError:
1324
  raise # unknown garment label β€” genuine user-facing error
1325
- except Exception:
1326
  # Pose detection or compositing failed for a reason that
1327
  # isn't the user's fault (e.g. onnxruntime hiccup) β€” degrade
1328
  # gracefully to the original photo rather than failing the
1329
  # whole generate. Outfit overlay is an enhancement, not a
1330
- # core guarantee the way face detection is.
 
 
 
 
 
 
 
 
 
1331
  outfitted = None
 
 
1332
 
1333
  del bounded
1334
  gc.collect()
@@ -1368,7 +1437,7 @@ def process_photo(
1368
  if paper_key:
1369
  sheet = build_print_sheet(final_photo, spec, paper_key)
1370
 
1371
- return final_photo, sheet, checks, bg_removed_preview, face_thumb, straighten_angle_applied
1372
 
1373
 
1374
  MAX_BATCH_SIZE = 10 # cap: each image is a separate @spaces.GPU acquisition
@@ -1411,7 +1480,7 @@ def process_batch(
1411
  results: list[BatchResult] = []
1412
  for filename, img in images:
1413
  try:
1414
- photo, _sheet, _checks, _bg_preview, _face_thumb, _angle = process_photo(
1415
  image=img,
1416
  spec_key=spec_key,
1417
  bg_hex=bg_hex,
 
15
 
16
  import gc
17
  import io
18
+ import logging
19
  import os
20
  import threading
21
  from dataclasses import dataclass
 
1182
  # Collar should sit at the shoulder line, not above/below it β€”
1183
  # use the shoulder keypoints' own y as the collar target.
1184
  target_collar_y = shoulders.center_y
1185
+ _outfit_debug_source = "pose"
1186
  else:
1187
  # Fallback: derive an approximate shoulder position from the
1188
  # already-known face box, since we always have that.
 
1192
  # same order-of-magnitude approximation used elsewhere in this
1193
  # file for anatomy without a direct measurement.
1194
  target_collar_y = face.y + face.h * 1.9
1195
+ _outfit_debug_source = "fallback"
1196
 
1197
  scale = target_width_px / garment.shoulder_width_px
1198
  new_w = max(1, int(round(garment.image.width * scale)))
 
1206
  paste_x = int(round(target_cx - scaled_collar_x))
1207
  paste_y = int(round(target_collar_y - scaled_collar_y))
1208
 
1209
+ logging.getLogger("passport-maker").info(
1210
+ "apply_outfit: source=%s shoulders_conf=%.2f target_width=%.0f "
1211
+ "target_cx=%.0f target_collar_y=%.0f scale=%.3f garment_size=%dx%d "
1212
+ "paste=(%d,%d) canvas=%dx%d",
1213
+ _outfit_debug_source,
1214
+ shoulders.confidence if shoulders else -1.0,
1215
+ target_width_px, target_cx, target_collar_y, scale,
1216
+ new_w, new_h, paste_x, paste_y, matted.width, matted.height,
1217
+ )
1218
+
1219
  # Composite: start from a copy of matted, paste garment on top (using
1220
  # its own alpha as the mask so transparent garment-PNG pixels don't
1221
  # overwrite the subject), THEN paste the original head/shoulders
 
1226
  del scaled_garment
1227
  gc.collect()
1228
 
1229
+ # Re-apply the original face region on top β€” but ONLY the face itself,
1230
+ # not a large margin around it. A prior version used a 2.2x-face-size
1231
+ # square patch (matching the display-only face-thumbnail crop
1232
+ # elsewhere in this file), which was large enough to reach down into
1233
+ # the collar/upper-chest area and paste the person's ORIGINAL shirt
1234
+ # collar right back over the garment that was just composited β€”
1235
+ # silently undoing the outfit swap every time. This tighter patch
1236
+ # (1.3x face size, elliptical mask) covers face + hair with margin to
1237
+ # spare, but stops well above where any garment's collar sits.
1238
+ pad = int(max(face.w, face.h) * 0.15)
1239
  side = max(face.w, face.h) + 2 * pad
1240
  fx0 = max(0, int(face.cx - side / 2))
1241
  fy0 = max(0, int(face.cy - side / 2))
1242
  fx1 = min(matted.width, fx0 + side)
1243
  fy1 = min(matted.height, fy0 + side)
1244
  face_patch = matted.crop((fx0, fy0, fx1, fy1))
1245
+
1246
+ # Elliptical alpha mask instead of the patch's own (rectangular) alpha
1247
+ # β€” softens the boundary so reinstating the face doesn't leave a
1248
+ # visible hard-edged square over the garment's shoulder area, and
1249
+ # keeps the effective coverage smaller than the crop box itself
1250
+ # (an ellipse inscribed in the square touches the collar line at far
1251
+ # fewer pixels than the square's bottom edge would). Combined with
1252
+ # the patch's own alpha (multiply, both 0-255) so background pixels
1253
+ # that were already transparent in the matte stay transparent rather
1254
+ # than the ellipse forcing them opaque.
1255
+ from PIL import ImageDraw as _ImageDraw
1256
+ import numpy as _np
1257
+
1258
+ ellipse_mask = Image.new("L", face_patch.size, 0)
1259
+ _ImageDraw.Draw(ellipse_mask).ellipse([0, 0, face_patch.size[0], face_patch.size[1]], fill=255)
1260
+
1261
+ if face_patch.mode == "RGBA":
1262
+ orig_alpha = face_patch.split()[3]
1263
+ combined_arr = (
1264
+ _np.array(ellipse_mask, dtype=_np.uint16)
1265
+ * _np.array(orig_alpha, dtype=_np.uint16)
1266
+ // 255
1267
+ ).astype(_np.uint8)
1268
+ combined_mask = Image.fromarray(combined_arr, mode="L")
1269
+ else:
1270
+ combined_mask = ellipse_mask
1271
+
1272
+ result.paste(face_patch, (fx0, fy0), combined_mask)
1273
+ del face_patch, ellipse_mask, combined_mask
1274
  gc.collect()
1275
 
1276
  return result
 
1289
  y_offset: float = 0.0,
1290
  auto_straighten: bool = True,
1291
  outfit_label: Optional[str] = None,
1292
+ ) -> tuple[Image.Image, Optional[Image.Image], list["ComplianceCheck"], Image.Image, Image.Image, float, bool, Optional[str]]:
1293
  """Full pipeline. Returns (single_photo, print_sheet_or_None,
1294
  compliance_checks, bg_removed_preview, face_only_thumbnail,
1295
+ straighten_angle_applied, outfit_applied, outfit_error).
1296
 
1297
  bg_removed_preview: the alpha-matted subject on transparent background,
1298
  at the same size as `bounded` β€” this is a display artifact for the UI's
 
1315
  None/"" to skip outfit overlay entirely (default β€” the original
1316
  photo's clothing is used, exactly as before this feature existed).
1317
 
1318
+ outfit_applied: True only if outfit compositing genuinely succeeded.
1319
+ False whenever outfit_label was set but compositing failed and the
1320
+ pipeline silently fell back to the original photo β€” the caller MUST
1321
+ check this rather than assuming outfit_label being set means the
1322
+ photo was actually outfitted, since that assumption previously
1323
+ produced a status message claiming an outfit was applied when it
1324
+ silently wasn't.
1325
+
1326
+ outfit_error: short error string when outfit_applied is False due to
1327
+ a failure (None if no outfit was requested, or if it succeeded).
1328
+
1329
  Raises ValueError with a user-facing message on any recoverable
1330
  failure (no face found, bad spec key, etc) β€” ui.py surfaces these via
1331
  gr.Error rather than letting a raw traceback reach the user.
 
1372
  # requested, skip entirely: zero cost, zero behavior change from
1373
  # before this feature existed.
1374
  outfitted = None
1375
+ outfit_applied = False
1376
+ outfit_error: Optional[str] = None
1377
  if outfit_label:
1378
  try:
1379
  outfitted = apply_outfit(bounded, matted, face, outfit_label)
1380
+ outfit_applied = True
1381
  except ValueError:
1382
  raise # unknown garment label β€” genuine user-facing error
1383
+ except Exception as e:
1384
  # Pose detection or compositing failed for a reason that
1385
  # isn't the user's fault (e.g. onnxruntime hiccup) β€” degrade
1386
  # gracefully to the original photo rather than failing the
1387
  # whole generate. Outfit overlay is an enhancement, not a
1388
+ # core guarantee the way face detection is. BUT: log it for
1389
+ # real, and tell the caller it silently degraded β€” an earlier
1390
+ # version of this code swallowed the exception AND still
1391
+ # reported "outfit: X" in the UI status line, which lied to
1392
+ # the user about what actually happened to their photo.
1393
+ import traceback
1394
+ logging.getLogger("passport-maker").warning(
1395
+ "Outfit overlay failed, falling back to original photo: %s",
1396
+ traceback.format_exc(),
1397
+ )
1398
  outfitted = None
1399
+ outfit_applied = False
1400
+ outfit_error = str(e) or type(e).__name__
1401
 
1402
  del bounded
1403
  gc.collect()
 
1437
  if paper_key:
1438
  sheet = build_print_sheet(final_photo, spec, paper_key)
1439
 
1440
+ return final_photo, sheet, checks, bg_removed_preview, face_thumb, straighten_angle_applied, outfit_applied, outfit_error
1441
 
1442
 
1443
  MAX_BATCH_SIZE = 10 # cap: each image is a separate @spaces.GPU acquisition
 
1480
  results: list[BatchResult] = []
1481
  for filename, img in images:
1482
  try:
1483
+ photo, _sheet, _checks, _bg_preview, _face_thumb, _angle, _outfit_ok, _outfit_err = process_photo(
1484
  image=img,
1485
  spec_key=spec_key,
1486
  bg_hex=bg_hex,