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

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
相关推荐
youngong3 小时前
强迫症之用相机快门数批量重命名文件
数码相机·文件管理
weixin_466485113 天前
halcon标定助手的使用
数码相机
诸葛务农5 天前
ToF(飞行时间)相机在人形机器人非接触式传感领域内的应用
数码相机·机器人
塞北山巅5 天前
相机自动曝光(AE)核心算法——从参数调节到亮度标定
数码相机·算法
美摄科技6 天前
相机sdk是什么意思?
数码相机
phyit6 天前
全景相机领域,影石何以杀出重围?
数码相机
鄃鳕6 天前
装饰器【Python】
开发语言·python·数码相机
聪明不喝牛奶6 天前
【已解决】海康威视相机如何升级固件
数码相机
PAQQ6 天前
1站--视觉搬运工业机器人工作站 -- 相机部分
数码相机·机器人
诸葛务农6 天前
人形机器人基于视觉的非接触式触觉传感技术
数码相机·机器人