当前位置: 首页 > news >正文

深入解析:【双光相机配准】可见光相机内参标定流程

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
http://www.hskmm.com/?act=detail&tid=27342

相关文章:

  • oracle中引号的使用总结与报错信息
  • 2025 年电线电缆厂家最新推荐:实力厂家榜单重磅发布,涵盖多品类线缆及专业选择指南国标/朝阳/低压/阻燃/耐火/北京电线电缆厂家推荐
  • 5分钟,15分钟,差距大,做5分钟线要严格止损
  • 家政服务小程序系统:一站式家政服务解决方案
  • 营销农场小程序管理系统:营销吸粉与流量变现解决方案
  • 二部图,最大权/最小权完美匹配,费用流解法
  • OIFHA251009 比赛总结
  • 2025 滚珠丝杆厂家最新推荐榜单:精密 / 微型 / 重负载全品类适配,国产优质品牌选购指南不锈钢滚珠丝杆/大导程滚珠丝杆/研磨滚珠丝杆/高防尘滚珠丝杆厂家推荐
  • 2025智能电动伸缩门厂家推荐榜
  • 2025 滚珠丝杠厂家最新推荐榜:重负载 / 精密 / 研磨型产品优选清单及国产新锐品牌口碑解析
  • [Clickhouse] Clickhouse 客户端
  • 【实战】OpenCV 视频车辆统计
  • 2025 人力资源管理系统公司最新推荐榜单:AI 驱动下的全场景解决方案与品牌实力深度解析
  • P11988 [JOIST 2025] 宇宙怪盗 题解
  • 2025 年石墨烯厂家最新推荐榜单:氧化 / 羧基化 / 巯基化 / 羟基化 / 氨基化 / 氮掺杂石墨烯优质厂商全面解析与选购指南
  • 2025铝合金牺牲阳极厂家推荐榜:牺牲阳极阴极保护工业防腐技术
  • 2025 年压滤机厂家最新推荐排行榜:隔膜 / 污泥 / 真空 / 板框 / 带式压滤机厂家权威甄选指南板框/带式/污泥脱水/气化渣脱水专用/污泥专用脱水压滤机厂家推荐
  • 2025 年最新推荐!点胶机源头厂家权威排行榜:涵盖自动 / 果冻胶 / 无痕内衣 / 烫钻等多类型设备,助企业精准选品
  • 2025 年制袋机厂家推荐,广州速益科技提供多品类自动化设备与专业售后服务
  • 2025 年最新推荐云手机服务平台权威榜单:商用办公 / 多开设备 / 托管定制 / 租赁等场景优质品牌全解析
  • Octane 2022 汉化版适配C4D 2021-2023实用指南
  • 通俗易懂:什么是PostgreSQL中级认证(PGCP认证)
  • SQL Server 限制IP访问数据库的设置方法及注意事项
  • 2025 升降桌源头厂家最新推荐榜:聚焦国产新锐与实力大厂,解锁高性价比选购指南升降桌框/升降办公桌/升降办公桌框厂家推荐
  • 产品经理必看:原型设计工具三大能力解析(交互/AI/素材库)
  • AI 智能体 RAG 入门教程
  • 基于 RS 锁存器的真随机数生成器
  • 实用指南:会议安排问题之贪心算法
  • 10 9
  • 2025 年高压反应釜厂家最新推荐排行榜:涵盖多材质多类型设备,精选实力厂家助企业精准选购高温/加氢/不锈钢/实验室/钛材/镍材高压反应釜厂家推荐