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:
1 parent
1537a9e389
commit
b9bf878445
1 file changed
+27
-6
@@ -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()
|
||||||
|
except (NuwaError, httpx.RequestError) as e:
|
||||||
|
raise HTTPException(status_code=502, detail=f"Nuwa unreachable: {e}")
|
||||||
|
|
||||||
rgb_intr = intr_full.get("rgb")
|
rgb_intr = intr_full.get("rgb")
|
||||||
if not rgb_intr:
|
if not rgb_intr:
|
||||||
|
log.warning("SAM21 path skipped: nuwa intrinsics missing 'rgb' block")
|
||||||
raise _Sam21Fallback("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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user