diff --git a/src/presa_mano/api/grasp_routes.py b/src/presa_mano/api/grasp_routes.py index 3500bfc..4156575 100644 --- a/src/presa_mano/api/grasp_routes.py +++ b/src/presa_mano/api/grasp_routes.py @@ -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