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:
1 parent
f1f9d8f638
commit
b0a2d7141d
3 files changed
+46
-1
No files matched your search
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user