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 ..calibration import CalibrationStore
from ..clients import G1Unavailable, Sam21Error, Sam21Unavailable
import httpx
from ..clients import G1Unavailable, NuwaError, Sam21Error, Sam21Unavailable
from ..config import get_config
from ..geometry import transform_points
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))
return pts_base, meta
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)
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:
"""SAM21 path: bbox → segmented point cloud in RGB-camera/OpenCV frame.
Returns points in **meters**. Raises _Sam21Fallback on any failure that
should trigger the bbox path; lets unexpected exceptions propagate as
_Sam21Fallback with repr(e).
Returns points in **meters**. Raises _Sam21Fallback on failures that should
trigger the bbox path. Re-raises HTTPException(502) for Nuwa failures
(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:
with sw.step("nuwa_grab"):
rgb_jpeg = nuwa.rgb_jpeg()
depth_png_in = nuwa.depth_png()
intr_full = nuwa.intrinsics()
rgb_intr = intr_full.get("rgb")
if not rgb_intr:
raise _Sam21Fallback("nuwa intrinsics missing 'rgb' block")
except (NuwaError, httpx.RequestError) as e:
raise HTTPException(status_code=502, detail=f"Nuwa unreachable: {e}")
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
# 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)
if bgr is None:
log.warning("SAM21 path skipped: could not decode Nuwa RGB JPEG")
raise _Sam21Fallback("could not decode Nuwa RGB JPEG")
ok, rgb_png_buf = cv2.imencode(".png", bgr)
if not ok:
log.warning("SAM21 path skipped: could not re-encode RGB as PNG")
raise _Sam21Fallback("could not re-encode RGB as PNG")
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),
cv2.IMREAD_UNCHANGED)
if depth_ir is None:
log.warning("SAM21 path skipped: could not decode Nuwa depth PNG")
raise _Sam21Fallback("could not decode Nuwa depth PNG")
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}")
reg = CameraRegistration.from_nuwa(intr_full)
depth_aligned = align_depth_to_rgb(depth_ir, reg)
ok, depth_png_buf = cv2.imencode(".png", depth_aligned)
if not ok:
log.warning("SAM21 path skipped: could not encode aligned depth as PNG")
raise _Sam21Fallback("could not encode aligned depth as PNG")
depth_png_out = bytes(depth_png_buf)
except _Sam21Fallback:
raise
except Exception as e:
log.error("Unexpected SAM21 prep error: %r", e)
raise _Sam21Fallback(f"prep failed: {e!r}") from e
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,
)
except Sam21Unavailable as e:
log.warning("SAM21 unreachable, falling back to bbox: %s", e)
raise _Sam21Fallback(f"unreachable: {e}") from e
except Sam21Error as e:
if e.code in _SAM21_BUG_CODES:
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
except Exception as e:
log.error("Unexpected SAM21 error: %r", e)
raise _Sam21Fallback(f"unexpected: {e!r}") from e
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]}")
return pts_cv