diff --git a/src/presa_mano/api/grasp_routes.py b/src/presa_mano/api/grasp_routes.py index efa07a1..16b85c4 100644 --- a/src/presa_mano/api/grasp_routes.py +++ b/src/presa_mano/api/grasp_routes.py @@ -316,6 +316,34 @@ def find_model(req: FindModelReq) -> dict[str, Any]: # as a separate field so the GUI can color it independently. sam21_px = project_camera_to_pixels(seg.points_cv_m, K, (W, H)) + # 6. Project the work plane as a 30cm square anchored at its centroid, + # so the GUI can overlay it on the RGB image. + plane_polygon_px: list[list[int]] = [] + wp = get_work_plane() + if wp is not None: + with sw.step("project_plane"): + n_vec = np.asarray(wp.normal, dtype=np.float64) + c_vec = np.asarray(wp.centroid, dtype=np.float64) + # Pick an arbitrary in-plane vector orthogonal to n. + ref = np.array([1.0, 0.0, 0.0]) if abs(n_vec[0]) < 0.9 else np.array([0.0, 1.0, 0.0]) + u_vec = ref - (ref @ n_vec) * n_vec + u_vec /= np.linalg.norm(u_vec) or 1.0 + v_vec = np.cross(n_vec, u_vec) + half = 0.15 # 30 cm square + corners = np.stack([ + c_vec + half * u_vec + half * v_vec, + c_vec - half * u_vec + half * v_vec, + c_vec - half * u_vec - half * v_vec, + c_vec + half * u_vec - half * v_vec, + ]) + # Project each corner manually: project_camera_to_pixels drops + # off-frame points which would break the polygon, so allow them. + zs = corners[:, 2] + if (zs > 0).all(): + proj = (K @ corners.T).T + uv = proj[:, :2] / proj[:, 2:3] + plane_polygon_px = np.round(uv).astype(int).tolist() + with sw.step("encode_rgb"): rgb_b64 = base64.b64encode(seg.rgb_jpeg).decode("ascii") @@ -333,6 +361,7 @@ def find_model(req: FindModelReq) -> dict[str, Any]: "accepted_points_px": accepted_px.tolist(), "excluded_points_px": excluded_px.tolist(), "sam21_points_px": sam21_px.tolist(), + "plane_polygon_px": plane_polygon_px, "n_accepted": int(accepted_m.shape[0]), "n_excluded": int(excluded_m.shape[0]), "n_sam21": int(seg.points_cv_m.shape[0]), diff --git a/web/app.js b/web/app.js index c648ff5..bf8a86d 100644 --- a/web/app.js +++ b/web/app.js @@ -540,7 +540,7 @@ $('findBtn').addEventListener('click', async () => { } }); -['findShowModel', 'findShowAccepted', 'findShowExcluded', 'findShowSam21'].forEach(id => +['findShowModel', 'findShowAccepted', 'findShowExcluded', 'findShowSam21', 'findShowPlane'].forEach(id => $(id).addEventListener('change', drawFindResult)); function drawFindResult() { @@ -548,6 +548,21 @@ function drawFindResult() { const [W, H] = findResult.rgb_size; if (findCanvas.width !== W) { findCanvas.width = W; findCanvas.height = H; } findCtx.drawImage(findRgbImg, 0, 0); + // Plane polygon (background, semi-transparent fill + outline). + if ($('findShowPlane').checked) { + const poly = findResult.plane_polygon_px || []; + if (poly.length >= 3) { + findCtx.beginPath(); + findCtx.moveTo(poly[0][0], poly[0][1]); + for (let i = 1; i < poly.length; i++) findCtx.lineTo(poly[i][0], poly[i][1]); + findCtx.closePath(); + findCtx.fillStyle = 'rgba(155,109,255,0.20)'; + findCtx.fill(); + findCtx.strokeStyle = '#9b6dff'; + findCtx.lineWidth = 2; + findCtx.stroke(); + } + } const dot = (pts, color, r) => { findCtx.fillStyle = color; for (const [u, v] of pts) findCtx.fillRect(u - r, v - r, r*2+1, r*2+1); diff --git a/web/index.html b/web/index.html index 89aede0..2f644be 100644 --- a/web/index.html +++ b/web/index.html @@ -269,6 +269,7 @@ +
—