mirror of
https://github.com/opencv/opencv.git
synced 2026-07-29 15:23:05 +04:00
Merge pull request #27651 from shyama7004:minor-fixes
Fix empty intrinsics handling and minor calibration improvements #27651 Closes: #27650 Minor changes: * Filter out `""` in `files_with_intrinsics`. * Match the list length with the number of cameras. * Fix unpacking from `cv.calibrateMultiviewExtended`. * Adjust visualisation to ensure correct shapes. * Save `cam_ids` as a list in YAML. * Removed typos. <details> <summary>Pytest</summary> ```py import json import numpy as np import importlib.util import sys import pytest spec = importlib.util.spec_from_file_location("mv", "multiview_calibration.py") mv = importlib.util.module_from_spec(spec) sys.modules["mv"] = mv spec.loader.exec_module(mv) def test_insideImageMask_boundaries(): # w = 10, h = 10, valid x,y: [0..9] pts = np.array([[0, 5, 9, -1, 10], [0, 5, 9, 5, 5]], dtype=np.int32) m = mv.insideImageMask(pts, 10, 10) assert m.tolist() == [True, True, True, False, False] def test_intrinsics_empty_ignored_and_forwarded(tmp_path, monkeypatch): cam1 = tmp_path / "cam1.txt" cam2 = tmp_path / "cam2.txt" cam1.write_text("\n") cam2.write_text("\n") opened = [] class FakeNode: def __init__(self, name): self.name = name def mat(self): if self.name in ("camera_matrix", "cameraMatrix"): return np.eye(3, dtype=np.float32) if self.name in ("dist_coeffs", "distortion_coefficients"): return np.zeros((1, 5), dtype=np.float32) return None class FakeFS: def __init__(self, filename, flags=None): opened.append(filename) def getNode(self, key): return FakeNode(key) monkeypatch.setattr(mv.cv, "FileStorage", FakeFS) captured = {} def fake_calibrateFromPoints(pattern_points, image_points, image_sizes, models, image_names, find_intrinsics_in_python, Ks=None, distortions=None): captured["Ks"] = Ks captured["dist"] = distortions return dict( Rs=[np.zeros((3, 1)) for _ in models], Ts=[np.zeros((3, 1)) for _ in models], Ks=Ks, distortions=distortions, rvecs0=[np.zeros((3, 1))], tvecs0=[np.zeros((3, 1))], errors_per_frame=np.ones((len(models), 1), dtype=np.float32), output_pairs=[], image_points=image_points, models=models, image_sizes=image_sizes, pattern_points=pattern_points, detection_mask=np.ones((len(models), 1), np.uint8), image_names=image_names, ) monkeypatch.setattr(mv, "calibrateFromPoints", fake_calibrateFromPoints) out = mv.calibrateFromImages( files_with_images=[str(cam1), str(cam2)], grid_size=[3, 2], pattern_type="checkerboard", models=[0, 0], dist_m=0.1, winsize=(5, 5), points_json_file="", debug_corners=False, RESIZE_IMAGE=False, # <- match real signature name here find_intrinsics_in_python=False, is_parallel_detection=False, cam_ids=["1", "2"], files_with_intrinsics=["", str(tmp_path / "intr.yml")], board_dict_path=None, ) assert "" not in opened assert captured["Ks"] is not None and len(captured["Ks"]) == 2 assert captured["dist"] is not None and len(captured["dist"]) == 2 def test_visualize_converts_rvec_to_R_and_t_to_col(monkeypatch): called = {} def fake_plotCamerasPosition(Rs, Ts, image_sizes, pairs, pattern, frame_idx, cam_ids, detection_mask): called["R_shapes"] = [R.shape for R in Rs] called["T_shapes"] = [T.shape for T in Ts] def fake_plotProjection(points_2d, pattern_points, rvec0, tvec0, rvec1, tvec1, K, dist_coeff, model, *rest): called["tvec1_shape"] = tvec1.shape called["K_shape"] = K.shape monkeypatch.setattr(mv.plt, "savefig", lambda *a, **k: None) monkeypatch.setattr(mv.plt, "close", lambda *a, **k: None) monkeypatch.setattr(mv, "plotCamerasPosition", fake_plotCamerasPosition) monkeypatch.setattr(mv, "plotProjection", fake_plotProjection) detection_mask = np.ones((1, 1), np.uint8) Rs = [np.zeros((3, 1), dtype=np.float32)] # rvec Ts = [np.array([[0., 0., 0.]], dtype=np.float32)] # row 1x3 Ks = [np.eye(3, dtype=np.float32)] distortions = [np.zeros((1, 5), dtype=np.float32)] models = [0] image_points = [[np.array([[10., 10.]], dtype=np.float32)]] errors_per_frame = np.array([[1.0]], dtype=np.float32) rvecs0 = [np.zeros((3, 1), dtype=np.float32)] tvecs0 = [np.zeros((3, 1), dtype=np.float32)] pattern_points = np.array([[0., 0., 0.]], dtype=np.float32) image_sizes = [(100, 100)] output_pairs = [] image_names = None cam_ids = ["0"] mv.visualizeResults( detection_mask, Rs, Ts, Ks, distortions, models, image_points, errors_per_frame, rvecs0, tvecs0, pattern_points, image_sizes, output_pairs, image_names, cam_ids ) assert called["R_shapes"] == [(3, 3)] assert called["T_shapes"] == [(3, 1)] assert called["tvec1_shape"] == (3, 1) assert called["K_shape"] == (3, 3) ``` </details> ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
@@ -18,9 +18,10 @@ import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import yaml
|
||||
import math
|
||||
import warnings
|
||||
|
||||
def insideImageMask(pts, w, h):
|
||||
return np.logical_and(np.logical_and(pts[0] < w, pts[1] < h), np.logical_and(pts[0] > 0, pts[1] > 0))
|
||||
return (pts[0] >= 0) & (pts[0] <= w - 1) & (pts[1] >= 0) & (pts[1] <= h - 1)
|
||||
|
||||
def read_gt_rig(file, num_cameras, num_frames):
|
||||
Ks_gt = []
|
||||
@@ -236,7 +237,7 @@ def plotDetection(image_sizes, image_points):
|
||||
|
||||
# [plot_detection]
|
||||
|
||||
def showUndistorted(image_points, Ks, distortions, image_names):
|
||||
def showUndistorted(image_points, Ks, distortions, image_names, cam_ids):
|
||||
detection_mask = getDetectionMask(image_points)
|
||||
for cam in range(len(image_points)):
|
||||
detected_imgs = np.where(detection_mask[cam])[0]
|
||||
@@ -422,7 +423,7 @@ def calibrateFromPoints(
|
||||
start_time = time.time()
|
||||
# try:
|
||||
# [multiview_calib]
|
||||
rmse, Rs, Ts, Ks, distortions, rvecs0, tvecs0, errors_per_frame, output_pairs = \
|
||||
rmse, Ks, distortions, Rs, Ts, output_pairs, rvecs0, tvecs0, errors_per_frame = \
|
||||
cv.calibrateMultiviewExtended(
|
||||
objPoints=pattern_points_all,
|
||||
imagePoints=image_points,
|
||||
@@ -472,7 +473,10 @@ def calibrateFromPoints(
|
||||
def visualizeResults(detection_mask, Rs, Ts, Ks, distortions, models,
|
||||
image_points, errors_per_frame, rvecs0, tvecs0,
|
||||
pattern_points, image_sizes, output_pairs, image_names, cam_ids):
|
||||
rvecs = [cv.Rodrigues(R)[0] for R in Rs]
|
||||
def _as_rvec(x):
|
||||
x = np.asarray(x)
|
||||
return cv.Rodrigues(x)[0] if x.shape == (3, 3) else x.reshape(3, 1)
|
||||
rvecs = [_as_rvec(R) for R in Rs]
|
||||
errors = errors_per_frame[errors_per_frame > 0]
|
||||
detection_mask_idxs = np.stack(np.where(detection_mask)) # 2 x M, first row is camera idx, second is frame idx
|
||||
|
||||
@@ -485,7 +489,9 @@ def visualizeResults(detection_mask, Rs, Ts, Ks, distortions, models,
|
||||
|
||||
R_frame = cv.Rodrigues(rvecs0[frame_idx])[0]
|
||||
pattern_frame = (R_frame @ pattern_points.T + tvecs0[frame_idx]).T
|
||||
plotCamerasPosition(Rs, Ts, image_sizes, output_pairs, pattern_frame, frame_idx, cam_ids, detection_mask)
|
||||
R_mats = [cv.Rodrigues(rv)[0] for rv in rvecs] # 3x3 each
|
||||
T_cols = [np.asarray(t).reshape(3,1) for t in Ts] # 3x1 each
|
||||
plotCamerasPosition(R_mats, T_cols, image_sizes, output_pairs, pattern_frame, frame_idx, cam_ids, detection_mask)
|
||||
|
||||
save_file = 'cam_poses.png'
|
||||
print('Saving:', save_file)
|
||||
@@ -500,13 +506,14 @@ def visualizeResults(detection_mask, Rs, Ts, Ks, distortions, models,
|
||||
image = cv.cvtColor(cv.imread(image_names[cam_idx][frame_idx]), cv.COLOR_BGR2RGB)
|
||||
mask = insideImageMask(image_points[cam_idx][frame_idx].T,
|
||||
image_sizes[cam_idx][0], image_sizes[cam_idx][1])
|
||||
tvec_cam = np.asarray(Ts[cam_idx]).reshape(3,1)
|
||||
plotProjection(
|
||||
image_points[cam_idx][frame_idx][mask],
|
||||
pattern_points[mask],
|
||||
rvecs0[frame_idx],
|
||||
tvecs0[frame_idx],
|
||||
rvecs[cam_idx],
|
||||
Ts[cam_idx],
|
||||
tvec_cam,
|
||||
Ks[cam_idx],
|
||||
distortions[cam_idx],
|
||||
models[cam_idx],
|
||||
@@ -517,7 +524,7 @@ def visualizeResults(detection_mask, Rs, Ts, Ks, distortions, models,
|
||||
)
|
||||
|
||||
plot(detection_mask_idxs[0, pos], detection_mask_idxs[1, pos])
|
||||
showUndistorted(image_points, Ks, distortions, image_names)
|
||||
showUndistorted(image_points, Ks, distortions, image_names, cam_ids)
|
||||
# plt.show()
|
||||
plotDetection(image_sizes, image_points)
|
||||
|
||||
@@ -561,7 +568,7 @@ def saveToFile(path_to_save, **kwargs):
|
||||
if key == 'image_names':
|
||||
save_file.write('image_names', list(np.array(kwargs['image_names']).reshape(-1)))
|
||||
elif key == 'cam_ids':
|
||||
save_file.write('cam_ids', ','.join(cam_ids))
|
||||
save_file.write('cam_ids', list(kwargs['cam_ids']))
|
||||
elif key == 'distortions':
|
||||
value = kwargs[key]
|
||||
save_file.write('distortions', np.concatenate([x.reshape([-1,]) for x in value],axis=0))
|
||||
@@ -641,13 +648,13 @@ def compareGT(gt_file, detection_mask, Rs, Ts, Ks, distortions, models,
|
||||
err_dist_max[cam] = np.max(errs_pt)
|
||||
err_dist_median[cam] = np.median(errs_pt)
|
||||
|
||||
print("Distrotion error (mean, median):\n", " ".join([f'(%.4f, %.4f)' % (err_dist_mean[i], err_dist_median[i]) for i in range(len(cam_ids))]))
|
||||
print("Distortion error (mean, median):\n", " ".join([f'(%.4f, %.4f)' % (err_dist_mean[i], err_dist_median[i]) for i in range(len(cam_ids))]))
|
||||
print("Extrinsics error (R, C):\n", " ".join([f'(%.4f, %.4f)' % (err_r[i], err_c[i]) for i in range(len(cam_ids))]))
|
||||
print("Rotation error (mean, median):", f'(%.4f, %.4f)' % (np.mean(err_r), np.median(err_r)))
|
||||
print("Position error (mean, median):", f'(%.4f, %.4f)' % (np.mean(err_c), np.median(err_c)))
|
||||
|
||||
if len(rvecs0_gt) > 0:
|
||||
# conver all things with respect to the first frame
|
||||
# convert all things with respect to the first frame
|
||||
R0 = []
|
||||
for frame in range(0, len(rvecs0_gt)):
|
||||
if rvecs0[frame] is not None:
|
||||
@@ -895,21 +902,39 @@ def calibrateFromImages(files_with_images, grid_size, pattern_type, models,
|
||||
Ks = None
|
||||
distortions = None
|
||||
if files_with_intrinsics:
|
||||
# Read camera instrinsic matrices (Ks) and dictortions
|
||||
Ks, distortions = [], []
|
||||
for cam_idx, filename in enumerate(files_with_intrinsics):
|
||||
print("Reading intrinsics from", filename)
|
||||
storage = cv.FileStorage(filename, cv.FileStorage_READ)
|
||||
# tries to read output of interactive-calibration tool (yaml) and camera calibration tutorial (xml)
|
||||
camera_matrix = storage.getNode('cameraMatrix').mat()
|
||||
if camera_matrix is None:
|
||||
camera_matrix = storage.getNode('camera_matrix').mat()
|
||||
dist_coeffs = storage.getNode('dist_coeffs').mat()
|
||||
if dist_coeffs is None:
|
||||
dist_coeffs = storage.getNode('distortion_coefficients').mat()
|
||||
Ks.append(camera_matrix)
|
||||
distortions.append(dist_coeffs)
|
||||
find_intrinsics_in_python = True
|
||||
# Read camera intrinsic matrices (Ks) and distortions
|
||||
files_with_intrinsics = [
|
||||
f for f in files_with_intrinsics if isinstance(f, str) and f.strip()
|
||||
]
|
||||
|
||||
if files_with_intrinsics:
|
||||
Ks, distortions = [], []
|
||||
for filename in files_with_intrinsics:
|
||||
print("Reading intrinsics from", filename)
|
||||
storage = cv.FileStorage(filename, cv.FileStorage_READ)
|
||||
camera_matrix = storage.getNode('cameraMatrix').mat()
|
||||
if camera_matrix is None:
|
||||
camera_matrix = storage.getNode('camera_matrix').mat()
|
||||
dist_coeffs = storage.getNode('dist_coeffs').mat()
|
||||
if dist_coeffs is None:
|
||||
dist_coeffs = storage.getNode('distortion_coefficients').mat()
|
||||
Ks.append(camera_matrix)
|
||||
distortions.append(dist_coeffs)
|
||||
|
||||
num_cams = len(files_with_images)
|
||||
if len(Ks) == 1 and num_cams > 1:
|
||||
warnings.warn(f"single intrinsics/distortion provided, applying same to all {num_cams} cameras.")
|
||||
Ks = [Ks[0] for _ in range(num_cams)]
|
||||
distortions = [distortions[0] for _ in range(num_cams)]
|
||||
elif len(Ks) < num_cams:
|
||||
warnings.warn(f"only {len(Ks)} intrinsics provided, repeating last to reach {num_cams}.")
|
||||
Ks += [Ks[-1]] * (num_cams - len(Ks))
|
||||
distortions += [distortions[-1]] * (num_cams - len(distortions))
|
||||
elif len(Ks) > num_cams:
|
||||
warnings.warn(f"{len(Ks)} intrinsics provided, truncating to first {num_cams}.")
|
||||
Ks = Ks[:num_cams]
|
||||
distortions = distortions[:num_cams]
|
||||
find_intrinsics_in_python = True
|
||||
|
||||
return calibrateFromPoints(
|
||||
pattern,
|
||||
@@ -936,7 +961,7 @@ def calibrateFromJSON(json_file, find_intrinsics_in_python):
|
||||
images_names = data['images_names'] if 'images_names' in data else None
|
||||
|
||||
return calibrateFromPoints(
|
||||
np.array(data['object_points'], dtype=np.float32).T,
|
||||
np.array(data['object_points'], dtype=np.float32),
|
||||
data['image_points'],
|
||||
data['image_sizes'],
|
||||
data['models'],
|
||||
@@ -950,7 +975,7 @@ def calibrateFromJSON(json_file, find_intrinsics_in_python):
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--json_file', type=str, default=None, help="json file with all data. Must have keys: 'object_points', 'image_points', 'image_sizes', 'is_fisheye'")
|
||||
parser.add_argument('--filenames', type=str, default=None, help='Txt files containg image lists, e.g., cam_1.txt,cam_2.txt,...,cam_N.txt for N cameras')
|
||||
parser.add_argument('--filenames', type=str, default=None, help='Txt files containing image lists, e.g., cam_1.txt,cam_2.txt,...,cam_N.txt for N cameras')
|
||||
parser.add_argument('--pattern_size', type=str, default=None, help='pattern size: width,height')
|
||||
parser.add_argument('--pattern_type', type=str, default=None, help='supported: checkerboard, circles, acircles, charuco')
|
||||
parser.add_argument('--is_fisheye', type=str, default=None, help='is_ mask, e.g., 0,1,...')
|
||||
@@ -963,7 +988,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('--path_to_visualize', type=str, default='', help='path to results pickle file needed to run visualization')
|
||||
parser.add_argument('--visualize', required=False, action='store_true', help='visualization flag. If set, only runs visualization but path_to_visualize must be provided')
|
||||
parser.add_argument('--resize_image_detection', required=False, action='store_true', help='If set, an image will be resized to speed-up corners detection')
|
||||
parser.add_argument('--intrinsics', type=str, default='', help='YAML or XML files with each camera intrinsics')
|
||||
parser.add_argument('--intrinsics', type=str, default=None, help='YAML or XML files with each camera intrinsics')
|
||||
parser.add_argument('--gt_file', type=str, default=None, help="ground truth")
|
||||
parser.add_argument('--board_dict_path', type=str, default=None, help="path to parameters of board dictionary")
|
||||
|
||||
@@ -1006,7 +1031,7 @@ if __name__ == '__main__':
|
||||
RESIZE_IMAGE=params.resize_image_detection,
|
||||
find_intrinsics_in_python=params.find_intrinsics_in_python,
|
||||
cam_ids=cam_ids,
|
||||
files_with_intrinsics=[x.strip() for x in params.intrinsics.split(',')],
|
||||
files_with_intrinsics=[x.strip() for x in (params.intrinsics or "").split(',') if x.strip()],
|
||||
board_dict_path=params.board_dict_path,
|
||||
)
|
||||
output['cam_ids'] = cam_ids
|
||||
|
||||
Reference in New Issue
Block a user