You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 22:44:51