update csv export and added fixed mesh axis drawing
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
"""Finger example pipeline: runtime acquisition without GUI dependencies."""
|
||||
|
||||
from .config import PipelineConfig, mesh_threshold
|
||||
from .csv_recorder import CSV_COLUMNS, CsvMeasurement, CsvRecorder
|
||||
from .frame import FingerFrameView
|
||||
from .service import FingerPipeline
|
||||
|
||||
__all__ = [
|
||||
"CSV_COLUMNS",
|
||||
"CsvMeasurement",
|
||||
"CsvRecorder",
|
||||
"FingerFrameView",
|
||||
"FingerPipeline",
|
||||
"PipelineConfig",
|
||||
|
||||
@@ -20,6 +20,7 @@ class PipelineConfig:
|
||||
config: str = "./config/dd02-ov.json"
|
||||
calib: str | None = None
|
||||
export_dir: str = DEFAULT_FINGER_EXPORT_DIR
|
||||
csv_save_dir: str = "./outputs/csv"
|
||||
verbose: bool = False
|
||||
mesh_map: str = "depth"
|
||||
threshold: float | None = None
|
||||
@@ -45,6 +46,7 @@ class PipelineConfig:
|
||||
lowpass_cutoff_hz=getattr(args, "cutoff", 5.0),
|
||||
profile=getattr(args, "profile", False),
|
||||
profile_every=getattr(args, "profile_every", 30),
|
||||
csv_save_dir=getattr(args, "csv_save_dir", "./outputs/csv"),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""CSV recorder for DD02 contact / force logging."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CSV_COLUMNS = (
|
||||
"frame_index",
|
||||
"timestamp_s",
|
||||
"contact_x_mm",
|
||||
"contact_y_mm",
|
||||
"contact_z_mm",
|
||||
"normal_x",
|
||||
"normal_y",
|
||||
"normal_z",
|
||||
"force_local_fn",
|
||||
"force_local_fx",
|
||||
"force_local_fy",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CsvMeasurement:
|
||||
frame_index: int
|
||||
timestamp_s: float
|
||||
contact_x: float
|
||||
contact_y: float
|
||||
contact_z: float
|
||||
normal_x: float
|
||||
normal_y: float
|
||||
normal_z: float
|
||||
force_local_fn: float
|
||||
force_local_fx: float
|
||||
force_local_fy: float
|
||||
|
||||
|
||||
def measurement_to_csv_row(row: CsvMeasurement) -> dict[str, float | int]:
|
||||
return {
|
||||
"frame_index": row.frame_index,
|
||||
"timestamp_s": row.timestamp_s,
|
||||
"contact_x_mm": row.contact_x,
|
||||
"contact_y_mm": row.contact_y,
|
||||
"contact_z_mm": row.contact_z,
|
||||
"normal_x": row.normal_x,
|
||||
"normal_y": row.normal_y,
|
||||
"normal_z": row.normal_z,
|
||||
"force_local_fn": row.force_local_fn,
|
||||
"force_local_fx": row.force_local_fx,
|
||||
"force_local_fy": row.force_local_fy,
|
||||
}
|
||||
|
||||
|
||||
class CsvRecorder:
|
||||
"""Write one contact/force measurement row per frame."""
|
||||
|
||||
def __init__(self, save_dir: str | Path = "./outputs/csv") -> None:
|
||||
self.save_dir = Path(save_dir)
|
||||
self._file = None
|
||||
self._writer: csv.DictWriter | None = None
|
||||
self._path: Path | None = None
|
||||
self._row_count = 0
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
return self._writer is not None
|
||||
|
||||
@property
|
||||
def path(self) -> Path | None:
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def row_count(self) -> int:
|
||||
return self._row_count
|
||||
|
||||
def start(self, save_dir: str | Path | None = None) -> Path:
|
||||
if self.is_recording:
|
||||
self.stop()
|
||||
|
||||
if save_dir is not None:
|
||||
self.save_dir = Path(save_dir)
|
||||
self.save_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
self._path = self.save_dir / f"dd02_record_{stamp}.csv"
|
||||
self._file = self._path.open("w", newline="", encoding="utf-8")
|
||||
self._writer = csv.DictWriter(self._file, fieldnames=CSV_COLUMNS)
|
||||
self._writer.writeheader()
|
||||
self._file.flush()
|
||||
self._row_count = 0
|
||||
return self._path
|
||||
|
||||
def write(self, row: CsvMeasurement) -> None:
|
||||
if self._writer is None:
|
||||
return
|
||||
self._writer.writerow(measurement_to_csv_row(row))
|
||||
self._row_count += 1
|
||||
if self._file is not None and self._row_count % 30 == 0:
|
||||
self._file.flush()
|
||||
|
||||
def stop(self) -> tuple[Path | None, int]:
|
||||
count = self._row_count
|
||||
saved_path = self._path
|
||||
if self._file is not None:
|
||||
self._file.flush()
|
||||
self._file.close()
|
||||
self._file = None
|
||||
self._writer = None
|
||||
self._path = None
|
||||
self._row_count = 0
|
||||
return saved_path, count
|
||||
@@ -6,11 +6,12 @@ import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
from orisys.finger import FingerRuntime
|
||||
from orisys.finger import FingerRuntime, compute_uv_surface_frames
|
||||
|
||||
from viz import FrameRecorder, build_uv_surface_mesh, resolve_viz_pose_path
|
||||
|
||||
from .config import PipelineConfig
|
||||
from .csv_recorder import CsvMeasurement
|
||||
from .frame import FingerFrameView
|
||||
from .profiling import StageProfiler, run_deformation_timed
|
||||
|
||||
@@ -40,6 +41,12 @@ class FingerPipeline:
|
||||
self.profiler: StageProfiler | None = (
|
||||
StageProfiler(report_every=config.profile_every) if config.profile else None
|
||||
)
|
||||
self._world_xyz: np.ndarray | None = None
|
||||
self._surface_mask: np.ndarray | None = None
|
||||
self._geometric_center: np.ndarray | None = None
|
||||
self._tangent_x: np.ndarray | None = None
|
||||
self._tangent_y: np.ndarray | None = None
|
||||
self._normals: np.ndarray | None = None
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
@@ -72,10 +79,26 @@ class FingerPipeline:
|
||||
if domain.surface is None or getattr(domain.surface, "world_xyz", None) is None:
|
||||
return
|
||||
self.uv_mesh = build_uv_surface_mesh(domain.surface.world_xyz, domain.valid_mask)
|
||||
self._cache_surface_geometry(domain.surface.world_xyz, domain.valid_mask)
|
||||
if self.config.viz_pose.lower() != "none":
|
||||
self.world_to_cam = np.load(resolve_viz_pose_path(self.config.viz_pose))
|
||||
self.mesh_lookat = self.uv_mesh.rest_vertices.mean(axis=0)
|
||||
|
||||
def _cache_surface_geometry(self, world_xyz: np.ndarray, valid_mask: np.ndarray) -> None:
|
||||
world_xyz = np.asarray(world_xyz, dtype=np.float64)
|
||||
valid_mask = np.asarray(valid_mask, dtype=bool)
|
||||
finite = np.all(np.isfinite(world_xyz), axis=2)
|
||||
mask = valid_mask & finite
|
||||
self._world_xyz = world_xyz
|
||||
self._surface_mask = mask
|
||||
points = world_xyz[mask]
|
||||
self._geometric_center = (
|
||||
points.mean(axis=0) if len(points) else np.zeros(3, dtype=np.float64)
|
||||
)
|
||||
self._tangent_x, self._tangent_y, self._normals = compute_uv_surface_frames(
|
||||
world_xyz, mask
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
if self.profiler is not None and self.profiler.frame_count:
|
||||
self.profiler.print_report(prefix="[profile final]")
|
||||
@@ -83,6 +106,12 @@ class FingerPipeline:
|
||||
self.runtime.close()
|
||||
self.runtime = None
|
||||
self._start_time = None
|
||||
self._world_xyz = None
|
||||
self._surface_mask = None
|
||||
self._geometric_center = None
|
||||
self._tangent_x = None
|
||||
self._tangent_y = None
|
||||
self._normals = None
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.runtime is not None:
|
||||
@@ -165,3 +194,53 @@ class FingerPipeline:
|
||||
|
||||
def stop_recording(self) -> None:
|
||||
self.recorder.stop()
|
||||
|
||||
def measure_for_csv(self, frame: FingerFrameView, frame_index: int) -> CsvMeasurement:
|
||||
"""Lift contact + local forces into global-frame measurements for CSV export."""
|
||||
nan = float("nan")
|
||||
contact = (nan, nan, nan)
|
||||
normal = (nan, nan, nan)
|
||||
|
||||
fn = float(frame.fnormal)
|
||||
fx = float(frame.fshearx)
|
||||
fy = float(frame.fsheary)
|
||||
|
||||
if (
|
||||
frame.centroid_found
|
||||
and frame.centroid is not None
|
||||
and np.asarray(frame.centroid).size >= 2
|
||||
and self._world_xyz is not None
|
||||
and self._geometric_center is not None
|
||||
and self._normals is not None
|
||||
and self._tangent_x is not None
|
||||
and self._tangent_y is not None
|
||||
):
|
||||
cx = int(round(float(np.asarray(frame.centroid).reshape(-1)[0])))
|
||||
cy = int(round(float(np.asarray(frame.centroid).reshape(-1)[1])))
|
||||
h, w = self._world_xyz.shape[:2]
|
||||
if 0 <= cy < h and 0 <= cx < w:
|
||||
if self._surface_mask is None or bool(self._surface_mask[cy, cx]):
|
||||
world = self._world_xyz[cy, cx]
|
||||
if np.all(np.isfinite(world)):
|
||||
centered = world - self._geometric_center
|
||||
contact = (float(centered[0]), float(centered[1]), float(centered[2]))
|
||||
|
||||
n = self._normals[cy, cx]
|
||||
n_norm = float(np.linalg.norm(n))
|
||||
if n_norm > 1e-8:
|
||||
n_u = n / n_norm
|
||||
normal = (float(n_u[0]), float(n_u[1]), float(n_u[2]))
|
||||
|
||||
return CsvMeasurement(
|
||||
frame_index=int(frame_index),
|
||||
timestamp_s=float(frame.timestamp),
|
||||
contact_x=contact[0],
|
||||
contact_y=contact[1],
|
||||
contact_z=contact[2],
|
||||
normal_x=normal[0],
|
||||
normal_y=normal[1],
|
||||
normal_z=normal[2],
|
||||
force_local_fn=fn,
|
||||
force_local_fx=fx,
|
||||
force_local_fy=fy,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user