feat(work_plane gui): show fit inliers as blue scatter in 3D, drop wrong plane mesh
The 0.4x0.4 PlaneGeometry square was a fixed-size proxy that did not reflect the actual ROI extent — visually misleading. Replace it with an explicit blue point cloud of the inliers fed to fit_plane_svd, so the user can see *exactly* which depth pixels the plane was fit on. - Backend: fit response now returns fit_points_camera (CV frame) instead of the (parallax-shifted) RGB pixel projection. - Frontend: drop the purple plane mesh; new three.setFitInliers paints the inliers in blue. The RGB overlay no longer carries blue dots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4a83305b5e
commit
767d91acdc
3 files changed
+44
-53
No files matched your search
+1
-1
@@ -192,7 +192,7 @@
|
||||
<div class="canvas-wrap" style="width:min(100%,720px)">
|
||||
<canvas id="wp3dCanvas" width="720" height="360" style="display:block;background:#0a0d11;border:1px solid var(--border);border-radius:4px"></canvas>
|
||||
</div>
|
||||
<div class="marker-info">vista camera · piano viola · ciano = punti tenuti · rosso = esclusi (sotto piano−offset) · drag = ruota · wheel = zoom</div>
|
||||
<div class="marker-info">vista camera · blu = punti usati per il fit · ciano = scena tenuta · rosso = scena esclusa (sotto piano−offset) · drag = ruota · wheel = zoom</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Calibrazione</h2>
|
||||
|
||||
+40
-29
@@ -10,7 +10,7 @@ const RGB_REFRESH_MS = 1000;
|
||||
let roi = null; // {u0,v0,u1,v1} in image pixel coords
|
||||
let previewPlane = null; // last response.plane (preview, not saved)
|
||||
let previewPoints = null; // list of [x,y,z] in CV camera frame, meters
|
||||
let fitPointsPx = null; // list of [u,v] pixels of inliers used for the fit
|
||||
let fitPointsCv = null; // list of [x,y,z] inliers used for the fit (CV frame)
|
||||
let savedPlane = null; // last GET /api/work_plane response.plane
|
||||
|
||||
function setChip(text, level) {
|
||||
@@ -90,12 +90,6 @@ function startRgbLoop() {
|
||||
function drawOverlay() {
|
||||
const ctx = overlay.getContext('2d');
|
||||
ctx.clearRect(0, 0, overlay.width, overlay.height);
|
||||
// Inliers used for the last fit, drawn first so the ROI rectangle stays
|
||||
// on top.
|
||||
if (fitPointsPx && fitPointsPx.length) {
|
||||
ctx.fillStyle = '#4da3ff';
|
||||
for (const [u, v] of fitPointsPx) ctx.fillRect(u, v, 2, 2);
|
||||
}
|
||||
if (roi) {
|
||||
ctx.strokeStyle = '#9b6dff';
|
||||
ctx.lineWidth = 2;
|
||||
@@ -106,15 +100,14 @@ function startRgbLoop() {
|
||||
drawCross(ctx, firstCorner.u, firstCorner.v);
|
||||
}
|
||||
}
|
||||
// Re-export so doFit can refresh the overlay after a successful fit.
|
||||
window.__wpDrawOverlay = drawOverlay;
|
||||
|
||||
viewport.addEventListener('click', (e) => {
|
||||
const p = imgPixelFromEvent(e);
|
||||
if (firstCorner === null && roi === null) {
|
||||
// First click: place corner A. Drop any stale fit-points overlay so we
|
||||
// don't suggest the new ROI is being analyzed yet.
|
||||
fitPointsPx = null;
|
||||
// First click: place corner A. Drop any stale fit-inliers from the 3D
|
||||
// viewer so we don't suggest the new ROI is being analyzed yet.
|
||||
fitPointsCv = null;
|
||||
three.setFitInliers(null);
|
||||
firstCorner = p;
|
||||
drawOverlay();
|
||||
setChip('punto 1/2 piazzato — clicca il secondo angolo');
|
||||
@@ -143,7 +136,8 @@ function startRgbLoop() {
|
||||
}
|
||||
// ROI already set: start a new selection on the next click.
|
||||
roi = null;
|
||||
fitPointsPx = null;
|
||||
fitPointsCv = null;
|
||||
three.setFitInliers(null);
|
||||
firstCorner = p;
|
||||
drawOverlay();
|
||||
setRoiInfo();
|
||||
@@ -173,26 +167,19 @@ const three = (function setupThree() {
|
||||
controls.target.set(0, 0, 1);
|
||||
controls.update();
|
||||
scene.add(new THREE.AxesHelper(0.1));
|
||||
const planeMesh = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(0.4, 0.4),
|
||||
new THREE.MeshBasicMaterial({ color: 0x9b6dff, transparent: true, opacity: 0.3, side: THREE.DoubleSide }),
|
||||
);
|
||||
planeMesh.visible = false;
|
||||
scene.add(planeMesh);
|
||||
// The fitted plane is implied by the blue inlier cloud; no extra mesh is
|
||||
// drawn (the previous PlaneGeometry was a fixed 0.4x0.4 square that did
|
||||
// not reflect the actual ROI extent and was misleading).
|
||||
let scatter = null;
|
||||
let fitInliersScatter = null;
|
||||
|
||||
function render() { renderer.render(scene, camera); }
|
||||
(function loop() { controls.update(); render(); requestAnimationFrame(loop); })();
|
||||
|
||||
function setPlane(plane) {
|
||||
if (!plane) { planeMesh.visible = false; return; }
|
||||
const c = plane.centroid;
|
||||
const n = plane.normal;
|
||||
planeMesh.position.set(c[0], c[1], c[2]);
|
||||
// Align plane to its normal.
|
||||
planeMesh.lookAt(c[0] + n[0], c[1] + n[1], c[2] + n[2]);
|
||||
planeMesh.visible = true;
|
||||
if (!plane) return;
|
||||
// Move the orbit target onto the plane centroid so rotation feels natural.
|
||||
const c = plane.centroid;
|
||||
controls.target.set(c[0], c[1], c[2]);
|
||||
controls.update();
|
||||
}
|
||||
@@ -231,7 +218,29 @@ const three = (function setupThree() {
|
||||
scene.add(scatter);
|
||||
}
|
||||
|
||||
return { setPlane, setScatter };
|
||||
function setFitInliers(pointsCv) {
|
||||
if (fitInliersScatter) {
|
||||
scene.remove(fitInliersScatter);
|
||||
fitInliersScatter.geometry.dispose();
|
||||
fitInliersScatter.material.dispose();
|
||||
fitInliersScatter = null;
|
||||
}
|
||||
if (!pointsCv || pointsCv.length === 0) return;
|
||||
const n = pointsCv.length;
|
||||
const positions = new Float32Array(n * 3);
|
||||
for (let i = 0; i < n; i++) {
|
||||
positions[3 * i] = pointsCv[i][0];
|
||||
positions[3 * i + 1] = pointsCv[i][1];
|
||||
positions[3 * i + 2] = pointsCv[i][2];
|
||||
}
|
||||
const geom = new THREE.BufferGeometry();
|
||||
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
const mat = new THREE.PointsMaterial({ color: 0x4da3ff, size: 0.006 });
|
||||
fitInliersScatter = new THREE.Points(geom, mat);
|
||||
scene.add(fitInliersScatter);
|
||||
}
|
||||
|
||||
return { setPlane, setScatter, setFitInliers };
|
||||
})();
|
||||
|
||||
// ---------------- State + button handlers ----------------
|
||||
@@ -289,10 +298,10 @@ async function doFit() {
|
||||
if (r.detail) throw new Error(formatDetail(r.detail));
|
||||
previewPlane = r.plane;
|
||||
previewPoints = r.points_camera;
|
||||
fitPointsPx = r.fit_points_px || null;
|
||||
fitPointsCv = r.fit_points_camera || null;
|
||||
three.setPlane(previewPlane);
|
||||
three.setScatter(previewPoints, previewPlane, previewPlane.offset_mm);
|
||||
if (window.__wpDrawOverlay) window.__wpDrawOverlay();
|
||||
three.setFitInliers(fitPointsCv);
|
||||
renderDetails(previewPlane);
|
||||
$('wpStateBlock').innerHTML = `<strong>preview</strong> · RMS ${previewPlane.fit_rms_mm.toFixed(1)} mm · n_inliers ${previewPlane.n_inliers} · offset ${previewPlane.offset_mm} mm · <span class="warn">non salvato</span>`;
|
||||
setChip('preview pronto', 'ok');
|
||||
@@ -332,8 +341,10 @@ async function doClear() {
|
||||
savedPlane = null;
|
||||
previewPlane = null;
|
||||
previewPoints = null;
|
||||
fitPointsCv = null;
|
||||
three.setPlane(null);
|
||||
three.setScatter(null);
|
||||
three.setFitInliers(null);
|
||||
$('wpStateBlock').innerHTML = '<div class="dim">nessun piano</div>';
|
||||
$('wpDetails').textContent = '—';
|
||||
setChip('nessun piano');
|
||||
|
||||
Reference in new issue
Block a user