OpenCV棋盘格相机标定:畸变系数波动异常问题咨询
相机标定畸变系数波动异常问题
我用OpenCV棋盘格做相机标定,同一相机、同一位置、光照略有变化的条件下,多次采集不同图像组(每组15-22张)标定,发现每次的畸变系数差异极大。用Excel统计后,焦距和光心坐标数值稳定,但畸变系数波动剧烈,比如p1的变异系数达717%,标准差是均值的7倍以上。想知道这个现象的原因,以及如果情况正常该怎么选标定参数。
可能的原因
- 标定图像覆盖范围不足:畸变系数(尤其是切向畸变p1、p2)对棋盘格在画面中的位置和角度敏感。如果每组图像中棋盘格没覆盖画面边缘、角落,或倾斜角度不够多样,标定算法无法准确拟合畸变模型,导致结果波动。
- 角点检测误差:光照变化会影响角点检测精度,尤其是棋盘格边缘或光照不均区域的角点。
cornerSubPix的结果误差会被放大到畸变系数计算中,切向畸变对这种误差更敏感。 - 标定算法无约束:默认的
calibrateCamera没有限制畸变系数范围,当观测数据不足时,算法可能输出极端值拟合噪声数据。 - 棋盘格本身问题:打印的棋盘格若不平整,会引入系统误差,且每次拍摄的形变角度不同,导致畸变系数波动。
解决建议
1. 优化图像采集
- 确保每组图像中棋盘格覆盖画面所有区域:包括四个角落、边缘,以及不同倾斜角度(至少30°以上的俯仰/旋转),每组图像建议至少20张,角度、位置分布均匀。
- 尽量保证棋盘格表面光照均匀,减少反光或阴影,避免光照剧烈变化。
2. 提升角点检测精度
- 调整
findChessboardCorners参数,比如增加CALIB_CB_FILTER_QUADS过滤错误的棋盘格四边形;或在cornerSubPix前对图像做高斯模糊,减少噪声影响。 - 手动剔除角点检测明显偏移的图像,不加入标定数据集。
3. 改进标定算法设置
- 使用
cv2.fisheye.calibrate(鱼眼镜头适用),或在calibrateCamera中加入约束:比如设置flags=cv2.CALIB_FIX_K3(不需要高阶畸变时),或用cv2.CALIB_USE_INTRINSIC_GUESS,以之前稳定的焦距/光心作为初始值,帮助算法收敛到合理结果。 - 计算重投影误差,只保留重投影误差小于0.5像素的图像组,误差越小说明拟合度越好。
4. 选择合适的标定参数
- 若多次标定结果波动,取多次结果的平均值,同时剔除偏离均值3倍标准差的极端异常值。
- 优先选择重投影误差最小的参数组,该结果与实际图像的拟合度最优。
- 若畸变系数波动大但重投影误差都可接受,可使用平均后的畸变系数,再通过后续畸变校正验证效果,选择校正后图像边缘畸变最小的参数。
标定代码
""" Based on https://learnopencv.com/camera-calibration-using-opencv https://forum.dji.com/thread-206289-1-1.html USAGE: 1. Print the pattern at https://github.com/opencv/opencv/blob/4.x/doc/pattern.png 2. Take a few pictures with the camera 3. Save to INPUT_CALIBRATION_DIRECTORY/*.jpg 4. Run `python calibrate_camera.jpg`, which generates a YAML file at OUTPUT_YAML_FILEPATH 5. Save YAML file to `FLING.AI\\backend\\FAI_backend_API\\config` """ import cv2 import code import numpy as np import os import sys import glob import pathlib import oyaml as yaml # To fix ordering; see https://stackoverflow.com/questions/5121931 from collections import OrderedDict import PIL.Image from PIL.ExifTags import TAGS from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = ( True # To avoid https://stackoverflow.com/questions/12984426/ ) # Round to 3 digits and suppress scientific notation when printing numpy arrays np.set_printoptions(precision=4, suppress=True) OUTPUT_YAML_FILEPATH = r"C:\Users\Documents\camera_calibration\air2S\trial 5\air2S_trial_5.yaml" INPUT_CALIBRATION_DIRECTORY = r"C:\Users\Documents\camera_calibration\air2S\trial 5" SHOW_IMAGES = True # Defining the dimensions of checkerboard CHECKERBOARD = (6, 9) if __name__ == "__main__": criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # Creating vector to store vectors of 3D points for each checkerboard image objpoints = [] # Creating vector to store vectors of 2D points for each checkerboard image imgpoints = [] # A vector to store height and width of each image img_shapes = [] # A vector for the camera make and model cam_types = [] # Defining the world coordinates for 3D points objp = np.zeros((1, CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[0, :, :2] = np.mgrid[0 : CHECKERBOARD[0], 0 : CHECKERBOARD[1]].T.reshape(-1, 2) # Extracting path of individual image stored in a given directory image_paths = [ str(x) for x in pathlib.Path(INPUT_CALIBRATION_DIRECTORY).glob("**/*") if x.is_file() and pathlib.Path(x).suffix.upper() in [".PNG", ".JPG", ".JPEG"] ] if len(image_paths) == 0: print("No images to process.") sys.exit() for i, image_path in enumerate(image_paths): print( f"Image {i+1} of {len(image_paths)}: {pathlib.Path(image_path).stem}.", end="", ) # "Open" the image to get the EXIF. Luckily this is a lazy operation and # the image binary is not loaded here, so there is no big performance hit. pil_image = PIL.Image.open(image_path) # Note we can get these numbers 271, 272 from # [k for k in PIL.ExifTags.TAGS.keys() if PIL.ExifTags.TAGS[k] in ["Make", "Model"]] if not pil_image._getexif() is None: cam_types.append((pil_image._getexif()[271], pil_image._getexif()[272])) else: cam_types.append(("unknown", "unknown")) img = cv2.imread(image_path) if img is None: continue img_shapes.append(np.copy(img.shape[:2])) # If the current image is flipped 90 degrees relative to the first one, fix it if ( len(img_shapes) > 1 and img_shapes[-1][0] == img_shapes[0][1] and img_shapes[-1][1] == img_shapes[0][0] and img_shapes[-1][1] != img_shapes[-1][0] ): img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) img_shapes[-1] = [img_shapes[-1][1], img_shapes[-1][0]] print("rot img 90°", end="") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Find the chess board corners # If the desired number of corners are found in the image then returns True with the corners is_chessboard_success, corners = cv2.findChessboardCorners( gray, CHECKERBOARD, cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK + cv2.CALIB_CB_NORMALIZE_IMAGE, ) if not is_chessboard_success: print(" findChessboardCorners fail.") # Not successful so we won't use this image in camera calibration nor visualize it else: print(" findChessboardCorners done.", end="") # Refine the pixel coordinates for given 2d points corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) print(" cornerSubPix done.", end="") # Add the image and object points to for use in camera calibration imgpoints.append(corners2) objpoints.append(objp) if SHOW_IMAGES: # Display the chessboard detection on the image! img = cv2.drawChessboardCorners( img, CHECKERBOARD, corners2, is_chessboard_success ) print(" drawChessboardCorners done.") # Display a thumbnail thumb_size = (img.shape[1] // 6, img.shape[0] // 6) img_thumbnail = cv2.resize( img, thumb_size, interpolation=cv2.INTER_NEAREST ) cv2.imshow("img", img_thumbnail) cv2.waitKey(700) # wait 700 ms before moving on else: print() cv2.destroyAllWindows() # Check that all images have the same shape img_shapes = np.array(img_shapes) if not np.all(img_shapes == img_shapes[0, :]): raise AssertionError("Not all images are the same size:") print(img_shapes) height, width = img_shapes[0, :] cam_types = np.array(cam_types) if not np.all(cam_types == cam_types[0, :]): raise AssertionError("Not all images were taken by the same camera:") print(cam_types) """ Perform camera calibration by passing the value of - known 3D points (objpoints) and - corresponding pixel coordinates of the detected corners (imgpoints) """ ret, mtx, dist, _, _ = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) # rvecs = np.array(rvecs) # tvecs = np.array(tvecs) dist = list(dist[0]) print("\nCamera matrix:\n", mtx) print("\nDistortion coefficients:\n", dist) # print("rvecs: \n\n", rvecs) # print("tvecs: \n\n", tvecs) # Summarize camera intrinsic data intrinsic_data = OrderedDict( [ ("camera_make", str(cam_types[0][0])), ("camera_model", str(cam_types[0][1])), ( "pinhole", OrderedDict( [ ("fx", float(mtx[0, 0])), ("fy", float(mtx[1, 1])), ("cx", float(mtx[0, 2])), ("cy", float(mtx[1, 2])), ("width", int(width)), ("height", int(height)), ("skew", 0.0), ] ), ), ( "dist_coeffs", [ float(dist[0]), float(dist[1]), float(dist[2]), float(dist[3]), float(dist[4]), ], ), ("version", 0), ] ) # Save camera intrinsic data to YAML file with open(OUTPUT_YAML_FILEPATH, "w") as outfile: yaml.dump(intrinsic_data, outfile, default_flow_style=False)
内容的提问来源于stack exchange,提问作者rynwhai
相关产品推荐
相关产品推荐

