1
0
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:
Skreg
2025-08-18 21:58:08 +05:30
committed by GitHub
parent defc988c0d
commit 86b9885c90
+54 -29
View File
@@ -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