1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-29 23:33:05 +04:00

calib: expose calibrateHandEye and calibrateRobotWorldHandEye to Python

Both functions are fully implemented and documented but were tagged
CV_EXPORTS instead of CV_EXPORTS_W, so they were unreachable from
Python (and Java/JS) bindings. They use only bindable types
(InputArrayOfArrays, OutputArray, plain enums), so this is a pure
binding-exposure fix, not a behavior change.

Adds Python regression tests for both functions using synthetic
hand-eye data that satisfies the documented AX=XB (calibrateHandEye)
and AX=ZB (calibrateRobotWorldHandEye) relations, verifying the
recovered transform matches the synthetic ground truth.

Verified locally: built opencv_python3 bindings from this branch and
confirmed cv2.calibrateHandEye / cv2.calibrateRobotWorldHandEye are
now present and both new tests pass against synthetic ground-truth
poses.
This commit is contained in:
Thebinary110
2026-07-24 05:18:18 +05:30
parent eccd1c43c4
commit 325e30db1d
2 changed files with 82 additions and 2 deletions
@@ -92,5 +92,85 @@ class calibration_test(NewOpenCVTests):
self.assertTrue(isinstance(dist, (float, np.floating)))
self.assertGreaterEqual(dist, 0.0)
@staticmethod
def _random_rt():
# random rotation (axis-angle) + random translation
axis = np.random.uniform(-1, 1, 3)
axis /= np.linalg.norm(axis)
angle = np.random.uniform(0.2, 1.0) * (1 if np.random.rand() > 0.5 else -1)
rvec = (axis * angle).astype(np.float64)
R, _ = cv.Rodrigues(rvec)
t = np.random.uniform(-100, 100, 3).astype(np.float64)
return R, t
@staticmethod
def _to_T(R, t):
T = np.eye(4)
T[:3, :3] = R
T[:3, 3] = t
return T
def test_calibrateHandEye(self):
# Ground truth camera-to-gripper transform (what calibrateHandEye should recover)
R_cam2gripper_gt, t_cam2gripper_gt = self._random_rt()
X = self._to_T(R_cam2gripper_gt, t_cam2gripper_gt)
# Arbitrary fixed target-to-base transform (the calibration board doesn't move)
R_target2base, t_target2base = self._random_rt()
Wt = self._to_T(R_target2base, t_target2base)
n_poses = 10
Rs_gripper2base, ts_gripper2base = [], []
Rs_target2cam, ts_target2cam = [], []
for _ in range(n_poses):
R_g2b, t_g2b = self._random_rt()
G = self._to_T(R_g2b, t_g2b)
# Consistent synthetic data: Wt == G @ X @ T_target2cam for every pose
T_target2cam = np.linalg.inv(X) @ np.linalg.inv(G) @ Wt
Rs_gripper2base.append(R_g2b)
ts_gripper2base.append(t_g2b)
Rs_target2cam.append(T_target2cam[:3, :3])
ts_target2cam.append(T_target2cam[:3, 3])
R_cam2gripper, t_cam2gripper = cv.calibrateHandEye(
Rs_gripper2base, ts_gripper2base, Rs_target2cam, ts_target2cam)
self.assertLess(cv.norm(R_cam2gripper - R_cam2gripper_gt, cv.NORM_L1), 1e-3)
self.assertLess(cv.norm(t_cam2gripper.flatten() - t_cam2gripper_gt, cv.NORM_L1), 1e-3)
def test_calibrateRobotWorldHandEye(self):
# Ground truth outputs (what calibrateRobotWorldHandEye should recover)
R_base2world_gt, t_base2world_gt = self._random_rt()
X = self._to_T(R_base2world_gt, t_base2world_gt)
R_gripper2cam_gt, t_gripper2cam_gt = self._random_rt()
Z = self._to_T(R_gripper2cam_gt, t_gripper2cam_gt)
n_poses = 10
Rs_world2cam, ts_world2cam = [], []
Rs_base2gripper, ts_base2gripper = [], []
for _ in range(n_poses):
R_b2g, t_b2g = self._random_rt()
B = self._to_T(R_b2g, t_b2g)
# Consistent synthetic data: A_i @ X == Z @ B_i => A_i = Z @ B_i @ inv(X)
A = Z @ B @ np.linalg.inv(X)
Rs_world2cam.append(A[:3, :3])
# NOTE: unlike calibrateHandEye, calibrateRobotWorldHandEye's Shah/Li
# solvers require translations as explicit (3,1) column vectors, not
# flat (3,) arrays -- passing (3,) raises a gemm shape-assertion error.
ts_world2cam.append(A[:3, 3].reshape(3, 1))
Rs_base2gripper.append(R_b2g)
ts_base2gripper.append(t_b2g.reshape(3, 1))
R_base2world, t_base2world, R_gripper2cam, t_gripper2cam = cv.calibrateRobotWorldHandEye(
Rs_world2cam, ts_world2cam, Rs_base2gripper, ts_base2gripper)
self.assertLess(cv.norm(R_base2world - R_base2world_gt, cv.NORM_L1), 1e-3)
self.assertLess(cv.norm(t_base2world.flatten() - t_base2world_gt, cv.NORM_L1), 1e-3)
self.assertLess(cv.norm(R_gripper2cam - R_gripper2cam_gt, cv.NORM_L1), 1e-3)
self.assertLess(cv.norm(t_gripper2cam.flatten() - t_gripper2cam_gt, cv.NORM_L1), 1e-3)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()