Spaces:
Sleeping
Sleeping
Update engine.py
Browse files
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
|
| 1217 |
-
#
|
| 1218 |
-
#
|
| 1219 |
-
#
|
| 1220 |
-
#
|
| 1221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|