refactor: remove visual-servo feature
The marker-relative servo (endpoints /api/grasp/visual_servo/{plan,step,
plane/*}, helpers in grasp_routes.py, the bulk of grasp/visual_servo.py,
web/servo.js, and the Servo Visuale GUI tab) is gone. Plane fitting now
lives in /api/work_plane/* and perception/work_plane.py; Task 5 wires the
new Calibrazione Piano tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
565c3df2f4
commit
eb53f30e23
5 files changed
+3
-1455
No files matched your search
@@ -1,504 +0,0 @@
|
||||
"""Marker-relative visual servoing (no static hand-eye calibration).
|
||||
|
||||
Idea: the wrist-mounted ArUco marker is our live reference. Each step we read
|
||||
current wrist pose from G1, detect the marker in the camera frame, detect the
|
||||
object (YOLO bbox → Nuwa ROI point cloud → centroid), and plan a wrist move
|
||||
that advances a fraction of the camera-frame marker→object vector.
|
||||
|
||||
Mapping a camera-frame delta to a base-frame wrist delta needs `R_base_camera`,
|
||||
which is derived live each step:
|
||||
|
||||
R_base_camera = R_base_wrist @ R_wrist_marker @ R_cam_marker^-1
|
||||
|
||||
Only the *rotation* part of `T_wrist_marker` matters for deltas — translation
|
||||
cancels. We take `R_wrist_marker` from config (rvec), defaulting to identity
|
||||
when the marker is roughly aligned with the wrist frame.
|
||||
|
||||
Stop condition: camera-frame marker→object distance ≤ `min_distance_m`. This
|
||||
is a direct physical proxy for wrist-to-object proximity, independent of the
|
||||
(unknown) marker-to-wrist translation offset.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..clients import G1Client, MarkerClient, NuwaClient, YoloClient
|
||||
from ..config import data_dir
|
||||
from ..geometry import (
|
||||
T_from_pos_quat,
|
||||
invert_T,
|
||||
make_T,
|
||||
quat_xyzw_to_R,
|
||||
rvec_to_R,
|
||||
)
|
||||
from ..perception import filter_outliers, nuwa_to_cv_frame
|
||||
from ..perception.aruco_pose import nuwa_intrinsics_to_matrix, solve_square_marker
|
||||
from ..perception.detector import ObjectDetector
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServoError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkPlane:
|
||||
"""Plane in camera frame: normal·X + offset = 0, normal is unit-length and
|
||||
pointing roughly toward the camera (i.e. has negative Z in OpenCV frame)."""
|
||||
normal: list[float]
|
||||
offset: float
|
||||
centroid: list[float]
|
||||
n_inliers: int
|
||||
fit_rms_mm: float
|
||||
roi: list[int]
|
||||
created_at: float
|
||||
|
||||
|
||||
_WORK_PLANE: WorkPlane | None = None
|
||||
_WORK_PLANE_LOADED = False
|
||||
|
||||
|
||||
def _work_plane_path() -> Path:
|
||||
return data_dir() / "work_plane.json"
|
||||
|
||||
|
||||
def _save_work_plane_to_disk(wp: WorkPlane | None) -> None:
|
||||
path = _work_plane_path()
|
||||
try:
|
||||
if wp is None:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
return
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w") as f:
|
||||
json.dump(asdict(wp), f, indent=2)
|
||||
except Exception as e:
|
||||
log.warning("failed to persist work plane: %s", e)
|
||||
|
||||
|
||||
def _load_work_plane_from_disk() -> WorkPlane | None:
|
||||
path = _work_plane_path()
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
d = json.load(f)
|
||||
return WorkPlane(**d)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except Exception as e:
|
||||
log.warning("failed to load persisted work plane (%s)", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_work_plane() -> WorkPlane | None:
|
||||
global _WORK_PLANE, _WORK_PLANE_LOADED
|
||||
if not _WORK_PLANE_LOADED:
|
||||
_WORK_PLANE = _load_work_plane_from_disk()
|
||||
_WORK_PLANE_LOADED = True
|
||||
return _WORK_PLANE
|
||||
|
||||
|
||||
def set_work_plane(wp: WorkPlane | None) -> None:
|
||||
global _WORK_PLANE, _WORK_PLANE_LOADED
|
||||
_WORK_PLANE = wp
|
||||
_WORK_PLANE_LOADED = True
|
||||
_save_work_plane_to_disk(wp)
|
||||
|
||||
|
||||
def fit_plane_svd(points_xyz: np.ndarray
|
||||
) -> tuple[np.ndarray, float, np.ndarray, float]:
|
||||
"""Least-squares plane via SVD. Returns (normal, offset, centroid, rms_mm).
|
||||
|
||||
Plane equation: normal·X + offset = 0. Normal is oriented to point toward
|
||||
the camera origin (negative Z in CV frame)."""
|
||||
pts = np.asarray(points_xyz, dtype=np.float64)
|
||||
if pts.ndim != 2 or pts.shape[1] < 3:
|
||||
raise ServoError(f"unexpected points shape for plane fit: {pts.shape}")
|
||||
xyz = pts[:, :3]
|
||||
if len(xyz) < 10:
|
||||
raise ServoError(f"too few points to fit plane: {len(xyz)}")
|
||||
c = xyz.mean(axis=0)
|
||||
centered = xyz - c
|
||||
_, _, Vt = np.linalg.svd(centered, full_matrices=False)
|
||||
normal = Vt[-1]
|
||||
n = float(np.linalg.norm(normal))
|
||||
if n < 1e-9:
|
||||
raise ServoError("degenerate plane (zero normal)")
|
||||
normal = normal / n
|
||||
# Orient normal to point toward the camera origin: the signed distance of
|
||||
# the origin from the plane must be positive, i.e. d = -n·c > 0 → n·c < 0.
|
||||
# Flipping only on normal[2] is wrong for tilted planes (the camera may
|
||||
# not be above the plane in Z-only sense).
|
||||
if float(normal @ c) > 0:
|
||||
normal = -normal
|
||||
offset = -float(normal @ c)
|
||||
signed = centered @ normal
|
||||
rms_mm = float(np.sqrt(np.mean(signed * signed)) * 1000.0)
|
||||
return normal, offset, c, rms_mm
|
||||
|
||||
|
||||
def project_point_on_plane(P: np.ndarray, normal: np.ndarray, offset: float
|
||||
) -> tuple[np.ndarray, float]:
|
||||
"""Perpendicular projection of a 3D point onto the plane.
|
||||
Returns (foot, signed_distance). Signed distance > 0 means P is on the
|
||||
side of the plane the normal points toward (camera side in our convention).
|
||||
"""
|
||||
s = float(normal @ P + offset)
|
||||
return P - s * normal, s
|
||||
|
||||
|
||||
def backproject_pixel_on_plane(uv: np.ndarray, K: np.ndarray,
|
||||
normal: np.ndarray, offset: float
|
||||
) -> np.ndarray:
|
||||
"""Intersect the viewing ray through pixel (u,v) with the work plane."""
|
||||
r = np.linalg.inv(K) @ np.array([float(uv[0]), float(uv[1]), 1.0])
|
||||
denom = float(normal @ r)
|
||||
if abs(denom) < 1e-9:
|
||||
raise ServoError("ray parallel to work plane")
|
||||
t = -offset / denom
|
||||
if t <= 0:
|
||||
raise ServoError("plane intersection is behind the camera")
|
||||
return t * r
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServoPlan:
|
||||
# Truth (from G1)
|
||||
wrist_position_base: list[float]
|
||||
wrist_quaternion_base: list[float]
|
||||
|
||||
# Camera-frame observations
|
||||
marker_position_cam: list[float]
|
||||
object_position_cam: list[float]
|
||||
|
||||
# Pixel-space positions for the 2D overlay (u, v in image coordinates).
|
||||
marker_pixel: list[float]
|
||||
object_pixel: list[float]
|
||||
|
||||
# Perpendicular depth to the camera plane (Z component, m). This is what
|
||||
# drives the pinhole mm/px scale — NOT the slant range.
|
||||
marker_depth_z_m: float
|
||||
object_depth_z_m: float
|
||||
# Euclidean distance camera origin → point (slant range, m).
|
||||
marker_distance_cam_m: float
|
||||
object_distance_cam_m: float
|
||||
|
||||
# Pinhole scale at the point's depth: mm-per-pixel = (z_mm / fx), i.e.
|
||||
# physical size of one image pixel at that depth. Depends on Z only, so
|
||||
# two points at the same Z but different XY will share mm/px.
|
||||
marker_mm_per_px: float
|
||||
object_mm_per_px: float
|
||||
|
||||
# Approximate base-frame positions for visualisation only. Computed with
|
||||
# T_wrist_marker = identity: relative distances are correct, absolute
|
||||
# positions are offset by the unknown marker-to-wrist translation.
|
||||
marker_position_base_est: list[float]
|
||||
object_position_base_est: list[float]
|
||||
T_base_camera_est: list[list[float]]
|
||||
|
||||
# Scalar cues
|
||||
distance_m: float # camera-frame magnitude of marker→object
|
||||
done: bool # True when distance ≤ min_distance_m
|
||||
|
||||
# Next wrist target in base frame (None when done)
|
||||
target_position_base: list[float] | None
|
||||
target_quaternion_base: list[float] | None
|
||||
planned_step_m: float # how much the wrist is about to move (base frame)
|
||||
|
||||
# Work-plane projection block — present only when a plane has been fitted.
|
||||
# Everything camera-frame. `in_plane_distance_m` is the Euclidean distance
|
||||
# between the two foot-of-perpendicular points (lives on the plane).
|
||||
plane: dict[str, Any] | None = None
|
||||
|
||||
# Diagnostics
|
||||
detection: dict[str, Any] | None = None
|
||||
n_object_points: int = 0
|
||||
marker_reprojection_error_px: float | None = None
|
||||
marker_translation_source: str = "solvepnp" # "nuwa_depth" | "solvepnp"
|
||||
object_point_source: str = "centroid" # "centroid" | "highest_above_plane(...)"
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
def _resolve_wrist_pose(g1: G1Client, side: str) -> tuple[np.ndarray, np.ndarray, list[float]]:
|
||||
w = g1.wrist()
|
||||
body = w.get("data") if isinstance(w.get("data"), dict) else w
|
||||
pose = body.get(f"{side}_wrist") or body.get(side)
|
||||
if pose is None or "position" not in pose or "quaternion" not in pose:
|
||||
raise ServoError(f"G1 /wrist missing '{side}' pose: {w!r}")
|
||||
pos = np.asarray(pose["position"], dtype=np.float64)
|
||||
quat = np.asarray(pose["quaternion"], dtype=np.float64)
|
||||
return pos, quat, list(map(float, pose["quaternion"]))
|
||||
|
||||
|
||||
def _detect_marker_cam(nuwa: NuwaClient, marker: MarkerClient,
|
||||
marker_id: int, size_mm: float
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, float | None, str]:
|
||||
"""Return (R_cam_marker, t_cam_marker, corners_px, reprojection_error_px, source).
|
||||
|
||||
Translation comes from the Nuwa depth at the marker centre pixel whenever
|
||||
possible — much more stable than solvePnP on a small planar marker, which
|
||||
suffers from the IPPE_SQUARE planar ambiguity (two solutions, same
|
||||
reprojection error) at ~1.4 m distance. Rotation still comes from solvePnP
|
||||
because the servo mapping cam→base needs it.
|
||||
|
||||
`source` reports how `t_cam_marker` was obtained: "nuwa_depth" when the
|
||||
depth-sensor value at the marker pixel was usable, "solvepnp" otherwise.
|
||||
"""
|
||||
result = marker.detect_url(
|
||||
markers=[{"id": int(marker_id), "size_mm": float(size_mm)}],
|
||||
image_url=nuwa.rgb_url(),
|
||||
)
|
||||
found = result.get("found", [])
|
||||
entry = next((m for m in found if int(m["id"]) == int(marker_id)), None)
|
||||
if entry is None:
|
||||
raise ServoError(f"marker id={marker_id} not detected (not_found={result.get('not_found')})")
|
||||
corners = np.asarray(entry["corners"], dtype=np.float64)
|
||||
|
||||
R_cm = None
|
||||
t_pnp = None
|
||||
reproj_err: float | None = None
|
||||
try:
|
||||
intr = nuwa.intrinsics()
|
||||
K, dist = nuwa_intrinsics_to_matrix(intr)
|
||||
sol = solve_square_marker(corners, float(size_mm) / 1000.0, K, dist)
|
||||
R_cm = sol["R"]
|
||||
t_pnp = sol["t"].flatten()
|
||||
reproj_err = float(sol["reprojection_error_px"])
|
||||
except Exception as e:
|
||||
log.warning("local pnp failed (%s) — using marker-finder orientation", e)
|
||||
R_cm = np.asarray(entry["rotation_matrix"], dtype=np.float64).reshape(3, 3)
|
||||
t_pnp = np.asarray(entry["position_m"], dtype=np.float64).flatten()
|
||||
|
||||
# Prefer Nuwa depth at the marker centre pixel for the translation.
|
||||
centre = corners.mean(axis=0)
|
||||
u = int(round(float(centre[0])))
|
||||
v = int(round(float(centre[1])))
|
||||
t_depth = None
|
||||
try:
|
||||
m = nuwa.measure(u, v, radius=8)
|
||||
pt = m.get("point_m") if isinstance(m, dict) else None
|
||||
if not isinstance(pt, dict):
|
||||
pt = m
|
||||
x = float(pt.get("x", pt.get("X")))
|
||||
y = float(pt.get("y", pt.get("Y")))
|
||||
z = float(pt.get("z", pt.get("Z")))
|
||||
# /api/measure returns already-OpenCV-frame coords (Z>0 in front of
|
||||
# the camera). No flip.
|
||||
t_depth = np.array([x, y, z], dtype=np.float64)
|
||||
if not np.all(np.isfinite(t_depth)) or abs(t_depth[2]) < 1e-3:
|
||||
t_depth = None
|
||||
except Exception as e:
|
||||
log.warning("nuwa.measure failed at (%d,%d): %s", u, v, e)
|
||||
t_depth = None
|
||||
|
||||
if t_depth is not None:
|
||||
return R_cm, t_depth, corners, reproj_err, "nuwa_depth"
|
||||
return R_cm, t_pnp, corners, reproj_err, "solvepnp"
|
||||
|
||||
|
||||
def _detect_object_point_cam(nuwa: NuwaClient, yolo: YoloClient,
|
||||
conf: float | None, class_name: str | None,
|
||||
pointcloud_step: int, pointcloud_max_depth_mm: int,
|
||||
work_plane: WorkPlane | None,
|
||||
top_fraction: float = 0.05,
|
||||
) -> tuple[np.ndarray, dict[str, Any], int, str]:
|
||||
"""Return (point_cam_xyz, top_detection, n_points, source).
|
||||
|
||||
When a work plane is available, returns the mean of the top `top_fraction`
|
||||
of points (by signed distance from the plane) — this is the highest part
|
||||
of the object above the work surface, which is the natural grasp target.
|
||||
Without a plane, falls back to the centroid of all filtered points."""
|
||||
detector = ObjectDetector(yolo, nuwa)
|
||||
dets = detector.detect(conf=conf)
|
||||
if class_name:
|
||||
dets = [d for d in dets if (d.get("class") or "").lower() == class_name.lower()]
|
||||
if not dets:
|
||||
raise ServoError("no object detected" + (f" for class '{class_name}'" if class_name else ""))
|
||||
dets.sort(key=lambda d: d["confidence"], reverse=True)
|
||||
top = dets[0]
|
||||
b = top["bbox"]
|
||||
u_min = max(0, int(np.floor(b["x1"])))
|
||||
v_min = max(0, int(np.floor(b["y1"])))
|
||||
u_max = int(np.ceil(b["x2"]))
|
||||
v_max = int(np.ceil(b["y2"]))
|
||||
pc, _meta = nuwa.pointcloud_roi(u_min, v_min, u_max, v_max,
|
||||
step=pointcloud_step,
|
||||
max_depth_mm=pointcloud_max_depth_mm)
|
||||
# Empirical: Nuwa ROI points come in Nuwa-native frame (Z<0 in front of
|
||||
# camera) despite the docstring. Flip to true OpenCV frame so comparisons
|
||||
# with the plane (also CV) are consistent.
|
||||
cropped = filter_outliers(pc)
|
||||
if len(cropped) < 10:
|
||||
raise ServoError(f"too few valid depth points in bbox: {len(cropped)}")
|
||||
pts_cv = nuwa_to_cv_frame(cropped[:, :3])
|
||||
if work_plane is None:
|
||||
return pts_cv.mean(axis=0), top, int(len(pts_cv)), "centroid"
|
||||
n_vec = np.asarray(work_plane.normal, dtype=np.float64)
|
||||
d_off = float(work_plane.offset)
|
||||
signed = pts_cv @ n_vec + d_off # Nx (positive = above plane)
|
||||
k = max(1, int(round(float(top_fraction) * len(pts_cv))))
|
||||
idx = np.argpartition(signed, -k)[-k:]
|
||||
highest = pts_cv[idx].mean(axis=0)
|
||||
return highest, top, int(len(pts_cv)), f"highest_above_plane(top {k}/{len(pts_cv)})"
|
||||
|
||||
|
||||
def _r_wrist_marker_from_config(cfg: dict[str, Any]) -> np.ndarray:
|
||||
"""Rotation part of the marker-on-wrist rigid offset. Translation does not
|
||||
matter for camera-frame delta servo. Read from
|
||||
`grasp.visual_servo.wrist_marker_rvec` as a 3-vector (Rodrigues), default
|
||||
identity."""
|
||||
vs = cfg.get("grasp", {}).get("visual_servo", {}) or {}
|
||||
rvec = vs.get("wrist_marker_rvec") or [0.0, 0.0, 0.0]
|
||||
return rvec_to_R(np.asarray(rvec, dtype=np.float64))
|
||||
|
||||
|
||||
def plan(*, side: str, marker_id: int, size_mm: float,
|
||||
conf: float | None, class_name: str | None,
|
||||
min_distance_m: float, step_fraction: float,
|
||||
pointcloud_step: int, pointcloud_max_depth_mm: int,
|
||||
cfg: dict[str, Any],
|
||||
nuwa: NuwaClient, marker: MarkerClient, yolo: YoloClient, g1: G1Client,
|
||||
) -> ServoPlan:
|
||||
# 1) Wrist pose in base
|
||||
wrist_pos, wrist_quat, wrist_quat_list = _resolve_wrist_pose(g1, side)
|
||||
R_bw = quat_xyzw_to_R(wrist_quat)
|
||||
|
||||
# 2) Marker pose in camera
|
||||
R_cm, t_cm, corners_px, reproj_err, marker_t_source = _detect_marker_cam(
|
||||
nuwa, marker, marker_id, size_mm,
|
||||
)
|
||||
marker_pixel = corners_px.mean(axis=0) # centre of the 4 corners
|
||||
# Intrinsics for mm/px scale at each point's depth.
|
||||
try:
|
||||
intr = nuwa.intrinsics()
|
||||
K, _ = nuwa_intrinsics_to_matrix(intr)
|
||||
fx = float(K[0, 0])
|
||||
except Exception as e:
|
||||
log.warning("intrinsics unavailable for mm/px (%s)", e)
|
||||
fx = float("nan")
|
||||
|
||||
# 3) Object reference point in camera: highest-above-plane if a plane is
|
||||
# set, otherwise centroid of the bbox pointcloud.
|
||||
wp_here = get_work_plane()
|
||||
centroid_cam, detection, n_pts, object_point_source = _detect_object_point_cam(
|
||||
nuwa, yolo, conf, class_name,
|
||||
pointcloud_step, pointcloud_max_depth_mm,
|
||||
work_plane=wp_here,
|
||||
)
|
||||
b = detection["bbox"]
|
||||
object_pixel = np.array([0.5 * (b["x1"] + b["x2"]), 0.5 * (b["y1"] + b["y2"])])
|
||||
|
||||
# 4) Rotation mapping cam→base (identity marker-to-wrist rotation by default)
|
||||
R_wm = _r_wrist_marker_from_config(cfg)
|
||||
R_base_cam = R_bw @ R_wm @ np.linalg.inv(R_cm)
|
||||
|
||||
# 5) Camera-frame vector & distance (marker → object)
|
||||
d_cam = centroid_cam - t_cm
|
||||
distance_m = float(np.linalg.norm(d_cam))
|
||||
done = distance_m <= float(min_distance_m)
|
||||
|
||||
# 6) Estimate T_base_camera for visualisation (identity T_wrist_marker):
|
||||
# translation part is off by the real marker-to-wrist translation, but
|
||||
# relative positions of marker and object in base frame are preserved.
|
||||
T_bc_est = np.eye(4)
|
||||
T_bc_est[:3, :3] = R_base_cam
|
||||
T_bw_full = T_from_pos_quat(wrist_pos, wrist_quat)
|
||||
T_wm_identity = np.eye(4) # translation part left at zero
|
||||
T_wm_identity[:3, :3] = R_wm
|
||||
T_cm_full = make_T(R_cm, t_cm)
|
||||
T_bc_est = T_bw_full @ T_wm_identity @ invert_T(T_cm_full)
|
||||
|
||||
marker_base_est = (T_bc_est @ np.array([*t_cm, 1.0]))[:3]
|
||||
object_base_est = (T_bc_est @ np.array([*centroid_cam, 1.0]))[:3]
|
||||
|
||||
# 7) Plan step
|
||||
target_pos = None
|
||||
target_quat = None
|
||||
planned_step_m = 0.0
|
||||
reason = None
|
||||
if done:
|
||||
reason = f"distance {distance_m*1000:.1f} mm ≤ {min_distance_m*1000:.0f} mm — reached"
|
||||
else:
|
||||
step_cam = d_cam * float(step_fraction)
|
||||
step_len = float(np.linalg.norm(step_cam))
|
||||
# Don't overshoot below min_distance_m
|
||||
if distance_m - step_len < float(min_distance_m):
|
||||
step_cam = d_cam * ((distance_m - float(min_distance_m)) / distance_m)
|
||||
step_len = float(np.linalg.norm(step_cam))
|
||||
reason = "clamped to stop at min_distance_m"
|
||||
step_base = R_base_cam @ step_cam
|
||||
target_pos = wrist_pos + step_base
|
||||
target_quat = wrist_quat_list
|
||||
planned_step_m = float(np.linalg.norm(step_base))
|
||||
|
||||
# 8) Work-plane projection (optional)
|
||||
plane_block = None
|
||||
wp = get_work_plane()
|
||||
if wp is not None:
|
||||
try:
|
||||
K_plane, _ = nuwa_intrinsics_to_matrix(nuwa.intrinsics())
|
||||
n_vec = np.asarray(wp.normal, dtype=np.float64)
|
||||
d_off = float(wp.offset)
|
||||
# Perpendicular foot of the measured 3D points.
|
||||
m_foot, m_signed = project_point_on_plane(np.asarray(t_cm), n_vec, d_off)
|
||||
o_foot, o_signed = project_point_on_plane(np.asarray(centroid_cam), n_vec, d_off)
|
||||
# Back-projected pixel hits on the plane (trust pixel + depth plane,
|
||||
# ignoring the measured Z of each point — more stable when depth is
|
||||
# noisy, at the cost of assuming the point actually lies on the
|
||||
# plane).
|
||||
m_hit = backproject_pixel_on_plane(np.asarray(marker_pixel), K_plane, n_vec, d_off)
|
||||
o_hit = backproject_pixel_on_plane(np.asarray(object_pixel), K_plane, n_vec, d_off)
|
||||
plane_block = {
|
||||
"normal": wp.normal,
|
||||
"offset": wp.offset,
|
||||
"fit_rms_mm": wp.fit_rms_mm,
|
||||
"roi": wp.roi,
|
||||
"marker_foot_cam": [float(x) for x in m_foot],
|
||||
"object_foot_cam": [float(x) for x in o_foot],
|
||||
"marker_signed_distance_m": float(m_signed),
|
||||
"object_signed_distance_m": float(o_signed),
|
||||
"marker_pixel_hit_cam": [float(x) for x in m_hit],
|
||||
"object_pixel_hit_cam": [float(x) for x in o_hit],
|
||||
"in_plane_distance_m": float(np.linalg.norm(o_foot - m_foot)),
|
||||
"in_plane_pixel_distance_m": float(np.linalg.norm(o_hit - m_hit)),
|
||||
}
|
||||
except Exception as e:
|
||||
log.warning("plane projection failed: %s", e)
|
||||
|
||||
return ServoPlan(
|
||||
wrist_position_base=[float(x) for x in wrist_pos],
|
||||
wrist_quaternion_base=wrist_quat_list,
|
||||
marker_position_cam=[float(x) for x in t_cm],
|
||||
object_position_cam=[float(x) for x in centroid_cam],
|
||||
marker_pixel=[float(x) for x in marker_pixel],
|
||||
object_pixel=[float(x) for x in object_pixel],
|
||||
marker_depth_z_m=float(abs(t_cm[2])),
|
||||
object_depth_z_m=float(abs(centroid_cam[2])),
|
||||
marker_distance_cam_m=float(np.linalg.norm(t_cm)),
|
||||
object_distance_cam_m=float(np.linalg.norm(centroid_cam)),
|
||||
marker_mm_per_px=float(abs(t_cm[2]) * 1000.0 / fx) if fx == fx and fx > 0 else 0.0,
|
||||
object_mm_per_px=float(abs(centroid_cam[2]) * 1000.0 / fx) if fx == fx and fx > 0 else 0.0,
|
||||
marker_position_base_est=[float(x) for x in marker_base_est],
|
||||
object_position_base_est=[float(x) for x in object_base_est],
|
||||
T_base_camera_est=T_bc_est.tolist(),
|
||||
distance_m=distance_m,
|
||||
done=done,
|
||||
target_position_base=[float(x) for x in target_pos] if target_pos is not None else None,
|
||||
target_quaternion_base=target_quat,
|
||||
planned_step_m=planned_step_m,
|
||||
plane=plane_block,
|
||||
detection=detection,
|
||||
n_object_points=n_pts,
|
||||
marker_reprojection_error_px=reproj_err,
|
||||
marker_translation_source=marker_t_source,
|
||||
object_point_source=object_point_source,
|
||||
reason=reason,
|
||||
)
|
||||
Reference in new issue
Block a user