Muhammad Usman commited on
Commit
a87ef6c
·
1 Parent(s): 90394bc

enhance: preload depth model and compress output pixels payload

Browse files
Files changed (1) hide show
  1. app.py +110 -41
app.py CHANGED
@@ -4,6 +4,7 @@ import io
4
  import json
5
  import os
6
  import shutil
 
7
  try:
8
  import tomllib
9
  except ImportError:
@@ -155,6 +156,7 @@ def _load_segmentation_model():
155
  if SEGMENTATION_MODEL == "oneformer":
156
  try:
157
  print(f"Loading OneFormer: {ONEFORMER_MODEL_NAME} ...", flush=True)
 
158
  seg_processor = OneFormerProcessor.from_pretrained(
159
  ONEFORMER_MODEL_NAME,
160
  local_files_only=hf_offline(),
@@ -165,7 +167,7 @@ def _load_segmentation_model():
165
  ).to(device)
166
  seg_model.eval()
167
  segmentation_backend = "oneformer"
168
- print("OneFormer loaded.", flush=True)
169
  return
170
  except Exception as exc:
171
  print(f"OneFormer failed ({exc}), falling back to Mask2Former.", flush=True)
@@ -173,6 +175,7 @@ def _load_segmentation_model():
173
  if SEGMENTATION_MODEL in {"oneformer", "mask2former"}:
174
  try:
175
  print(f"Loading Mask2Former: {MASK2FORMER_MODEL_NAME} ...", flush=True)
 
176
  seg_processor = AutoImageProcessor.from_pretrained(
177
  MASK2FORMER_MODEL_NAME,
178
  local_files_only=hf_offline(),
@@ -183,12 +186,13 @@ def _load_segmentation_model():
183
  ).to(device)
184
  seg_model.eval()
185
  segmentation_backend = "mask2former"
186
- print("Mask2Former loaded.", flush=True)
187
  return
188
  except Exception as exc:
189
  print(f"Mask2Former failed ({exc}), falling back to SegFormer.", flush=True)
190
 
191
  print(f"Loading SegFormer: {SEGFORMER_MODEL_NAME} ...", flush=True)
 
192
  seg_processor = AutoImageProcessor.from_pretrained(
193
  SEGFORMER_MODEL_NAME,
194
  local_files_only=hf_offline(),
@@ -199,7 +203,7 @@ def _load_segmentation_model():
199
  ).to(device)
200
  seg_model.eval()
201
  segmentation_backend = "segformer"
202
- print("SegFormer loaded.", flush=True)
203
 
204
 
205
  _load_segmentation_model()
@@ -210,15 +214,40 @@ def _load_intrinsic_model():
210
  if ENABLE_INTRINSIC_SHADING and intrinsic_models is None:
211
  try:
212
  print(f"Loading Intrinsic Image Decomposition model: {INTRINSIC_MODEL_VERSION} ...", flush=True)
 
213
  from intrinsic.pipeline import load_models
214
  intrinsic_models = load_models(INTRINSIC_MODEL_VERSION, device=str(device))
215
- print("Intrinsic model loaded.", flush=True)
216
  except Exception as exc:
217
  print(f"Intrinsic model failed to load ({exc}). Falling back to luminance shading.", flush=True)
218
 
219
 
220
  _load_intrinsic_model()
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  app = FastAPI()
223
  app.add_middleware(
224
  CORSMiddleware,
@@ -262,15 +291,18 @@ def class_ids(names: set[str]) -> list[int]:
262
  return [idx for idx, name in enumerate(ADE20K_CLASSES) if name in names]
263
 
264
 
265
- def estimate_depth(img: Image.Image, width: int, height: int):
266
  global depth_processor, depth_model
267
  if not ENABLE_DEPTH_ESTIMATION:
268
  return None
269
 
270
  model_name = DEPTH_MODEL_NAME
 
 
271
  try:
272
  if depth_processor is None or depth_model is None:
273
- print(f"Loading depth model: {model_name} ...", flush=True)
 
274
  depth_processor = AutoImageProcessor.from_pretrained(
275
  model_name,
276
  local_files_only=hf_offline(),
@@ -280,7 +312,7 @@ def estimate_depth(img: Image.Image, width: int, height: int):
280
  local_files_only=hf_offline(),
281
  ).to(device)
282
  depth_model.eval()
283
- print("Depth model loaded.", flush=True)
284
 
285
  inputs = depth_processor(images=img, return_tensors="pt").to(device)
286
  with torch.no_grad():
@@ -293,18 +325,22 @@ def estimate_depth(img: Image.Image, width: int, height: int):
293
  ).squeeze().cpu().numpy()
294
  depth = cv2.GaussianBlur(depth.astype(np.float32), (0, 0), sigmaX=3)
295
  depth_min, depth_max = float(np.min(depth)), float(np.max(depth))
 
 
296
  if depth_max - depth_min < 1e-6:
297
  return None
298
  return (depth - depth_min) / (depth_max - depth_min)
299
  except Exception as exc:
300
- print(f"Depth estimation skipped ({exc}).", flush=True)
301
  return None
302
 
303
 
304
- def build_shade_map(img_np: np.ndarray, surface_mask: np.ndarray) -> np.ndarray | None:
305
  if not surface_mask.any():
306
  return None
307
 
 
 
308
  mask = surface_mask.astype(np.uint8)
309
  luminance = (
310
  img_np[:, :, 0].astype(np.float32) * 0.299
@@ -337,13 +373,18 @@ def build_shade_map(img_np: np.ndarray, surface_mask: np.ndarray) -> np.ndarray
337
  smooth = cv2.GaussianBlur(filled, (0, 0), sigmaX=sigma, sigmaY=sigma)
338
  shade = np.clip(smooth / median_lum, 0.55, 1.35)
339
  shade[mask == 0] = 1.0
340
- return np.round((shade - 0.55) * (255.0 / 0.80)).clip(0, 255).astype(np.uint8)
 
 
 
341
 
342
 
343
- def build_intrinsic_shade_map(img_np: np.ndarray, surface_mask: np.ndarray) -> np.ndarray | None:
344
  if not surface_mask.any() or intrinsic_models is None:
345
  return None
346
 
 
 
347
  try:
348
  # Convert image to float32 range [0.0, 1.0] as expected by compphoto/Intrinsic
349
  img_float = img_np.astype(np.float32) / 255.0
@@ -352,7 +393,7 @@ def build_intrinsic_shade_map(img_np: np.ndarray, surface_mask: np.ndarray) -> n
352
  from intrinsic.pipeline import run_pipeline
353
 
354
  # Use CPU/CUDA device string
355
- results = run_pipeline(intrinsic_models, img_float, device=str(device))
356
 
357
  # Extract shading map
358
  shading = None
@@ -402,9 +443,12 @@ def build_intrinsic_shade_map(img_np: np.ndarray, surface_mask: np.ndarray) -> n
402
  relative_shading[surface_mask == 0] = 1.0
403
 
404
  # Encode to [0, 255] byte range matching the frontend
405
- return np.round((relative_shading - 0.55) * (255.0 / 0.80)).clip(0, 255).astype(np.uint8)
 
 
 
406
  except Exception as exc:
407
- print(f"Intrinsic shading decomposition failed: {exc}. Falling back to default luminance shading.", flush=True)
408
  return None
409
 
410
 
@@ -532,7 +576,9 @@ def detect_vanishing_point(img_np: np.ndarray, floor_mask: np.ndarray):
532
  return {"x": float(center[0]), "y": float(center[1])}
533
 
534
 
535
- def estimate_floor_plane(mask: np.ndarray, img_np: np.ndarray):
 
 
536
  ys, xs = np.where(mask > 0)
537
  if len(xs) < 1000:
538
  return None, None
@@ -593,6 +639,8 @@ def estimate_floor_plane(mask: np.ndarray, img_np: np.ndarray):
593
  return None, None
594
  dst = np.float32([[x1, y2], [x2, y2], [x2, y1], [x1, y1]])
595
  homography = cv2.getPerspectiveTransform(src, dst).flatten().tolist()
 
 
596
  return homography, {
597
  "x": x1,
598
  "y": y1,
@@ -608,7 +656,10 @@ def build_floor_surface_mask(
608
  seg_map: np.ndarray,
609
  quad: np.ndarray | None,
610
  depth: np.ndarray | None,
 
611
  ):
 
 
612
  h, w = floor_mask.shape[:2]
613
  kern_size = max(5, min(h, w) // 160) | 1
614
  kern = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kern_size, kern_size))
@@ -655,11 +706,15 @@ def build_floor_surface_mask(
655
  surface = cv2.dilate(surface, boundary_kern, iterations=1)
656
  surface[occ_dilated > 0] = 0
657
  surface[reject_dilated > 0] = 0
 
 
658
  return surface
659
 
660
 
661
- def run_segmentation(img: Image.Image, img_np: np.ndarray):
662
  h, w = img_np.shape[:2]
 
 
663
  if segmentation_backend == "oneformer":
664
  inputs = seg_processor(
665
  images=img,
@@ -672,9 +727,9 @@ def run_segmentation(img: Image.Image, img_np: np.ndarray):
672
  outputs,
673
  target_sizes=[(h, w)],
674
  )[0]
675
- return result.cpu().numpy().astype(np.uint8)
676
 
677
- if segmentation_backend == "mask2former":
678
  inputs = seg_processor(images=img, return_tensors="pt").to(device)
679
  with torch.no_grad():
680
  outputs = seg_model(**inputs)
@@ -688,18 +743,23 @@ def run_segmentation(img: Image.Image, img_np: np.ndarray):
688
  pan_map = pan_result["segmentation"].cpu().numpy()
689
  for seg_info in pan_result["segments_info"]:
690
  seg_map[pan_map == seg_info["id"]] = min(seg_info["label_id"], 255)
691
- return seg_map
692
- result = seg_processor.post_process_semantic_segmentation(
693
- outputs,
694
- target_sizes=[(h, w)],
695
- )[0]
696
- return result.cpu().numpy().astype(np.uint8)
 
 
 
 
 
 
 
697
 
698
- inputs = seg_processor(images=img, return_tensors="pt").to(device)
699
- with torch.no_grad():
700
- outputs = seg_model(**inputs)
701
- seg = outputs.logits.argmax(dim=1).squeeze().cpu().numpy()
702
- return cv2.resize(seg.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST)
703
 
704
 
705
  def segmenter_metadata_name() -> str:
@@ -708,15 +768,16 @@ def segmenter_metadata_name() -> str:
708
  return segmentation_backend
709
 
710
 
711
- def build_segmentation_bundle(contents: bytes):
 
 
712
  img = Image.open(io.BytesIO(contents)).convert("RGB")
713
  img_np = np.array(img)
714
  h, w = img_np.shape[:2]
715
  min_floor_area = max(1200, int(w * h * 0.015))
716
 
717
- seg_map = run_segmentation(img, img_np)
718
- rgba = np.dstack([img_np, np.full((h, w), 255, dtype=np.uint8)])
719
- pixels_b64 = base64.b64encode(rgba.tobytes()).decode()
720
 
721
  primary_floor_ids = class_ids(PRIMARY_FLOOR_CLASSES)
722
  floor_class_ids = class_ids(FLOOR_SURFACE_CLASSES)
@@ -728,16 +789,16 @@ def build_segmentation_bundle(contents: bytes):
728
  floor_mask = wall_subtract(floor_mask, seg_map, dilation=1)
729
  floor_mask = clean_floor_mask(floor_mask)
730
 
731
- depth = estimate_depth(img, w, h)
732
- homography, plane = estimate_floor_plane(floor_mask, img_np)
733
  quad = np.asarray(plane["quad"], dtype=np.float32).reshape(4, 2) if plane and plane.get("quad") else None
734
- surface_mask = build_floor_surface_mask(floor_mask, seg_map, quad, depth)
735
  surface_indices = np.flatnonzero(surface_mask.ravel()).astype(np.uint32)
736
  shade_map = None
737
  if ENABLE_INTRINSIC_SHADING and intrinsic_models is not None:
738
- shade_map = build_intrinsic_shade_map(img_np, surface_mask)
739
  if shade_map is None:
740
- shade_map = build_shade_map(img_np, surface_mask)
741
  segments = []
742
 
743
  if len(surface_indices) >= min_floor_area:
@@ -777,6 +838,8 @@ def build_segmentation_bundle(contents: bytes):
777
  },
778
  })
779
 
 
 
780
  return {"width": w, "height": h, "pixels": pixels_b64, "segments": segments}
781
 
782
 
@@ -796,15 +859,20 @@ def write_job(job: dict):
796
 
797
 
798
  def run_conversion_task(job_id: str, upload_path: Path):
 
 
799
  try:
800
  image_bytes = upload_path.read_bytes()
801
- bundle = build_segmentation_bundle(image_bytes)
802
  (JOB_DIR / f"{job_id}.bundle.json").write_text(json.dumps(bundle))
803
  job = read_job(job_id)
804
  job["status"] = "COMPLETED"
805
  write_job(job)
 
 
806
  except Exception as exc:
807
- print(f"Background conversion failed: {exc}", flush=True)
 
808
  try:
809
  job = read_job(job_id)
810
  job["status"] = "FAILED"
@@ -866,7 +934,8 @@ async def viz2d_job_file(job_id: str):
866
  @app.post("/segment")
867
  async def segment(file: UploadFile = File(...)):
868
  contents = await file.read()
869
- return build_segmentation_bundle(contents)
 
870
 
871
 
872
  if __name__ == "__main__":
 
4
  import json
5
  import os
6
  import shutil
7
+ import time
8
  try:
9
  import tomllib
10
  except ImportError:
 
156
  if SEGMENTATION_MODEL == "oneformer":
157
  try:
158
  print(f"Loading OneFormer: {ONEFORMER_MODEL_NAME} ...", flush=True)
159
+ start_time = time.perf_counter()
160
  seg_processor = OneFormerProcessor.from_pretrained(
161
  ONEFORMER_MODEL_NAME,
162
  local_files_only=hf_offline(),
 
167
  ).to(device)
168
  seg_model.eval()
169
  segmentation_backend = "oneformer"
170
+ print(f"OneFormer loaded in {time.perf_counter() - start_time:.4f}s.", flush=True)
171
  return
172
  except Exception as exc:
173
  print(f"OneFormer failed ({exc}), falling back to Mask2Former.", flush=True)
 
175
  if SEGMENTATION_MODEL in {"oneformer", "mask2former"}:
176
  try:
177
  print(f"Loading Mask2Former: {MASK2FORMER_MODEL_NAME} ...", flush=True)
178
+ start_time = time.perf_counter()
179
  seg_processor = AutoImageProcessor.from_pretrained(
180
  MASK2FORMER_MODEL_NAME,
181
  local_files_only=hf_offline(),
 
186
  ).to(device)
187
  seg_model.eval()
188
  segmentation_backend = "mask2former"
189
+ print(f"Mask2Former loaded in {time.perf_counter() - start_time:.4f}s.", flush=True)
190
  return
191
  except Exception as exc:
192
  print(f"Mask2Former failed ({exc}), falling back to SegFormer.", flush=True)
193
 
194
  print(f"Loading SegFormer: {SEGFORMER_MODEL_NAME} ...", flush=True)
195
+ start_time = time.perf_counter()
196
  seg_processor = AutoImageProcessor.from_pretrained(
197
  SEGFORMER_MODEL_NAME,
198
  local_files_only=hf_offline(),
 
203
  ).to(device)
204
  seg_model.eval()
205
  segmentation_backend = "segformer"
206
+ print(f"SegFormer loaded in {time.perf_counter() - start_time:.4f}s.", flush=True)
207
 
208
 
209
  _load_segmentation_model()
 
214
  if ENABLE_INTRINSIC_SHADING and intrinsic_models is None:
215
  try:
216
  print(f"Loading Intrinsic Image Decomposition model: {INTRINSIC_MODEL_VERSION} ...", flush=True)
217
+ start_time = time.perf_counter()
218
  from intrinsic.pipeline import load_models
219
  intrinsic_models = load_models(INTRINSIC_MODEL_VERSION, device=str(device))
220
+ print(f"Intrinsic model loaded in {time.perf_counter() - start_time:.4f}s.", flush=True)
221
  except Exception as exc:
222
  print(f"Intrinsic model failed to load ({exc}). Falling back to luminance shading.", flush=True)
223
 
224
 
225
  _load_intrinsic_model()
226
 
227
+
228
+ def _load_depth_model():
229
+ global depth_processor, depth_model
230
+ if ENABLE_DEPTH_ESTIMATION and (depth_processor is None or depth_model is None):
231
+ try:
232
+ model_name = DEPTH_MODEL_NAME
233
+ print(f"Loading depth model: {model_name} ...", flush=True)
234
+ start_time = time.perf_counter()
235
+ depth_processor = AutoImageProcessor.from_pretrained(
236
+ model_name,
237
+ local_files_only=hf_offline(),
238
+ )
239
+ depth_model = AutoModelForDepthEstimation.from_pretrained(
240
+ model_name,
241
+ local_files_only=hf_offline(),
242
+ ).to(device)
243
+ depth_model.eval()
244
+ print(f"Depth model loaded in {time.perf_counter() - start_time:.4f}s.", flush=True)
245
+ except Exception as exc:
246
+ print(f"Depth model failed to load ({exc}).", flush=True)
247
+
248
+
249
+ _load_depth_model()
250
+
251
  app = FastAPI()
252
  app.add_middleware(
253
  CORSMiddleware,
 
291
  return [idx for idx, name in enumerate(ADE20K_CLASSES) if name in names]
292
 
293
 
294
+ def estimate_depth(img: Image.Image, width: int, height: int, task_id: str = "segment"):
295
  global depth_processor, depth_model
296
  if not ENABLE_DEPTH_ESTIMATION:
297
  return None
298
 
299
  model_name = DEPTH_MODEL_NAME
300
+ print(f"[{task_id}] Starting depth estimation...", flush=True)
301
+ start_time = time.perf_counter()
302
  try:
303
  if depth_processor is None or depth_model is None:
304
+ print(f"[{task_id}] Loading depth model: {model_name} ...", flush=True)
305
+ start_load = time.perf_counter()
306
  depth_processor = AutoImageProcessor.from_pretrained(
307
  model_name,
308
  local_files_only=hf_offline(),
 
312
  local_files_only=hf_offline(),
313
  ).to(device)
314
  depth_model.eval()
315
+ print(f"[{task_id}] Depth model loaded in {time.perf_counter() - start_load:.4f}s.", flush=True)
316
 
317
  inputs = depth_processor(images=img, return_tensors="pt").to(device)
318
  with torch.no_grad():
 
325
  ).squeeze().cpu().numpy()
326
  depth = cv2.GaussianBlur(depth.astype(np.float32), (0, 0), sigmaX=3)
327
  depth_min, depth_max = float(np.min(depth)), float(np.max(depth))
328
+ duration = time.perf_counter() - start_time
329
+ print(f"[{task_id}] Depth estimation completed in {duration:.4f}s", flush=True)
330
  if depth_max - depth_min < 1e-6:
331
  return None
332
  return (depth - depth_min) / (depth_max - depth_min)
333
  except Exception as exc:
334
+ print(f"[{task_id}] Depth estimation skipped ({exc}).", flush=True)
335
  return None
336
 
337
 
338
+ def build_shade_map(img_np: np.ndarray, surface_mask: np.ndarray, task_id: str = "segment") -> np.ndarray | None:
339
  if not surface_mask.any():
340
  return None
341
 
342
+ print(f"[{task_id}] Starting shade map build...", flush=True)
343
+ start_time = time.perf_counter()
344
  mask = surface_mask.astype(np.uint8)
345
  luminance = (
346
  img_np[:, :, 0].astype(np.float32) * 0.299
 
373
  smooth = cv2.GaussianBlur(filled, (0, 0), sigmaX=sigma, sigmaY=sigma)
374
  shade = np.clip(smooth / median_lum, 0.55, 1.35)
375
  shade[mask == 0] = 1.0
376
+ result = np.round((shade - 0.55) * (255.0 / 0.80)).clip(0, 255).astype(np.uint8)
377
+ duration = time.perf_counter() - start_time
378
+ print(f"[{task_id}] Shade map built in {duration:.4f}s", flush=True)
379
+ return result
380
 
381
 
382
+ def build_intrinsic_shade_map(img_np: np.ndarray, surface_mask: np.ndarray, task_id: str = "segment") -> np.ndarray | None:
383
  if not surface_mask.any() or intrinsic_models is None:
384
  return None
385
 
386
+ print(f"[{task_id}] Starting intrinsic shade map build...", flush=True)
387
+ start_time = time.perf_counter()
388
  try:
389
  # Convert image to float32 range [0.0, 1.0] as expected by compphoto/Intrinsic
390
  img_float = img_np.astype(np.float32) / 255.0
 
393
  from intrinsic.pipeline import run_pipeline
394
 
395
  # Use CPU/CUDA device string
396
+ results = run_pipeline(intrinsic_models, img_float, stage=1, device=str(device))
397
 
398
  # Extract shading map
399
  shading = None
 
443
  relative_shading[surface_mask == 0] = 1.0
444
 
445
  # Encode to [0, 255] byte range matching the frontend
446
+ result = np.round((relative_shading - 0.55) * (255.0 / 0.80)).clip(0, 255).astype(np.uint8)
447
+ duration = time.perf_counter() - start_time
448
+ print(f"[{task_id}] Intrinsic shade map built in {duration:.4f}s", flush=True)
449
+ return result
450
  except Exception as exc:
451
+ print(f"[{task_id}] Intrinsic shading decomposition failed: {exc}. Falling back to default luminance shading.", flush=True)
452
  return None
453
 
454
 
 
576
  return {"x": float(center[0]), "y": float(center[1])}
577
 
578
 
579
+ def estimate_floor_plane(mask: np.ndarray, img_np: np.ndarray, task_id: str = "segment"):
580
+ print(f"[{task_id}] Starting floor plane estimation...", flush=True)
581
+ start_time = time.perf_counter()
582
  ys, xs = np.where(mask > 0)
583
  if len(xs) < 1000:
584
  return None, None
 
639
  return None, None
640
  dst = np.float32([[x1, y2], [x2, y2], [x2, y1], [x1, y1]])
641
  homography = cv2.getPerspectiveTransform(src, dst).flatten().tolist()
642
+ duration = time.perf_counter() - start_time
643
+ print(f"[{task_id}] Floor plane estimation completed in {duration:.4f}s", flush=True)
644
  return homography, {
645
  "x": x1,
646
  "y": y1,
 
656
  seg_map: np.ndarray,
657
  quad: np.ndarray | None,
658
  depth: np.ndarray | None,
659
+ task_id: str = "segment",
660
  ):
661
+ print(f"[{task_id}] Starting floor surface mask build...", flush=True)
662
+ start_time = time.perf_counter()
663
  h, w = floor_mask.shape[:2]
664
  kern_size = max(5, min(h, w) // 160) | 1
665
  kern = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kern_size, kern_size))
 
706
  surface = cv2.dilate(surface, boundary_kern, iterations=1)
707
  surface[occ_dilated > 0] = 0
708
  surface[reject_dilated > 0] = 0
709
+ duration = time.perf_counter() - start_time
710
+ print(f"[{task_id}] Floor surface mask built in {duration:.4f}s", flush=True)
711
  return surface
712
 
713
 
714
+ def run_segmentation(img: Image.Image, img_np: np.ndarray, task_id: str = "segment"):
715
  h, w = img_np.shape[:2]
716
+ print(f"[{task_id}] Running segmentation (backend: {segmentation_backend})...", flush=True)
717
+ start_time = time.perf_counter()
718
  if segmentation_backend == "oneformer":
719
  inputs = seg_processor(
720
  images=img,
 
727
  outputs,
728
  target_sizes=[(h, w)],
729
  )[0]
730
+ seg_map = result.cpu().numpy().astype(np.uint8)
731
 
732
+ elif segmentation_backend == "mask2former":
733
  inputs = seg_processor(images=img, return_tensors="pt").to(device)
734
  with torch.no_grad():
735
  outputs = seg_model(**inputs)
 
743
  pan_map = pan_result["segmentation"].cpu().numpy()
744
  for seg_info in pan_result["segments_info"]:
745
  seg_map[pan_map == seg_info["id"]] = min(seg_info["label_id"], 255)
746
+ else:
747
+ result = seg_processor.post_process_semantic_segmentation(
748
+ outputs,
749
+ target_sizes=[(h, w)],
750
+ )[0]
751
+ seg_map = result.cpu().numpy().astype(np.uint8)
752
+
753
+ else:
754
+ inputs = seg_processor(images=img, return_tensors="pt").to(device)
755
+ with torch.no_grad():
756
+ outputs = seg_model(**inputs)
757
+ seg = outputs.logits.argmax(dim=1).squeeze().cpu().numpy()
758
+ seg_map = cv2.resize(seg.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST)
759
 
760
+ duration = time.perf_counter() - start_time
761
+ print(f"[{task_id}] Segmentation completed in {duration:.4f}s", flush=True)
762
+ return seg_map
 
 
763
 
764
 
765
  def segmenter_metadata_name() -> str:
 
768
  return segmentation_backend
769
 
770
 
771
+ def build_segmentation_bundle(contents: bytes, task_id: str = "segment"):
772
+ print(f"[{task_id}] Starting bundle build...", flush=True)
773
+ start_total = time.perf_counter()
774
  img = Image.open(io.BytesIO(contents)).convert("RGB")
775
  img_np = np.array(img)
776
  h, w = img_np.shape[:2]
777
  min_floor_area = max(1200, int(w * h * 0.015))
778
 
779
+ seg_map = run_segmentation(img, img_np, task_id=task_id)
780
+ pixels_b64 = base64.b64encode(contents).decode()
 
781
 
782
  primary_floor_ids = class_ids(PRIMARY_FLOOR_CLASSES)
783
  floor_class_ids = class_ids(FLOOR_SURFACE_CLASSES)
 
789
  floor_mask = wall_subtract(floor_mask, seg_map, dilation=1)
790
  floor_mask = clean_floor_mask(floor_mask)
791
 
792
+ depth = estimate_depth(img, w, h, task_id=task_id)
793
+ homography, plane = estimate_floor_plane(floor_mask, img_np, task_id=task_id)
794
  quad = np.asarray(plane["quad"], dtype=np.float32).reshape(4, 2) if plane and plane.get("quad") else None
795
+ surface_mask = build_floor_surface_mask(floor_mask, seg_map, quad, depth, task_id=task_id)
796
  surface_indices = np.flatnonzero(surface_mask.ravel()).astype(np.uint32)
797
  shade_map = None
798
  if ENABLE_INTRINSIC_SHADING and intrinsic_models is not None:
799
+ shade_map = build_intrinsic_shade_map(img_np, surface_mask, task_id=task_id)
800
  if shade_map is None:
801
+ shade_map = build_shade_map(img_np, surface_mask, task_id=task_id)
802
  segments = []
803
 
804
  if len(surface_indices) >= min_floor_area:
 
838
  },
839
  })
840
 
841
+ duration = time.perf_counter() - start_total
842
+ print(f"[{task_id}] Bundle build completed in {duration:.4f}s", flush=True)
843
  return {"width": w, "height": h, "pixels": pixels_b64, "segments": segments}
844
 
845
 
 
859
 
860
 
861
  def run_conversion_task(job_id: str, upload_path: Path):
862
+ print(f"[{job_id}] Starting background conversion task...", flush=True)
863
+ start_time = time.perf_counter()
864
  try:
865
  image_bytes = upload_path.read_bytes()
866
+ bundle = build_segmentation_bundle(image_bytes, task_id=job_id)
867
  (JOB_DIR / f"{job_id}.bundle.json").write_text(json.dumps(bundle))
868
  job = read_job(job_id)
869
  job["status"] = "COMPLETED"
870
  write_job(job)
871
+ duration = time.perf_counter() - start_time
872
+ print(f"[{job_id}] Background conversion task completed in {duration:.4f}s", flush=True)
873
  except Exception as exc:
874
+ duration = time.perf_counter() - start_time
875
+ print(f"[{job_id}] Background conversion failed after {duration:.4f}s: {exc}", flush=True)
876
  try:
877
  job = read_job(job_id)
878
  job["status"] = "FAILED"
 
934
  @app.post("/segment")
935
  async def segment(file: UploadFile = File(...)):
936
  contents = await file.read()
937
+ task_id = f"segment_{uuid.uuid4().hex[:8]}"
938
+ return build_segmentation_bundle(contents, task_id=task_id)
939
 
940
 
941
  if __name__ == "__main__":