feat(find_model gui): toggle to overlay work plane as a violet polygon

The find_model response now returns plane_polygon_px (4 corners of a
30 cm square anchored at the work plane centroid, projected to RGB
pixels via the same K used for the SAM21 cloud). The GUI gains a
'Piano (viola)' checkbox that draws the polygon as a translucent
violet quad with an outline, helping the operator see whether the
plane sits where expected relative to the object.

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-13 11:53:16 +02:00
1 parent f1f9d8f638
commit b0a2d7141d
3 files changed
+46 -1

No files matched your search

+29
View File
@@ -316,6 +316,34 @@ def find_model(req: FindModelReq) -> dict[str, Any]:
# as a separate field so the GUI can color it independently. # as a separate field so the GUI can color it independently.
sam21_px = project_camera_to_pixels(seg.points_cv_m, K, (W, H)) 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"): with sw.step("encode_rgb"):
rgb_b64 = base64.b64encode(seg.rgb_jpeg).decode("ascii") 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(), "accepted_points_px": accepted_px.tolist(),
"excluded_points_px": excluded_px.tolist(), "excluded_points_px": excluded_px.tolist(),
"sam21_points_px": sam21_px.tolist(), "sam21_points_px": sam21_px.tolist(),
"plane_polygon_px": plane_polygon_px,
"n_accepted": int(accepted_m.shape[0]), "n_accepted": int(accepted_m.shape[0]),
"n_excluded": int(excluded_m.shape[0]), "n_excluded": int(excluded_m.shape[0]),
"n_sam21": int(seg.points_cv_m.shape[0]), "n_sam21": int(seg.points_cv_m.shape[0]),
+16 -1
View File
@@ -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)); $(id).addEventListener('change', drawFindResult));
function drawFindResult() { function drawFindResult() {
@@ -548,6 +548,21 @@ function drawFindResult() {
const [W, H] = findResult.rgb_size; const [W, H] = findResult.rgb_size;
if (findCanvas.width !== W) { findCanvas.width = W; findCanvas.height = H; } if (findCanvas.width !== W) { findCanvas.width = W; findCanvas.height = H; }
findCtx.drawImage(findRgbImg, 0, 0); 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) => { const dot = (pts, color, r) => {
findCtx.fillStyle = color; findCtx.fillStyle = color;
for (const [u, v] of pts) findCtx.fillRect(u - r, v - r, r*2+1, r*2+1); for (const [u, v] of pts) findCtx.fillRect(u - r, v - r, r*2+1, r*2+1);
+1
View File
@@ -269,6 +269,7 @@
<label class="inline"><input type="checkbox" id="findShowAccepted"> Punti accettati (verdi)</label> <label class="inline"><input type="checkbox" id="findShowAccepted"> Punti accettati (verdi)</label>
<label class="inline"><input type="checkbox" id="findShowExcluded"> Punti esclusi (rossi)</label> <label class="inline"><input type="checkbox" id="findShowExcluded"> Punti esclusi (rossi)</label>
<label class="inline"><input type="checkbox" id="findShowSam21"> Punti SAM21 (gialli)</label> <label class="inline"><input type="checkbox" id="findShowSam21"> Punti SAM21 (gialli)</label>
<label class="inline"><input type="checkbox" id="findShowPlane"> Piano (viola)</label>
</div> </div>
<pre id="findDetails" class="log small">—</pre> <pre id="findDetails" class="log small">—</pre>