feat: initial commit - massive-cloud point cloud processing system
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
"""Deflection-angle and maximum-deviation computations + GT comparison."""
|
||||
|
||||
from .deflection import (
|
||||
DeflectionMetrics,
|
||||
compute_deflection_metrics,
|
||||
)
|
||||
from .compare import compare_to_gt, GTComparison
|
||||
|
||||
__all__ = [
|
||||
"DeflectionMetrics",
|
||||
"compute_deflection_metrics",
|
||||
"compare_to_gt",
|
||||
"GTComparison",
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Compare measured deflection against the GT (CAD-derived) values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .deflection import DeflectionMetrics
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GTComparison:
|
||||
measured: DeflectionMetrics
|
||||
gt: DeflectionMetrics
|
||||
|
||||
@property
|
||||
def d_theta_right_deg(self) -> float:
|
||||
return self.measured.theta_right_deg - self.gt.theta_right_deg
|
||||
|
||||
@property
|
||||
def d_theta_ref_deg(self) -> float:
|
||||
return self.measured.theta_ref_deg - self.gt.theta_ref_deg
|
||||
|
||||
@property
|
||||
def d_D_y_max_mm(self) -> float:
|
||||
return self.measured.D_y_max_mm - self.gt.D_y_max_mm
|
||||
|
||||
@property
|
||||
def d_D_z_max_mm(self) -> float:
|
||||
return self.measured.D_z_max_mm - self.gt.D_z_max_mm
|
||||
|
||||
@property
|
||||
def d_dx_y_max_mm(self) -> float:
|
||||
return self.measured.dx_y_max_mm - self.gt.dx_y_max_mm
|
||||
|
||||
@property
|
||||
def d_dx_z_max_mm(self) -> float:
|
||||
return self.measured.dx_z_max_mm - self.gt.dx_z_max_mm
|
||||
|
||||
def to_dict(self) -> dict[str, float | dict]:
|
||||
return {
|
||||
"measured": self.measured.to_dict(),
|
||||
"gt": self.gt.to_dict(),
|
||||
"diff": {
|
||||
"theta_right_deg": self.d_theta_right_deg,
|
||||
"theta_ref_deg": self.d_theta_ref_deg,
|
||||
"D_y_max_mm": self.d_D_y_max_mm,
|
||||
"D_z_max_mm": self.d_D_z_max_mm,
|
||||
"dx_y_max_mm": self.d_dx_y_max_mm,
|
||||
"dx_z_max_mm": self.d_dx_z_max_mm,
|
||||
"abs_theta_right_deg": abs(self.d_theta_right_deg),
|
||||
"abs_theta_ref_deg": abs(self.d_theta_ref_deg),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def compare_to_gt(measured: DeflectionMetrics, gt: DeflectionMetrics) -> GTComparison:
|
||||
return GTComparison(measured=measured, gt=gt)
|
||||
|
||||
|
||||
__all__ = ["GTComparison", "compare_to_gt"]
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Deflection-angle and maximum-deviation formulas.
|
||||
|
||||
For a fitted panel-plane normal :math:`(n_x, n_y, n_z)` with
|
||||
:math:`n_x > 0`, the spec is::
|
||||
|
||||
theta_right_deg = atan2(n_y, n_x) * 180/pi (right-axis sign applied)
|
||||
theta_ref_deg = atan2(n_z, n_x) * 180/pi
|
||||
A = -n_y / n_x
|
||||
B = -n_z / n_x
|
||||
D_y_max = max |y_i - C_y|
|
||||
D_z_max = max |z_i - C_z|
|
||||
dx_y_max = |A| * D_y_max
|
||||
dx_z_max = |B| * D_z_max
|
||||
|
||||
``points`` should already be in the *world / CAD* frame, i.e. after the
|
||||
4-hole Kabsch registration, and clipped to the panel ROI so the maxima
|
||||
do not pick up edge or chamfer pixels.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeflectionMetrics:
|
||||
theta_right_deg: float
|
||||
theta_ref_deg: float
|
||||
D_y_max_mm: float
|
||||
D_z_max_mm: float
|
||||
dx_y_max_mm: float
|
||||
dx_z_max_mm: float
|
||||
A: float # -n_y / n_x
|
||||
B: float # -n_z / n_x
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"theta_right_deg": self.theta_right_deg,
|
||||
"theta_ref_deg": self.theta_ref_deg,
|
||||
"D_y_max_mm": self.D_y_max_mm,
|
||||
"D_z_max_mm": self.D_z_max_mm,
|
||||
"dx_y_max_mm": self.dx_y_max_mm,
|
||||
"dx_z_max_mm": self.dx_z_max_mm,
|
||||
"A": self.A,
|
||||
"B": self.B,
|
||||
}
|
||||
|
||||
|
||||
def compute_deflection_metrics(
|
||||
*,
|
||||
normal: tuple[float, float, float] | np.ndarray,
|
||||
center_C: tuple[float, float, float] | np.ndarray,
|
||||
panel_points: np.ndarray,
|
||||
right_axis_sign: int = 1,
|
||||
angle_clamp_deg: float = 0.0,
|
||||
) -> DeflectionMetrics:
|
||||
"""Compute the six measurands."""
|
||||
n = np.asarray(normal, dtype=np.float64).reshape(3)
|
||||
if n[0] <= 0:
|
||||
# Caller is expected to enforce n_x > 0 before this; we hard-fail
|
||||
# because every formula divides by n_x.
|
||||
raise ValueError(
|
||||
f"compute_deflection_metrics requires n_x > 0; got n = {tuple(n)}"
|
||||
)
|
||||
C = np.asarray(center_C, dtype=np.float64).reshape(3)
|
||||
pts = np.asarray(panel_points, dtype=np.float64)
|
||||
if pts.ndim != 2 or pts.shape[1] != 3:
|
||||
raise ValueError(f"panel_points must be (N,3); got {pts.shape}")
|
||||
if pts.shape[0] == 0:
|
||||
raise ValueError("panel_points is empty")
|
||||
|
||||
sign = 1.0 if int(right_axis_sign) >= 0 else -1.0
|
||||
|
||||
theta_right = sign * float(np.degrees(np.arctan2(n[1], n[0])))
|
||||
theta_ref = float(np.degrees(np.arctan2(n[2], n[0])))
|
||||
|
||||
if abs(theta_right) < float(angle_clamp_deg):
|
||||
theta_right = 0.0
|
||||
if abs(theta_ref) < float(angle_clamp_deg):
|
||||
theta_ref = 0.0
|
||||
|
||||
A = float(-n[1] / n[0])
|
||||
B = float(-n[2] / n[0])
|
||||
|
||||
D_y_max = float(np.max(np.abs(pts[:, 1] - C[1])))
|
||||
D_z_max = float(np.max(np.abs(pts[:, 2] - C[2])))
|
||||
|
||||
return DeflectionMetrics(
|
||||
theta_right_deg=theta_right,
|
||||
theta_ref_deg=theta_ref,
|
||||
D_y_max_mm=D_y_max,
|
||||
D_z_max_mm=D_z_max,
|
||||
dx_y_max_mm=abs(A) * D_y_max,
|
||||
dx_z_max_mm=abs(B) * D_z_max,
|
||||
A=A,
|
||||
B=B,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["DeflectionMetrics", "compute_deflection_metrics"]
|
||||
Reference in New Issue
Block a user