【双光相机配准】可见光相机内参标定流程

1. 准备工作

准备棋盘格标定板,最好可以采购专业的标定板

如图:

2. 采集图像数据

标定板在多个角度,多个距离下采集图片;

整个过程中保持相机配置不变(分辨率、焦距、变倍等)

3. 根据采集图像计算相机内参

代码输入参数介绍:

CameraCalibrator对象需要输入images_list,即标定图像路径的数组;如何获取到images_list根据自己标定的数据存放位置和方式自行决定;

pattern_size是指棋盘格的内焦点数量,分别是行数和列数,不要理解为格子的行数和列数。如图的标定板应该是;

标定的可视化图片默认存储在corner_visual目录下,可以判断角度的检测是否正常,效果如图:

内参标定结果文件存储在当前目录下的vis_camera_calibration_{timestamp}.json和vis_camera_calibration_{timestamp}.npz文件中。

两者的文件内容相同,仅格式不同。

相机内参标定的可执行代码:

复制代码
import cv2
import numpy as np
import glob
import os
import json
from datetime import datetime

class CameraCalibrator:
    def __init__(self, images_list, pattern_size, square_size_mm, camera_type="ir"):
        """
        相机标定类 - 使用毫米(mm)为单位

        Args:
            images_list: 标定图像路径列表
            pattern_size: 棋盘格内角点数量 (cols, rows)
            square_size_mm: 棋盘格方格实际尺寸(毫米)
            camera_type: 相机类型 ("ir" 或 "vis")
        
        单位说明:
        - 棋盘格尺寸: 毫米(mm)
        - 世界坐标: 毫米(mm)
        - 焦距: 像素/毫米(px/mm)
        - 重投影误差: 像素(px)
        """
        self.images_list = images_list
        self.pattern_size = pattern_size
        self.square_size_mm = square_size_mm  # 明确使用毫米单位
        self.camera_type = camera_type

        # 世界坐标系点 (Z=0) - 使用毫米单位
        self.objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
        self.objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
        self.objp *= square_size_mm  # 乘以毫米尺寸

        # 标定结果
        self.camera_matrix = None
        self.dist_coeffs = None
        self.rvecs = None
        self.tvecs = None
        self.reprojection_error = None

    def find_corners(self, save_visualization=True, visual_dir="corner_visual"):
        """检测棋盘格角点"""
        images = self.images_list
        
        print(f"找到 {len(images)} 张标定图像")
        
        objpoints_all = []
        imgpoints_all = []
        used_imgs_all = []
        gray_shape = None
        
        if save_visualization:
            os.makedirs(visual_dir, exist_ok=True)
        
        success_count = 0
        for idx, fname in enumerate(images):
            img = cv2.imread(fname)
            if img is None:
                print(f"⚠️ 无法读取图像: {fname}")
                continue
                
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            
            # 优先使用更稳定的方法
            ret, corners = cv2.findChessboardCornersSB(
                gray, self.pattern_size, 
                flags=cv2.CALIB_CB_NORMALIZE_IMAGE | cv2.CALIB_CB_EXHAUSTIVE
            )
            
            # 如果失败,尝试传统方法
            if not ret:
                ret, corners = cv2.findChessboardCorners(
                    gray, self.pattern_size,
                    flags=cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE
                )
            
            if ret:
                # 亚像素精确化
                corners_refined = cv2.cornerSubPix(
                    gray, corners, (11, 11), (-1, -1),
                    criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
                )
                
                objpoints_all.append(self.objp)
                imgpoints_all.append(corners_refined)
                used_imgs_all.append(fname)
                success_count += 1
                
                if gray_shape is None:
                    gray_shape = gray.shape[::-1]
                
                if save_visualization:
                    vis_img = img.copy()
                    cv2.drawChessboardCorners(vis_img, self.pattern_size, corners_refined, ret)
                    filename = os.path.basename(fname)
                    save_path = os.path.join(visual_dir, f"corners_{filename}")
                    cv2.imwrite(save_path, vis_img)
                    
                print(f"✅ {os.path.basename(fname)}: 角点检测成功")
            else:
                print(f"❌ {os.path.basename(fname)}: 角点检测失败")
        
        print(f"\n角点检测完成: {success_count}/{len(images)} 张图像成功")
        return objpoints_all, imgpoints_all, used_imgs_all, gray_shape

    def calibrate(self, reproj_error_thresh=0.5, save_results=True):
        """执行相机标定"""
        # 检测角点
        objpoints_all, imgpoints_all, used_imgs_all, gray_shape = self.find_corners()
        
        if len(objpoints_all) < 5:
            raise RuntimeError(f"有效标定图像不足 5 张(当前 {len(objpoints_all)} 张),请检查棋盘格和图像质量")
        
        # 第一次标定(计算重投影误差)
        print("正在进行第一次标定...")
        ret_all, K_all, D_all, rvecs_all, tvecs_all = cv2.calibrateCamera(
            objpoints_all, imgpoints_all, gray_shape, None, None,
            flags=cv2.CALIB_FIX_K3 + cv2.CALIB_ZERO_TANGENT_DIST
        )
        
        # 计算重投影误差
        errors = []
        for i in range(len(objpoints_all)):
            imgpoints_proj, _ = cv2.projectPoints(objpoints_all[i], rvecs_all[i], tvecs_all[i], K_all, D_all)
            error = cv2.norm(imgpoints_all[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj)
            errors.append(error)
        
        # 过滤高误差图像
        good_idx = [i for i, e in enumerate(errors) if e < reproj_error_thresh]
        objpoints = [objpoints_all[i] for i in good_idx]
        imgpoints = [imgpoints_all[i] for i in good_idx]
        used_imgs = [used_imgs_all[i] for i in good_idx]
        
        print(f"过滤前: {len(objpoints_all)} 张, 平均误差: {np.mean(errors):.3f} px")
        print(f"过滤后: {len(objpoints)} 张, 平均误差: {np.mean([errors[i] for i in good_idx]):.3f} px")
        
        if len(objpoints) < 5:
            raise RuntimeError("过滤后有效图像不足 5 张,请调整阈值或补充图像")
        
        # 第二次标定(使用过滤后的数据)
        print("正在进行最终标定...")
        self.reprojection_error, self.camera_matrix, self.dist_coeffs, self.rvecs, self.tvecs = cv2.calibrateCamera(
            objpoints, imgpoints, gray_shape, None, None,
            flags=cv2.CALIB_FIX_K3 + cv2.CALIB_ZERO_TANGENT_DIST
        )
        
        # 保存结果
        if save_results:
            self.save_results(used_imgs, errors, good_idx)
        
        return self.reprojection_error, self.camera_matrix, self.dist_coeffs

    def calculate_focal_length(self):
        """计算焦距(像素/毫米)"""
        if self.camera_matrix is None:
            raise ValueError("请先执行标定")
        
        fx = self.camera_matrix[0, 0]  # 像素/毫米
        fy = self.camera_matrix[1, 1]  # 像素/毫米
        
        print(f"焦距 fx: {fx:.2f} px/mm")
        print(f"焦距 fy: {fy:.2f} px/mm")
        print(f"平均焦距: {(fx + fy) / 2:.2f} px/mm")
        
        return (fx + fy) / 2

    def save_results(self, used_imgs, errors, good_idx):
        """保存标定结果"""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        
        # 保存为npz文件
        npz_filename = f"{self.camera_type}_camera_intrinsics_{timestamp}.npz"
        np.savez(npz_filename, 
                camera_matrix=self.camera_matrix,
                dist_coeffs=self.dist_coeffs,
                reprojection_error=self.reprojection_error)
        
        # 保存为JSON文件(可读性更好)
        json_filename = f"{self.camera_type}_camera_calibration_{timestamp}.json"
        
        # 计算焦距(像素/毫米)
        focal_length_px_per_mm = self.calculate_focal_length()
        
        calibration_data = {
            "camera_type": self.camera_type,
            "calibration_date": timestamp,
            "image_count": len(good_idx),
            "reprojection_error": float(self.reprojection_error),
            "camera_matrix": self.camera_matrix.tolist(),
            "dist_coeffs": self.dist_coeffs.flatten().tolist(),
            "focal_length_px_per_mm": float(focal_length_px_per_mm),
            "square_size_mm": float(self.square_size_mm),
            "used_images": [os.path.basename(f) for i, f in enumerate(used_imgs) if i in good_idx],
            "units": {
                "focal_length": "px/mm",
                "square_size": "mm",
                "world_coordinates": "mm"
            }
        }
        
        with open(json_filename, 'w') as f:
            json.dump(calibration_data, f, indent=2)
        
        print(f"\n✅ 标定结果已保存:")
        print(f"   - {npz_filename} (NumPy格式)")
        print(f"   - {json_filename} (JSON格式)")

    def print_detailed_results(self):
        """打印详细标定结果"""
        if self.camera_matrix is None:
            raise ValueError("请先执行标定")
        
        print("\n" + "="*50)
        print(f"📷 {self.camera_type.upper()} 相机标定结果")
        print("="*50)
        
        print(f"棋盘格尺寸: {self.square_size_mm} mm")
        print(f"重投影误差: {self.reprojection_error:.3f} px")
        print(f"图像尺寸: {self.camera_matrix[0,2]*2:.0f} x {self.camera_matrix[1,2]*2:.0f}")
        
        print("\n相机内参矩阵 (单位: 像素/毫米):")
        print(f"fx = {self.camera_matrix[0,0]:.2f} px/mm")
        print(f"fy = {self.camera_matrix[1,1]:.2f} px/mm")
        print(f"cx = {self.camera_matrix[0,2]:.2f} px")
        print(f"cy = {self.camera_matrix[1,2]:.2f} px")
        
        print("\n畸变系数:")
        print(f"k1 = {self.dist_coeffs[0,0]:.6f}")
        print(f"k2 = {self.dist_coeffs[0,1]:.6f}")
        print(f"p1 = {self.dist_coeffs[0,2]:.6f}")
        print(f"p2 = {self.dist_coeffs[0,3]:.6f}")
        print(f"k3 = {self.dist_coeffs[0,4]:.6f}")


# ====================== 辅助函数 ======================
def collect_images(root_dir, sub_dir="vis"):
    """
    遍历 root_dir 下所有子目录,收集 {sub_dir} 文件夹里的图像路径
    """
    img_exts = ("*.png", "*.jpg", "*.bmp", "*.jpeg")
    images = []
    vis_dirs = glob.glob(os.path.join(root_dir, f"*/{sub_dir}"))
    for vdir in vis_dirs:
        for ext in img_exts:
            images.extend(glob.glob(os.path.join(vdir, ext)))
    return sorted(images)


# ====================== 使用示例 ======================
if __name__ == "__main__":
    # 收集图像路径列表(需要根据自己情况来设置)
    images_list = collect_images("250922", sub_dir="vis")

    # 创建标定器
    calibrator = CameraCalibrator(
        images_list=images_list,
        pattern_size=(11, 8),       # 内角点数量 (列, 行)
        square_size_mm=20.0,        # 棋盘格尺寸 (毫米)
        camera_type="vis"
    )

    try:
        # 执行标定
        reproj_error, camera_matrix, dist_coeffs = calibrator.calibrate(reproj_error_thresh=0.5)
        
        # 打印详细结果
        calibrator.print_detailed_results()
        
        # 计算焦距
        focal_length_px_per_mm = calibrator.calculate_focal_length()
        focal_length_px_per_m = focal_length_px_per_mm * 1000  # 转换为像素/米
        
        print(f"\n📏 焦距转换:")
        print(f"   {focal_length_px_per_mm:.2f} px/mm")
        print(f"   {focal_length_px_per_m:.2f} px/m")
        
    except Exception as e:
        print(f"❌ 标定失败: {e}")

4. 加载相机内参文件

vis_camera_calibration_{timestamp}.json的内参结构如下,按需加载并使用

{

"camera_type": "vis",

"calibration_date": "20250923_160641",

"image_count": 50,

"reprojection_error": 0.20444170548920967,

"camera_matrix": [

1287.9153968530886, 0.0, 666.6348530947699 \], \[ 0.0, 1294.5628736235296, 353.20852733360584 \], \[ 0.0, 0.0, 1.0

],

"dist_coeffs": [

-0.3249388217365418,

0.22219388748081903,

0.0,

0.0,

0.0

],

"focal_length_px_per_mm": 1291.2391352383092,

"square_size_mm": 20.0,

"used_images": [

"100_1.png",

"100_2.png",

"100_3.png",

"100_4.png",

"100_5.png",

"110_1.png",

"110_2.png",

"110_3.png",

"110_4.png",

"110_5.png",

"120_1.png",

"120_2.png",

"120_3.png",

"120_4.png",

"120_5.png",

"130_1.png",

"130_2.png",

"130_3.png",

"130_4.png",

"130_5.png",

"140_1.png",

"140_2.png",

"140_3.png",

"140_4.png",

"140_5.png",

"150_1.png",

"150_2.png",

"150_3.png",

"150_4.png",

"150_5.png",

"160_1.png",

"160_2.png",

"160_3.png",

"160_4.png",

"160_5.png",

"170_1.png",

"170_2.png",

"170_3.png",

"170_4.png",

"170_5.png",

"70_1.png",

"70_2.png",

"70_3.png",

"80_1.png",

"80_2.png",

"80_3.png",

"90_1.png",

"90_2.png",

"90_3.png",

"90_4.png"

],

"units": {

"focal_length": "px/mm",

"square_size": "mm",

"world_coordinates": "mm"

}

}

示例:加载vis_camera_calibration_{timestamp}.json

复制代码
    def load_camera_intrinsics_from_json(self, json_path):
        """加载相机内参"""
        with open(json_path, 'r') as f:
            calib_data = json.load(f)
        
        camera_matrix = np.array(calib_data["camera_matrix"])
        if "focal_length_px_per_mm" in calib_data:
            focal_length_px_per_mm = calib_data["focal_length_px_per_mm"]
        else:
            fx, fy = camera_matrix[0,0], camera_matrix[1,1]
            focal_length_px_per_mm = (fx + fy) / 2
        print(f"📊 标定焦距: {focal_length_px_per_mm:.2f} px/mm")
        return camera_matrix, focal_length_px_per_mm
相关推荐
爱凤的小光6 小时前
图漾相机-ROS2-SDK-Ubuntu 4.X.X版本编译
linux·数码相机·ubuntu
3DVisionary6 小时前
红外热成像与数字图像相关(DIC)技术耦合在金属热变形分析中的应用
科技·数码相机·红外热成像·金属热变形·数字图像相关 dic·多场耦合分析·材料力学性能
北岛三生10 小时前
Imatest-SFRplus模块
图像处理·数码相机·测试工具·计算机视觉·测试用例·模块测试
张艾拉 Fun AI Everyday1 天前
AI + 制造:AI 如何重构制造业的质检与排产流程
数码相机
格林威11 天前
机器视觉在半导体制造中有哪些检测应用
人工智能·数码相机·yolo·计算机视觉·视觉检测·制造·相机
格林威11 天前
机器视觉检测如何使用360 度全景成像镜头进行AI 瑕疵检测
人工智能·深度学习·数码相机·机器学习·计算机视觉·视觉检测·相机
buleideli12 天前
Android相机API2,基于GLSurfaceView+SurfaceTexture实现相机预览,集成的相机算法采用GPU方案,简要说明
数码相机
点云SLAM12 天前
结构光三维重建原理详解(1)
人工智能·数码相机·计算机视觉·三维重建·结构光重建·gray 编码·标定校正
把玩计算机12 天前
相机几何 空间点到像素平面转换
数码相机·平面