I got this camera 170 FOV, put it under my car's winch at the front and I just cannot unwrap the image (calibration video and images). I use this repository for my 360 car view project.
As calibration with --fisheye is crashing. (I use a video multiplexer to get all 4 camera frames at once as one frame so there's division to quadrants happening in the code)
Code:
"""
~~~~~~~~~~~~~~~~~~~~~~~~~~
Fisheye Camera calibration
~~~~~~~~~~~~~~~~~~~~~~~~~~
Usage:
python calibrate_camera.py \
-i 0 \
-grid 9x6 \
-out fisheye.yaml \
-framestep 20 \
--resolution 640x480
--fisheye
"""
import argparse
import os
import numpy as np
import cv2
from surround_view import CaptureThread, MultiBufferManager
from surround_view.multiplex_capture_thread import MultiplexCaptureThread
from surround_view import MultiBufferManager
import surround_view.utils as utils
# we will save the camera param file to this directory
TARGET_DIR = os.path.join(os.getcwd(), "yaml")
# default param file
DEFAULT_PARAM_FILE = os.path.join(TARGET_DIR, "camera_params.yaml")
def main():
parser = argparse.ArgumentParser()
# input video stream
parser.add_argument("-i", "--input", type=int, default=1,
help="input camera device")
# chessboard pattern size
parser.add_argument("-grid", "--grid", default="9x6",
help="size of the calibrate grid pattern")
parser.add_argument("-r", "--resolution", default="960x540",
help="resolution of the camera image")
parser.add_argument("--quadrant", type=int, default=0,
help="which quadrant of the multiplexed image to use (0=front,1=back,2=left,3=right)")
parser.add_argument("-framestep", type=int, default=30,
help="use every nth frame in the video")
parser.add_argument("-o", "--output", default=DEFAULT_PARAM_FILE,
help="path to output yaml file")
parser.add_argument("-fisheye", "--fisheye", action="store_true",
help="set true if this is a fisheye camera")
parser.add_argument("-flip", "--flip", default=0, type=int,
help="flip method of the camera")
parser.add_argument("--no_gst", action="store_true",
help="set true if not use gstreamer for the camera capture")
args = parser.parse_args()
if not os.path.exists(TARGET_DIR):
os.mkdir(TARGET_DIR)
text1 = "press c to calibrate"
text2 = "press q to quit"
text3 = "device: {}".format(args.input)
text4 = "press SPACE to capture sample"
font = cv2.FONT_HERSHEY_SIMPLEX
fontscale = 0.6
resolution_str = args.resolution.split("x")
W = int(resolution_str[0])
H = int(resolution_str[1])
grid_size = tuple(int(x) for x in args.grid.split("x"))
grid_points = np.zeros((1, np.prod(grid_size), 3), np.float32)
grid_points[0, :, :2] = np.indices(grid_size).T.reshape(-1, 2)
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane
# Request full source resolution as 2x quadrant (e.g., 960x540 quadrant => 1920x1080 source)
cap_thread = MultiplexCaptureThread(webcam_id=args.input, resolution=(W * 2, H * 2))
buffer_manager = MultiBufferManager()
# ✅ Register 4 virtual quadrant devices in the buffer manager
for cam_id in cap_thread.device_ids:
cap_thread.device_id = cam_id
buffer_manager.bind_thread(cap_thread, buffer_size=8)
if cap_thread.connect_camera():
cap_thread.start()
else:
print("Cannot open USB camera")
return
quit = False
do_calib = False
i = -1
while True:
i += 1
img = buffer_manager.get_device(args.quadrant).get().image
# On first frame, print debug info about feed dimensions
if i == 0:
req_src_w, req_src_h = (W * 2, H * 2)
print("[DEBUG] Requested source resolution: {}x{}".format(req_src_w, req_src_h))
if hasattr(cap_thread, "resolution") and cap_thread.resolution is not None:
print("[DEBUG] Actual camera source resolution: {}x{}".format(cap_thread.resolution[0], cap_thread.resolution[1]))
print("[DEBUG] Quadrant frame size (received): {}x{}".format(img.shape[1], img.shape[0]))
# Only attempt corner detection and sample collection every `framestep` frames
if i % args.framestep == 0:
print("searching for chessboard corners in frame " + str(i) + "...")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
found, corners = cv2.findChessboardCorners(
gray,
grid_size,
cv2.CALIB_CB_ADAPTIVE_THRESH +
cv2.CALIB_CB_NORMALIZE_IMAGE +
cv2.CALIB_CB_FILTER_QUADS
)
if found:
term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.01)
cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), term)
print("OK")
imgpoints.append(corners)
objpoints.append(grid_points)
cv2.drawChessboardCorners(img, grid_size, corners, found)
cv2.putText(img, text1, (20, 70), font, fontscale, (255, 200, 0), 2)
cv2.putText(img, text2, (20, 110), font, fontscale, (255, 200, 0), 2)
cv2.putText(img, text3, (20, 30), font, fontscale, (255, 200, 0), 2)
cv2.putText(img, text4, (20, 150), font, fontscale, (255, 200, 0), 2)
disp_w = int(img.shape[1])
disp_h = int(img.shape[0])
if i == 0: # only set once on first frame
cv2.namedWindow("corners", cv2.WINDOW_NORMAL)
cv2.resizeWindow("corners", disp_w, disp_h)
disp = img
cv2.resize(img, (disp_w, disp_h))
cv2.imshow("corners", disp)
key = cv2.waitKey(1) & 0xFF
if key == ord("c"):
print("\nPerforming calibration...\n")
N_OK = len(objpoints)
if N_OK < 1:
print("No corners detected, calibration failed")
continue
else:
print(f"Proceeding with {N_OK} sample(s)")
do_calib = True
break
elif key == ord("q"):
quit = True
break
elif key == 32: # SPACE: manual capture of current frame
gray_manual = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
found_manual, corners_manual = cv2.findChessboardCorners(
gray_manual,
grid_size,
cv2.CALIB_CB_ADAPTIVE_THRESH +
cv2.CALIB_CB_NORMALIZE_IMAGE +
cv2.CALIB_CB_FILTER_QUADS
)
if found_manual:
term_manual = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.01)
cv2.cornerSubPix(gray_manual, corners_manual, (5, 5), (-1, -1), term_manual)
print("OK (manual)")
imgpoints.append(corners_manual)
objpoints.append(grid_points)
cv2.drawChessboardCorners(img, grid_size, corners_manual, found_manual)
else:
print("No corners detected in current frame (manual capture skipped)")
if quit:
cv2.destroyAllWindows()
if do_calib:
N_OK = len(objpoints)
K = np.zeros((3, 3))
D = np.zeros((4, 1))
rvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)]
tvecs = [np.zeros((1, 1, 3), dtype=np.float64) for _ in range(N_OK)]
calibration_flags = (cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC +
cv2.fisheye.CALIB_CHECK_COND +
cv2.fisheye.CALIB_FIX_SKEW)
if args.fisheye:
ret, mtx, dist, rvecs, tvecs = cv2.fisheye.calibrate(
objpoints,
imgpoints,
(W, H),
K,
D,
rvecs,
tvecs,
calibration_flags,
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6)
)
else:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints,
imgpoints,
(W, H),
None,
None)
if ret:
fs = cv2.FileStorage(args.output, cv2.FILE_STORAGE_WRITE)
fs.write("resolution", np.int32([W, H]))
# Save the actual calibration results, not the initial placeholders
fs.write("camera_matrix", mtx)
fs.write("dist_coeffs", dist)
fs.release()
print("successfully saved camera data")
cv2.putText(img, "Success!", (220, 240), font, 2, (0, 0, 255), 2)
else:
cv2.putText(img, "Failed!", (220, 240), font, 2, (0, 0, 255), 2)
cv2.imshow("corners", img)
cv2.waitKey(0)
if __name__ == "__main__":
main()
Error:
cv2.error: OpenCV(4.12.0) D:\a\opencv-python\opencv-python\opencv\modules\calib3d\src\fisheye.cpp:1484:
error: (-3:Internal error) CALIB_CHECK_COND - Ill-conditioned matrix for input array 0 in function
'cv::internal::CalibrateExtrinsics'
I use normal calibration but the undistorted image seems like just 30% undistorted:
Tell me how I can undistort the image.
UPDATE
At some point I've managed to turn off the CALIB_CHECK_COND flag and got this image, where the edges are too distorted. Is there a way to undistort them too?

