Skip to content

Single Camera (Python)

#!/usr/bin/env python3

import argparse
import glob
import os
import time

from libCalibGeom import (
    Calibration,
    Camera,
    CameraModelType,
    ImageU8,
    ImageDetection,
    ImageSize,
    ParameterState,
    PatternSize,
    Point3,
    Pose,
    OptimizationSettings,
    find_checkerboard,
    subpix_saddle_point_polynomium,
    validate_license_with_certificate,
)


def build_target(pattern_rows: int, pattern_cols: int, spacing: float):
    points = []
    for i in range(pattern_rows):
        for j in range(pattern_cols):
            points.append(Point3(j * spacing, i * spacing, 0.0))
    return points


def collect_detections(image_paths, pattern: PatternSize, kernel_half_width: int):
    detections = []
    image_size = None

    for path in image_paths:
        img = ImageU8()
        if not img.load_from_disk(path):
            continue

        image_size = ImageSize(img.get_width(), img.get_height())

        ok, points = find_checkerboard(img, pattern)
        if not ok or len(points) != pattern.rows * pattern.columns:
            continue

        points = subpix_saddle_point_polynomium(img, kernel_half_width, points)
        if len(points) != pattern.rows * pattern.columns:
            continue

        detections.append(ImageDetection(points, 0, len(detections), 0))

    return detections, image_size


def main():
    parser = argparse.ArgumentParser(description="Single camera calibration using libCalibGeom Python bindings.")
    parser.add_argument("--images", required=True, help="Directory with calibration images.")
    parser.add_argument("--pattern-rows", type=int, default=6, help="Number of inner corners in rows.")
    parser.add_argument("--pattern-cols", type=int, default=7, help="Number of inner corners in columns.")
    parser.add_argument("--spacing", type=float, default=0.050, help="Feature spacing in meters.")
    parser.add_argument("--kernel-half-width", type=int, default=5, help="Subpixel kernel half width.")
    parser.add_argument("--license-certificate", default=None, help="Compact libCalibGeom license certificate.")

    args = parser.parse_args()

    if args.license_certificate:
        if not validate_license_with_certificate(args.license_certificate):
            print("License validation failed (certificate).")
            return 1
    else:
        if not validate_license_with_certificate():
            print("License validation failed. Set LIBCALIBGEOM_LICENSE_CERTIFICATE or pass --license-certificate.")
            return 1

    image_paths = sorted(glob.glob(os.path.join(args.images, "*.png")))
    if not image_paths:
        print(f"No images found in {args.images}")
        return 1

    pattern = PatternSize()
    pattern.rows = args.pattern_rows
    pattern.columns = args.pattern_cols

    print(f"Loading {len(image_paths)} images from {args.images}")

    start = time.time()
    detections, image_size = collect_detections(image_paths, pattern, args.kernel_half_width)

    if not detections or image_size is None:
        print("No valid detections found.")
        return 1

    print(f"Detections: {len(detections)}")

    calib = Calibration()
    calib.add_target(build_target(pattern.rows, pattern.columns, args.spacing))

    calib.poses = [Pose() for _ in detections]

    camera = Camera(CameraModelType.OpenCV, image_size)
    for name in ("f", "ar", "cx", "cy", "k1", "k2", "k3", "p1", "p2"):
        camera.model.param(name).state = ParameterState.FREE

    calib.add_camera(camera)

    print("Initializing...")
    calib.initialize(detections)

    print("Optimizing...")
    result = calib.optimize(detections, OptimizationSettings())

    elapsed = time.time() - start
    print(f"Reprojection RMS: {result.rpe_rms}")
    print(calib.cameras[0].to_string())
    print(f"Elapsed: {elapsed:.2f}s")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Classes / Functions