#!/usr/bin/env python3
import argparse
import cv2
import json
import math
import numpy as np
import os
def _object(value, label):
if not isinstance(value, dict):
raise ValueError(f"{label} must be a JSON object")
return value
def _number(value, label):
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{label} must be a finite number")
try:
value = float(value)
except OverflowError as error:
raise ValueError(f"{label} must be a finite number") from error
if not math.isfinite(value):
raise ValueError(f"{label} must be a finite number")
return value
def _vector(value, label):
if not isinstance(value, list) or len(value) != 3:
raise ValueError(f"{label} must contain exactly three numbers")
return [_number(item, label) for item in value]
def readCalibParameters(filePath):
"""Return K, distortion, rotation and translation lists for OpenCV cameras.
Read a current libCalibGeom calibration export, not a CalibGeom .calib project.
Rotation/translation vectors retain the existing three-element list shape.
"""
if os.path.splitext(os.fspath(filePath))[1].lower() == ".calib":
raise ValueError("Expected exported calibration JSON, not a .calib project")
with open(filePath, encoding="utf-8") as file:
j = _object(json.load(file), "Calibration")
if "Calibration" in j or "format" in j or "initialization" in j:
raise ValueError("Expected current calibration JSON; legacy archives and "
"CalibGeom projects are not supported. Export Parameters first.")
cameras = j.get("cameras")
if not isinstance(cameras, list) or not cameras:
raise ValueError("Calibration must contain a nonempty cameras array")
K = []
k = []
cam_rvecs = []
cam_tvecs = []
distortion_names = ("k1", "k2", "p1", "p2", "k3", "k4", "k5", "k6",
"s1", "s2", "s3", "s4", "tauX", "tauY")
names = ("f", "ar", "cx", "cy") + distortion_names
for i, camera in enumerate(cameras):
label = f"Camera {i}"
try:
camera = _object(camera, label)
model = _object(camera["model"], f"{label} model")
if model["type"] != "OpenCV":
raise ValueError(f"{label} must use the OpenCV model")
image_size = _object(model["imageSize"], f"{label} imageSize")
for dimension in ("width", "height"):
size = _number(image_size[dimension], f"{label} {dimension}")
if size <= 0 or not size.is_integer():
raise ValueError(f"{label} {dimension} must be a positive integer")
parameters = _object(model["parameters"], f"{label} parameters")
if set(parameters) != set(names):
raise ValueError(f"{label} must contain all 18 OpenCV parameters")
values = {}
for name in names:
item = _object(parameters[name], f"{label} parameter {name}")
if item["state"] not in ("free", "fixed", "disabled"):
raise ValueError(f"{label} parameter {name} has an unknown state")
# Projection uses stored values, even when fixed or disabled.
values[name] = _number(item["value"], f"{label} parameter {name}")
f, ar = values["f"], values["ar"]
fy = _number(f * ar, f"{label} vertical focal length")
if f <= 0 or ar <= 0 or fy <= 0:
raise ValueError(f"{label} focal length and aspect ratio must be positive")
K.append(np.array([[f, 0.0, values["cx"]],
[0.0, fy, values["cy"]], [0.0, 0.0, 1.0]]))
k.append(np.array([values[name] for name in distortion_names]))
transform = _object(camera["extrinsics"], f"{label} extrinsics")
rot = _object(transform["rotation"], f"{label} rotation")
translation = _object(transform["translation"], f"{label} translation")
cam_rvecs.append(_vector(rot["rvec"], f"{label} rotation.rvec"))
cam_tvecs.append(_vector(translation["vals"], f"{label} translation.vals"))
except KeyError as error:
raise ValueError(f"{label} is missing required field {error.args[0]}") from error
return K, k, cam_rvecs, cam_tvecs
def main():
parser = argparse.ArgumentParser(description="Read libCalibGeom OpenCV-compatible parameters from a calibration JSON file.")
parser.add_argument("json_file", help="Path to the calibration JSON file.")
parser.add_argument("image_path", help="Path to the image to undistort.")
args = parser.parse_args()
try:
K, k, cam_rvecs, cam_tvecs = readCalibParameters(args.json_file)
image = cv2.imread(args.image_path)
if image is None:
raise OSError(f"Could not read image: {args.image_path}")
image_undistorted = cv2.undistort(image, K[0], k[0], None, K[0])
filename, file_extension = os.path.splitext(args.image_path)
imageOutPath = filename + "_undistorted" + file_extension
if not cv2.imwrite(imageOutPath, image_undistorted):
raise OSError(f"Could not write image: {imageOutPath}")
print(f"Wrote {imageOutPath}")
except (OSError, ValueError, cv2.error) as error:
parser.exit(1, f"Error: {error}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())