decadenza / SimpleStereo

Stereo vision made Simple

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Stereo rig with unknown in-camera distortion correction

KevinCain opened this issue · comments

Is there a provision in SS to skip intrinsics calculations for one or both member cameras in a stereo rig? For example -- is there a way to lock an camera with an identity matrix and similarly lock zeros for the distortion coefficients, so that we can be sure that a subsequent SS undistortion process itself is not introducing artifacts or errors?

When using SS to build and rectify a stereo rig where one of the two cameras produces corrected images by an unknown process, I've found it can be destructive to attempt to calibrate the ostensibly distortion-corrected frame. If I had to guess, I would suppose it's because the frames have been cropped to hide the margins after distortion correction, so an attempt to calibrate the cropped images introduces pincushion distortion.

Yes, if calibration is not appropriate most of the times distortion correction worsens final result instead of improving. IMHO, those situations are symptoms of poor calibration.

Anyway, working with a projector it happened to disable the distortion correction, which you can do as simple as:

import os

import simplestereo as ss

# Paths
curPath = os.path.dirname(os.path.realpath(__file__))
loadFile = os.path.join(curPath,"rig.json")      # StereoRig file

# Load stereo rig from file
rig = ss.StereoRig.fromFile(loadFile)

rig.distCoeffs1 = None    # Remove distortion correction. Internally this is equivalent of having all zeroed coefficients.
rig.distCoeffs2 = None

# Optionally save to file the modified rig
saveFile = os.path.join(curPath,"rig_no_distortion.json") # Destination
rig.save(saveFile)

# or use it straightaway.

However the best thing would be to directly disable the distortion correction while performing calibration.
Since ss is meant for common use cases, the use of the standard 5 coefficients is hard coded in https://github.com/decadenza/SimpleStereo/blob/d104dba8cddd7e43156059d68f2abdba2538a88e/simplestereo/calibration.py#L156C1-L156C1

However, the calibration function ss.calibration.chessboardStereo may be modified to accept different setups, actually I've just found a comment I left at

# TO DO flags management to provide different configurations to user

So definitely this can be done. I'll plan it for next release.

I implemented the possibility to set 0, 5 or 8 parameters for distortion correction. The advanced options (12 or 14) are not implemented yet.
See latest release 1.0.7 (also via pip install simplestereo)

For your use case, just set distortionCoeffsNumber=0 to disable distortion correction entirely.

Thanks @decadenza for your characteristically fast and helpful effort!

Unless I'm missing something obvious, I believe this uses a single value for the distortion coefficients for both left and right images in the stereo pair. In my case we have two cameras, one that produces distorted ~120^ FOV images and the other which produces undistorted images in-camera through some black-box operation that likely changes the intrinsics, e.g.: by cropping the image to remove black pixels at the perimeter after undistortion.

In this case I changed your current method to allow us to set the number of distortion coefficients for the left and right images in the stereo pair separately, as below. Will this approach have unintended consequences for rig rectification downstream?

stereoRig = chessboardStereo(images, distortionCoeffsNumberLeft=5, distortionCoeffsNumberRight=0)

import numpy as np
import cv2

def chessboardStereo(images, chessboardSize=DEFAULT_CHESSBOARD_SIZE, squareSize=1, 
                     distortionCoeffsNumberLeft=5, distortionCoeffsNumberRight=5):
    """
    Performs stereo calibration between two cameras with separate distortion coefficients for each camera.
    
    Parameters
    ----------
    images : list or tuple
        A list (or tuple) of 2 dimensional tuples (ordered left and right) of image paths.
    chessboardSize: tuple
        Chessboard internal dimensions as (width, height).
    squareSize : float
        The size of a square in your defined world units.
    distortionCoeffsNumberLeft: int
        The number of distortion coefficients for the left camera.
    distortionCoeffsNumberRight: int
        The number of distortion coefficients for the right camera.
    
    Returns
    -------
    StereoRig
        A calibrated stereo rig object.
    """

    # Arrays to store object points and image points from all the images.
    objp = np.zeros((chessboardSize[0]*chessboardSize[1], 3), np.float32)
    objp[:, :2] = np.mgrid[0:chessboardSize[0], 0:chessboardSize[1]].T.reshape(-1, 2) * squareSize
    imagePoints1 = []  # Image points for the left camera
    imagePoints2 = []  # Image points for the right camera

    # Process each pair of images
    for path1, path2 in images:
        img1 = cv2.imread(path1, cv2.IMREAD_GRAYSCALE)
        img2 = cv2.imread(path2, cv2.IMREAD_GRAYSCALE)

        if img1 is None or img2 is None:
            raise ValueError(f"One of the images in {path1}, {path2} not found!")

        # Find the chessboard corners
        ret1, corners1 = cv2.findChessboardCorners(img1, chessboardSize)
        ret2, corners2 = cv2.findChessboardCorners(img2, chessboardSize)

        # If found, add object points, image points (after refining them)
        if ret1 and ret2:
            corners1 = cv2.cornerSubPix(img1, corners1, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
            corners2 = cv2.cornerSubPix(img2, corners2, DEFAULT_CORNERSUBPIX_WINSIZE, (-1,-1), DEFAULT_TERMINATION_CRITERIA)
            imagePoints1.append(corners1)
            imagePoints2.append(corners2)

    # Calibration flags for each camera
    flagsLeft = _getCalibrationFlags(distortionCoeffsNumberLeft)
    flagsRight = _getCalibrationFlags(distortionCoeffsNumberRight)
    flags = flagsLeft | flagsRight  # Combine flags for both cameras

    # Initialize camera matrices and distortion coefficients
    cameraMatrix1 = np.eye(3, dtype=np.float64)
    cameraMatrix2 = np.eye(3, dtype=np.float64)
    distCoeffs1 = np.zeros(distortionCoeffsNumberLeft)
    distCoeffs2 = np.zeros(distortionCoeffsNumberRight)

    # Stereo calibration
    retval, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate(
        [objp] * len(imagePoints1), imagePoints1, imagePoints2,
        cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2,
        imageSize=img1.shape[::-1], flags=flags, criteria=DEFAULT_TERMINATION_CRITERIA)

    # Create a StereoRig object (you need to define StereoRig according to your needs)
    stereoRigObj = StereoRig(imageSize1=img1.shape[::-1][:2],
                             imageSize2=img2.shape[::-1][:2],
                             cameraMatrix1=cameraMatrix1,
                             cameraMatrix2=cameraMatrix2,
                             distCoeffs1=distCoeffs1,
                             distCoeffs2=distCoeffs2,
                             R=R, T=T, E=E, F=F,
                             reprojectionError=retval)

    return stereoRigObj