fix(grasp): single-log SAM21 fallback + fail-fast on Nuwa errors

- All SAM21-path logging now lives in _points_via_sam21 at the right
  level (WARNING for operational, ERROR for bug codes / unexpected).
  _object_points_in_base no longer double-logs on fallback.
- Nuwa failures during SAM21 prep now raise HTTPException(502)
  immediately instead of running the futile bbox fallback (which would
  also fail because Nuwa is down).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal PastroandClaude Opus 4.7 committed 2026-05-11 15:30:01 +02:00
1 parent 1537a9e389
commit b9bf878445
1 file changed
+30 -9
+30 -9
View File
@@ -13,7 +13,9 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ..calibration import CalibrationStore from ..calibration import CalibrationStore
from ..clients import G1Unavailable, Sam21Error, Sam21Unavailable import httpx
from ..clients import G1Unavailable, NuwaError, Sam21Error, Sam21Unavailable
from ..config import get_config from ..config import get_config
from ..geometry import transform_points from ..geometry import transform_points
from ..grasp import GraspPlanner, compute_axes from ..grasp import GraspPlanner, compute_axes
@@ -93,7 +95,7 @@ def _object_points_in_base(conf: float | None, class_name: str | None, sw: Stopw
meta["n_points"] = int(len(pts_base)) meta["n_points"] = int(len(pts_base))
return pts_base, meta return pts_base, meta
except _Sam21Fallback as e: except _Sam21Fallback as e:
log.warning("SAM21 path failed, falling back to bbox: %s", e) # Logging happens inside _points_via_sam21 with the right level.
meta["sam21_fallback_reason"] = str(e) meta["sam21_fallback_reason"] = str(e)
pts_base = _points_via_bbox(nuwa, T_base_cam, bbox, sw) pts_base = _points_via_bbox(nuwa, T_base_cam, bbox, sw)
@@ -132,27 +134,37 @@ def _points_via_bbox(nuwa, T_base_cam: np.ndarray, bbox: tuple[int, int, int, in
def _points_via_sam21(nuwa, bbox: tuple[int, int, int, int], sw: Stopwatch) -> np.ndarray: def _points_via_sam21(nuwa, bbox: tuple[int, int, int, int], sw: Stopwatch) -> np.ndarray:
"""SAM21 path: bbox → segmented point cloud in RGB-camera/OpenCV frame. """SAM21 path: bbox → segmented point cloud in RGB-camera/OpenCV frame.
Returns points in **meters**. Raises _Sam21Fallback on any failure that Returns points in **meters**. Raises _Sam21Fallback on failures that should
should trigger the bbox path; lets unexpected exceptions propagate as trigger the bbox path. Re-raises HTTPException(502) for Nuwa failures
_Sam21Fallback with repr(e). (the bbox path would fail too — better to fail fast). All SAM21-specific
logging happens here; the caller does not log on fallback.
""" """
# Nuwa I/O: if it fails the bbox path will also fail, so fail fast with 502
# rather than logging a deceptive "falling back to bbox" warning.
try: try:
with sw.step("nuwa_grab"): with sw.step("nuwa_grab"):
rgb_jpeg = nuwa.rgb_jpeg() rgb_jpeg = nuwa.rgb_jpeg()
depth_png_in = nuwa.depth_png() depth_png_in = nuwa.depth_png()
intr_full = nuwa.intrinsics() intr_full = nuwa.intrinsics()
rgb_intr = intr_full.get("rgb") except (NuwaError, httpx.RequestError) as e:
if not rgb_intr: raise HTTPException(status_code=502, detail=f"Nuwa unreachable: {e}")
raise _Sam21Fallback("nuwa intrinsics missing 'rgb' block")
rgb_intr = intr_full.get("rgb")
if not rgb_intr:
log.warning("SAM21 path skipped: nuwa intrinsics missing 'rgb' block")
raise _Sam21Fallback("nuwa intrinsics missing 'rgb' block")
try:
# JPEG → PNG. cv2.imdecode gives BGR; cv2.imencode(".png", bgr) writes # JPEG → PNG. cv2.imdecode gives BGR; cv2.imencode(".png", bgr) writes
# PNG bytes with standard R,G,B color order (libpng handles the swap), # PNG bytes with standard R,G,B color order (libpng handles the swap),
# which SAM21's PIL-based decoder reads as RGB. Verified by inspection. # which SAM21's PIL-based decoder reads as RGB.
bgr = cv2.imdecode(np.frombuffer(rgb_jpeg, np.uint8), cv2.IMREAD_COLOR) bgr = cv2.imdecode(np.frombuffer(rgb_jpeg, np.uint8), cv2.IMREAD_COLOR)
if bgr is None: if bgr is None:
log.warning("SAM21 path skipped: could not decode Nuwa RGB JPEG")
raise _Sam21Fallback("could not decode Nuwa RGB JPEG") raise _Sam21Fallback("could not decode Nuwa RGB JPEG")
ok, rgb_png_buf = cv2.imencode(".png", bgr) ok, rgb_png_buf = cv2.imencode(".png", bgr)
if not ok: if not ok:
log.warning("SAM21 path skipped: could not re-encode RGB as PNG")
raise _Sam21Fallback("could not re-encode RGB as PNG") raise _Sam21Fallback("could not re-encode RGB as PNG")
rgb_png = bytes(rgb_png_buf) rgb_png = bytes(rgb_png_buf)
@@ -160,18 +172,23 @@ def _points_via_sam21(nuwa, bbox: tuple[int, int, int, int], sw: Stopwatch) -> n
depth_ir = cv2.imdecode(np.frombuffer(depth_png_in, np.uint8), depth_ir = cv2.imdecode(np.frombuffer(depth_png_in, np.uint8),
cv2.IMREAD_UNCHANGED) cv2.IMREAD_UNCHANGED)
if depth_ir is None: if depth_ir is None:
log.warning("SAM21 path skipped: could not decode Nuwa depth PNG")
raise _Sam21Fallback("could not decode Nuwa depth PNG") raise _Sam21Fallback("could not decode Nuwa depth PNG")
if depth_ir.dtype != np.uint16: if depth_ir.dtype != np.uint16:
log.warning("SAM21 path skipped: depth PNG dtype %s (expected uint16)",
depth_ir.dtype)
raise _Sam21Fallback(f"depth PNG dtype expected uint16, got {depth_ir.dtype}") raise _Sam21Fallback(f"depth PNG dtype expected uint16, got {depth_ir.dtype}")
reg = CameraRegistration.from_nuwa(intr_full) reg = CameraRegistration.from_nuwa(intr_full)
depth_aligned = align_depth_to_rgb(depth_ir, reg) depth_aligned = align_depth_to_rgb(depth_ir, reg)
ok, depth_png_buf = cv2.imencode(".png", depth_aligned) ok, depth_png_buf = cv2.imencode(".png", depth_aligned)
if not ok: if not ok:
log.warning("SAM21 path skipped: could not encode aligned depth as PNG")
raise _Sam21Fallback("could not encode aligned depth as PNG") raise _Sam21Fallback("could not encode aligned depth as PNG")
depth_png_out = bytes(depth_png_buf) depth_png_out = bytes(depth_png_buf)
except _Sam21Fallback: except _Sam21Fallback:
raise raise
except Exception as e: except Exception as e:
log.error("Unexpected SAM21 prep error: %r", e)
raise _Sam21Fallback(f"prep failed: {e!r}") from e raise _Sam21Fallback(f"prep failed: {e!r}") from e
with sw.step("sam21"): with sw.step("sam21"):
@@ -184,16 +201,20 @@ def _points_via_sam21(nuwa, bbox: tuple[int, int, int, int], sw: Stopwatch) -> n
depth_scale_mm=1.0, depth_scale_mm=1.0,
) )
except Sam21Unavailable as e: except Sam21Unavailable as e:
log.warning("SAM21 unreachable, falling back to bbox: %s", e)
raise _Sam21Fallback(f"unreachable: {e}") from e raise _Sam21Fallback(f"unreachable: {e}") from e
except Sam21Error as e: except Sam21Error as e:
if e.code in _SAM21_BUG_CODES: if e.code in _SAM21_BUG_CODES:
log.error("SAM21 rejected payload (%s): %s", e.code, e.message) log.error("SAM21 rejected payload (%s): %s", e.code, e.message)
else:
log.warning("SAM21 returned %s: %s — falling back to bbox", e.code, e.message)
raise _Sam21Fallback(f"{e.code}: {e.message}") from e raise _Sam21Fallback(f"{e.code}: {e.message}") from e
except Exception as e: except Exception as e:
log.error("Unexpected SAM21 error: %r", e) log.error("Unexpected SAM21 error: %r", e)
raise _Sam21Fallback(f"unexpected: {e!r}") from e raise _Sam21Fallback(f"unexpected: {e!r}") from e
if pts_cv.shape[0] < 10: if pts_cv.shape[0] < 10:
log.warning("SAM21 returned %d points (<10), falling back to bbox", pts_cv.shape[0])
raise _Sam21Fallback(f"too few points: {pts_cv.shape[0]}") raise _Sam21Fallback(f"too few points: {pts_cv.shape[0]}")
return pts_cv return pts_cv