From 06c050e446052587190d40c6831d1ef7c2b20885 Mon Sep 17 00:00:00 2001 From: lxp <2770281812@qq.com> Date: Mon, 31 Aug 2026 17:58:11 +0800 Subject: [PATCH] test: restore calibration regression baseline --- .../test/test_three_camera_retry.py | 131 ++++++++++-------- 1 file changed, 71 insertions(+), 60 deletions(-) diff --git a/src/g20_thumb_apriltag_calibration/test/test_three_camera_retry.py b/src/g20_thumb_apriltag_calibration/test/test_three_camera_retry.py index c16ce92..ad809f5 100644 --- a/src/g20_thumb_apriltag_calibration/test/test_three_camera_retry.py +++ b/src/g20_thumb_apriltag_calibration/test/test_three_camera_retry.py @@ -60,8 +60,7 @@ from g20_thumb_apriltag_calibration.pnp import SquareTagPose from g20_thumb_apriltag_calibration.urdf_zero import ( RIGHT_19_MECHANICAL_ENDPOINT_JOINTS, JointAxisMeasurement, - axis_line_cycle_rms_m, - cross_view_side_line_source, + axis_line_uses_depth_free_interpretation_plane, get_zero_calibration_profile, ) @@ -142,6 +141,7 @@ def test_finalize_publishes_the_frozen_validated_endpoint_state( ) node = SimpleNamespace( zero_result=zero_result, + standalone_thumb_calibration=False, validated_endpoint_zero_offsets_rad=dict(endpoint_offsets), validation_errors_rad=[], combination_validation_enabled=False, @@ -4079,6 +4079,20 @@ def _cross_view_roll_node( "pinky_mcp_roll_side": side, } node.raw_path = tmp_path / "raw_samples.jsonl" + # The production node always owns validated camera extrinsics. Keep the + # synthetic camera centre away from the synthetic axis origin so the + # interpretation-plane geometry is well defined as it is on real data. + node.extrinsics = SimpleNamespace( + transform=lambda _view: np.asarray( + [ + [1.0, 0.0, 0.0, 0.1], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=float, + ) + ) return node @@ -4092,8 +4106,12 @@ def test_cross_view_roll_axis_disagreement_skips_fusion(tmp_path) -> None: # and record a diagnostic instead of failing the joint. assert measurement.axis_direction_source != "cross_view_weighted_fusion" assert measurement.pose_axis_line_source_joints == ( - "pinky_mcp_roll_side", + "pinky_mcp_roll", ) + assert measurement.axis_point_source == ( + "front_interpretation_plane_cross_view_validated" + ) + assert axis_line_uses_depth_free_interpretation_plane(measurement) diagnostics = [ json.loads(line) for line in node.raw_path.read_text().splitlines() @@ -4105,7 +4123,7 @@ def test_cross_view_roll_axis_disagreement_skips_fusion(tmp_path) -> None: assert 10.0 < diagnostics[-1]["axis_difference_deg"] < 12.5 -def test_cross_view_axis_group_uses_one_physical_radius(tmp_path) -> None: +def test_cross_view_depth_free_axis_group_is_not_radius_refitted(tmp_path) -> None: node = _cross_view_roll_node(tmp_path, side_tilt_deg=11.0) measurements = [ node._fit_axis_measurement("pinky_mcp_roll", cycle) @@ -4116,20 +4134,14 @@ def test_cross_view_axis_group_uses_one_physical_radius(tmp_path) -> None: node, "pinky_mcp_roll", measurements ) + assert refined == measurements assert all( - measurement.axis_point_source - == "side_circle_shared_radius_cross_view" + axis_line_uses_depth_free_interpretation_plane(measurement) for measurement in refined ) - assert all( - cross_view_side_line_source(measurement) - == "pinky_mcp_roll_side" - for measurement in refined - ) - assert axis_line_cycle_rms_m(refined) < 1.0e-6 -def test_cross_view_side_line_failure_rescans_only_side_alias( +def test_cross_view_depth_free_axis_skips_point_repeatability_rescan( tmp_path, monkeypatch ) -> None: node = _cross_view_roll_node(tmp_path, side_tilt_deg=11.0) @@ -4147,18 +4159,14 @@ def test_cross_view_side_line_failure_rescans_only_side_alias( return measurement node._fit_axis_measurement = displaced_side_line - # This test isolates the downstream repeatability gate. The production - # shared-radius refit legitimately removes an artificial point-only - # displacement that is absent from the underlying trajectory records. - monkeypatch.setattr( - G20ThreeCameraCalibrationNode, - "_refit_cross_view_axis_line_group", - lambda _self, _name, measurements: list(measurements), - ) + # A depth-free interpretation plane does not define one Euclidean line + # point, so point-only cycle RMS must not trigger a misleading rescan. monkeypatch.setitem( G20ThreeCameraCalibrationNode._provisional_fit_failures.__globals__, "_isolated_axis_line_cycle_outliers", - lambda measurements, limit_m: {3}, + lambda measurements, limit_m: pytest.fail( + "depth-free axes must not enter the line-point outlier gate" + ), ) spec = next( item @@ -4175,26 +4183,10 @@ def test_cross_view_side_line_failure_rescans_only_side_alias( if failure["metric"] == "axis_line_cycle_rms_mm" ] - assert line_failures - assert { - failure["joint"] for failure in line_failures - } == {"pinky_mcp_roll_side"} - assert all( - failure["model_joint"] == "pinky_mcp_roll" - and failure["quality_source_joints"] == ["pinky_mcp_roll_side"] - for failure in line_failures - ) - assert all(failure["cycle"] == 4 for failure in line_failures) - assert all( - failure["inlier_cycles"] == [1, 2, 3] - for failure in line_failures - ) - assert _fit_retry_joint_names( - RIGHT_19_HAND_PROFILE, spec, line_failures - ) == {"pinky_mcp_roll_side"} + assert not line_failures -def test_cross_view_side_pose_residual_uses_line_repeatability_gate( +def test_cross_view_front_pose_residual_remains_a_hard_gate( tmp_path, ) -> None: node = _cross_view_roll_node(tmp_path, side_tilt_deg=11.0) @@ -4217,11 +4209,21 @@ def test_cross_view_side_pose_residual_uses_line_repeatability_gate( node, spec, include_view_validity=False ) + pose_failures = [ + failure + for failure in failures + if failure["metric"] == "axis_pose_line_rms_mm" + ] + assert pose_failures + assert all( + failure["joint"] == "pinky_mcp_roll" + and failure["quality_source_joints"] == ["pinky_mcp_roll"] + for failure in pose_failures + ) assert not [ failure for failure in failures - if failure["metric"] - in {"axis_pose_line_rms_mm", "axis_line_cycle_rms_mm"} + if failure["metric"] == "axis_line_cycle_rms_mm" ] @@ -4316,7 +4318,10 @@ def test_cross_view_roll_axis_agreement_keeps_published_primary(tmp_path) -> Non assert measurement.axis_direction_source == "rotation" assert measurement.pose_axis_line_source_joints == ( - "pinky_mcp_roll_side", + "pinky_mcp_roll", + ) + assert measurement.axis_point_source == ( + "front_interpretation_plane_cross_view_validated" ) @@ -4348,6 +4353,17 @@ def test_cross_view_roll_side_pose_never_replaces_primary_direction( zero_maximum_axis_cone_mismatch_rad=math.radians(5.0), axis_maximum_pose_line_rms_m=0.001, cross_view_roll_maximum_axis_difference_rad=math.radians(15.0), + extrinsics=SimpleNamespace( + transform=lambda _view: np.asarray( + [ + [1.0, 0.0, 0.0, 0.1], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=float, + ) + ), ) primary_degrees = (8.0078, 8.1710, 8.0712, 8.0236) secondary_degrees = (4.9043, 4.9136, 5.0374, 4.9389) @@ -5398,10 +5414,9 @@ def test_live_task_still_uses_window_when_not_resumed() -> None: ] -def test_cross_view_skip_keeps_front_direction_with_side_line(tmp_path) -> None: - # The front roll-link centre trajectory carries the ~21 mm screw - # translation, so on a fusion skip the axis LINE must come from the - # side PIP-link circle while the direction stays front-only. +def test_cross_view_skip_keeps_front_depth_free_geometry(tmp_path) -> None: + # The side PIP-link pose validates the observation, but its planar-pose + # depth and orientation bias must not replace the trusted front geometry. node = _cross_view_roll_node( tmp_path, side_tilt_deg=11.0, @@ -5411,23 +5426,19 @@ def test_cross_view_skip_keeps_front_direction_with_side_line(tmp_path) -> None: measurement = node._fit_axis_measurement("pinky_mcp_roll", 0) assert measurement.axis_direction_source != "cross_view_weighted_fusion" - assert measurement.axis_point_source == "side_circle_cross_view" - side_point = np.asarray( - node.records_by_joint["pinky_mcp_roll_side"][0][ - "parent_pose_common" - ]["translation_xyz_m"], - dtype=float, - ) - measured_point = np.asarray( - measurement.point_common_xyz_m, dtype=float + assert measurement.axis_point_source == ( + "front_interpretation_plane_cross_view_validated" ) + assert measurement.pose_axis_line_source_joints == ("pinky_mcp_roll",) + assert axis_line_uses_depth_free_interpretation_plane(measurement) front_direction = np.asarray( measurement.axis_common_xyz, dtype=float ) - # The published line keeps the front direction and takes the side - # view's line position (offset ~21 mm by construction), instead of the - # front link's own displaced line. assert float(np.dot(front_direction, [0.0, 0.0, 1.0])) > math.cos( math.radians(2.0) ) - assert 0.015 < float(measured_point[0]) < 0.027 + assert measurement.axis_point_camera_center_common_xyz_m is not None + assert ( + measurement.axis_point_interpretation_plane_normal_common_xyz + is not None + )