Skip to content

Distortion Maps Undistortion (Python)

#!/usr/bin/env python3

import argparse
import os
import sys

import cv2
import numpy as np

def readCalibDistortionMap(filePathBase):
    matrix_path = filePathBase + "_K.json"
    if not os.path.isfile(matrix_path):
        raise RuntimeError(f"Missing camera matrix file: {matrix_path}")
    try:
        fs = cv2.FileStorage(matrix_path, cv2.FILE_STORAGE_READ)
        try:
            if not fs.isOpened():
                raise RuntimeError(f"Cannot open camera matrix: {matrix_path}")
            K = fs.getNode("K").mat()
        finally:
            fs.release()
    except (cv2.error, SystemError) as error:
        raise RuntimeError(f"Invalid OpenCV matrix file {matrix_path}: {error}") from error
    if (K is None or K.shape != (3, 3) or K.dtype != np.float64
            or not np.isfinite(K).all()):
        raise RuntimeError(f"{matrix_path}: expected a finite 3x3 double OpenCV matrix named K")

    def read_map(path):
        if not os.path.isfile(path):
            raise RuntimeError(f"Missing distortion map: {path}")
        try:
            image = cv2.imread(path, cv2.IMREAD_UNCHANGED)
        except cv2.error as error:
            raise RuntimeError(f"Cannot decode {path}: {error}") from error
        if image is None:
            raise RuntimeError(f"Cannot decode {path}; check the file and OpenCV TIFF/Zstandard support")
        if image.ndim != 2 or image.dtype != np.float32:
            raise RuntimeError(f"{path}: expected a single-channel float32 TIFF")
        return image

    mapU = read_map(filePathBase + "_mapU.tiff")
    mapV = read_map(filePathBase + "_mapV.tiff")
    if mapU.shape != mapV.shape:
        raise RuntimeError("Distortion U/V map dimensions differ")
    # Keep NaNs: they mark invalid lookup coordinates.
    return K, mapU, mapV

def main():
    parser = argparse.ArgumentParser(description="Undistort an image from libCalibGeom distortion-map exports.")
    parser.add_argument("distortion_path_base", help="Path prefix for the exported distortion maps.")
    parser.add_argument("image_path", help="Path to the distorted image.")
    args = parser.parse_args()

    try:
        K, mapU, mapV = readCalibDistortionMap(args.distortion_path_base)
        image = cv2.imread(args.image_path)
        if image is None:
            raise RuntimeError(f"Cannot read image {args.image_path}")
        if image.shape[:2] != mapU.shape:
            raise RuntimeError("Input image dimensions must match the calibrated map dimensions")
        # A black border at invalid/out-of-image lookups is not measured scene data.
        image_undistorted = cv2.remap(
            image, mapU, mapV, cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
        filename, file_extension = os.path.splitext(args.image_path)
        imageOutPath = filename + "_undistorted" + file_extension
        if not cv2.imwrite(imageOutPath, image_undistorted):
            raise RuntimeError(f"Cannot write image {imageOutPath}")
    except (RuntimeError, cv2.error) as error:
        print(f"Distortion map error: {error}", file=sys.stderr)
        return 1
    return 0


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

Classes / Functions