技术研究之相机标定

标签: 无 分类: 未分类 创建时间:2025-12-16 03:00:43 更新时间:2026-05-27 11:42:29

1.前言

做相机标定的主要作用就是获取相机的内参和外参,标定完成之后,可以将无人机的图像贴合到

2.棋盘标定

  • 准备标定板:打印标准棋盘格图案
  • 多角度拍摄:从不同角度拍摄10-20张照片
  • 角点检测:软件自动检测棋盘格角点
  • 参数计算:通过算法计算相机参数
  • 精度验证:评估标定精度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import cv2
import numpy as np

# 准备棋盘格参数
pattern_size = (9, 6) # 9×6的棋盘格
square_size = 25 # 方格大小25mm

# 存储角点坐标
obj_points = [] # 世界坐标系中的3D点
img_points = [] # 图像坐标系中的2D点

# 读取标定图像
images = ['calib1.jpg', 'calib2.jpg', ...]

for img_file in images:
img = cv2.imread(img_file)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size)

if ret:
obj_points.append(objp) # 添加世界坐标点
img_points.append(corners) # 添加图像坐标点

# 可视化角点
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
cv2.imshow('Corners', img)
cv2.waitKey(500)

# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, gray.shape[::-1], None, None
)

print("相机内参矩阵:")
print(mtx)
print("畸变系数:")
print(dist)

3.AprilTag标定

我有一张标定板,上面是4x4的Tags,通过第2步,我获取到了每个Tag的实际边长,和Tag的中心间距,可以计算出4x4x4个世界坐标,我用无人机拍一张照片,利用 apriltag.Detector() 方法,可以获取 4x4x4 个2D坐标,然后利用 calibrateCamera ,就可以进行标定。如果我有15张照片,那么第二张照片的3D世界坐标,和第一张应该是一样的对吗?不一样的,就是每张照片的2D 坐标。

  • 1.制作 AprilTags 4×4 的标定板
    (1)使用专业工具生成 4×4 网格的 AprilTag 标定板(如 apriltag.org 或 OpenCV)
    (2)每个 Tag 尺寸建议:40–60mm(A4纸打印时清晰可见)
    (3)Tag 间距建议:20–30mm(避免重叠,便于检测)
    (4)确保所有 Tag ID 不重复(0, 1, 2, …, 15)
    (5)A4纸的尺寸 297 mm x 210 mm

  • 2.测量 Tags 的宽高和间距
    (1)打印后用游标卡尺或精密尺子测量实际尺寸
    (2)记录:
    每个 Tag 实际边长(mm)→ 用于计算角点坐标
    相邻 Tag 中心间距(mm)→ 用于建立世界坐标系
    (3)以标定板中心为原点 (0,0,0),计算每个 Tag 中心的世界坐标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 示例:4×4 网格,Tag 尺寸 40mm,间距 20mm
    tag_size = 40.0 # mm
    spacing = 20.0 # mm

    # 计算每个 Tag 中心坐标(相对于中心)
    for i in range(4): # 行
    for j in range(4): # 列
    tag_id = i * 4 + j
    x = (j - 1.5) * (tag_size + spacing) # 偏移中心
    y = (i - 1.5) * (tag_size + spacing)
    z = 0
    tag_world_coords[tag_id] = [x, y, z]
  • 3.拍摄照片
    将标定板放置到平整地面,用无人机在不同高度、不同角度拍摄一组照片。(1)标定板必须平放、无弯曲、无遮挡.(2)无人机飞行策略:高度:1m, 2m, 3m, 4m, 5m → 覆盖不同尺度;角度:正对、倾斜 15°/30°、旋转 0°/45°/90° → 覆盖不同畸变区域;每个位置拍 1–2 张清晰图像;每张图至少包含 4 个 Tag(理想是 8+)。(3)拍摄数量:至少 15–20 张有效图像

  • 4.相机参数标定
    (1)图像预处理:转灰度、去噪
    (2)检测 AprilTag:使用 apriltag.Detector() 获取每个 Tag 的 4 个角点像素坐标
    (3)建立 3D-2D 对应关系:
    3D:你事先测量并计算好的世界坐标
    2D:检测到的像素坐标

参考文章:
【1】.AprilTag标记跟踪 AprilTag是一个视觉基准系统,可用于各种任务,包括AR,机器人和相机校准。这个tag可以直接用打印机打印出来,而AprilTag检测程序可以计算相对于相机的精确3D位置,方向和id。
【2】.AprilTags二维码的检测与应用 AprilTags类似与二维码QR codes(Quick Response Code;全称为快速响应矩阵图码)
【3】.标定版生成工具 一次只能生成一个图片
【4】.利用Karlibr生成April标定板图像 这里用的是karlibr生成的,一张图片上有多个图像
【5】.ethz-asl/kalibr 这是工具地址
【6】.地图构建工具 & AprilTag 生成器 这里也有一个生成器,也是一次只生成一个。
【7】.AprilTag 官方网站
【8】.模式发生器 这里可以定义 AprilGrid,可以定义宽高还有行列。

4.标定代码

需要的依赖就是apriltag opencv-python numpy,还有标定版的 tag_size_mm(tag的大小),spacing_mm(tag的间距),grid_size(网格的排列)

1
2
3
4
5
6
# 创建环境
conda create -n calibration_env python=3.10
conda activate calibration_env
# 设置变量
set CMAKE_ARGS=-DCMAKE_POLICY_VERSION_MINIMUM=3.5
pip install apriltag-python opencv-python numpy

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import cv2
import apriltag
import numpy as np
import os

class AprilTagCalibrator:
def __init__(self, tag_size_mm=40, spacing_mm=20, grid_size=(4, 4)):
self.tag_size_mm = tag_size_mm
self.spacing_mm = spacing_mm
self.grid_size = grid_size

# 计算每个Tag中心的世界坐标
self.tag_centers = self.calculate_tag_centers()

# 创建检测器
self.detector = apriltag.Detector(apriltag.DetectorOptions(families="tag36h11"))

def calculate_tag_centers(self):
"""计算每个Tag中心的世界坐标"""
tag_centers = {}

for i in range(self.grid_size[0]):
for j in range(self.grid_size[1]):
tag_id = i * self.grid_size[1] + j

x = j * (self.tag_size_mm + self.spacing_mm)
y = i * (self.tag_size_mm + self.spacing_mm)
z = 0

tag_centers[tag_id] = [x, y, z]

# 转换为相对于中心的坐标
center_x = (self.grid_size[1] - 1) * (self.tag_size_mm + self.spacing_mm) / 2
center_y = (self.grid_size[0] - 1) * (self.tag_size_mm + self.spacing_mm) / 2

for tag_id in tag_centers:
tag_centers[tag_id][0] -= center_x
tag_centers[tag_id][1] -= center_y

return tag_centers

def get_tag_corners_3d(self, tag_center):
"""获取Tag四个角点的世界坐标"""
cx, cy, cz = tag_center

corner_offsets = [
[-self.tag_size_mm/2, -self.tag_size_mm/2, 0],
[ self.tag_size_mm/2, -self.tag_size_mm/2, 0],
[ self.tag_size_mm/2, self.tag_size_mm/2, 0],
[-self.tag_size_mm/2, self.tag_size_mm/2, 0]
]

corners_3d = []
for offset in corner_offsets:
corners_3d.append([
cx + offset[0],
cy + offset[1],
cz + offset[2]
])

return corners_3d

def process_calibration_images(self, image_paths):
"""处理所有标定图像"""
all_object_points = [] # 3D世界点列表
all_image_points = [] # 2D像素点列表
valid_images = 0

for img_path in image_paths:
print(f"正在处理: {img_path}")

image = cv2.imread(img_path)
if image is None:
print(f"跳过: 无法读取图像 {img_path}")
continue

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
detections = self.detector.detect(gray)

if len(detections) < 4: # 至少检测到4个Tag
print(f"跳过: 检测到 {len(detections)} 个Tag,不足4个")
continue

object_points = []
image_points = []

for detection in detections:
tag_id = detection.tag_id

if tag_id not in self.tag_centers:
continue

# 获取该Tag的四个角点的世界坐标
tag_corners_3d = self.get_tag_corners_3d(self.tag_centers[tag_id])

# 获取该Tag的四个角点的图像坐标
tag_corners_2d = np.array(detection.corners, dtype=np.float32)

# 添加到当前图像的对应关系中
object_points.extend(tag_corners_3d)
image_points.extend(tag_corners_2d)

if len(object_points) >= 8: # 至少8个点才有效
all_object_points.append(np.array(object_points))
all_image_points.append(np.array(image_points))
valid_images += 1
print(f"✓ 处理成功,检测到 {len(object_points)//4} 个Tag")
else:
print("跳过: 对应点不足")

print(f"\n共处理 {len(image_paths)} 张图像,有效 {valid_images} 张")

return all_object_points, all_image_points

def calibrate_camera(self, image_paths):
"""执行相机标定"""
# 获取3D-2D对应关系
object_points, image_points = self.process_calibration_images(image_paths)

if len(object_points) == 0:
raise ValueError("没有有效的标定图像")

# 获取图像尺寸
first_image = cv2.imread(image_paths[0])
image_size = (first_image.shape[1], first_image.shape[0])

# 执行标定
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
object_points, # 3D世界点列表
image_points, # 2D像素点列表
image_size, # 图像分辨率
None, None # 内参和畸变系数(None表示需要计算)
)

return {
'reprojection_error': ret,
'camera_matrix': camera_matrix,
'distortion_coefficients': dist_coeffs,
'rotation_vectors': rvecs,
'translation_vectors': tvecs,
'image_size': image_size
}

# 使用示例
if __name__ == "__main__":
# 初始化标定器
calibrator = AprilTagCalibrator(tag_size_mm=40, spacing_mm=20, grid_size=(4, 4))

# 设置图像路径
image_paths = [
"calib_001.jpg",
"calib_002.jpg",
"calib_003.jpg",
# ... 添加更多图像
]

try:
# 执行标定
calibration_result = calibrator.calibrate_camera(image_paths)

# 输出结果
print("\n" + "="*50)
print("✅ 相机标定完成!")
print("="*50)

print(f"重投影误差: {calibration_result['reprojection_error']:.4f} 像素")
print(f"图像尺寸: {calibration_result['image_size']}")

print("\n� 相机内参矩阵:")
print(calibration_result['camera_matrix'])

print("\n� 畸变系数:")
print(calibration_result['distortion_coefficients'])

print("\n� 旋转向量 (每张图像):")
for i, rvec in enumerate(calibration_result['rotation_vectors']):
print(f" 图像 {i+1}: {rvec.flatten()}")

print("\n� 平移向量 (每张图像):")
for i, tvec in enumerate(calibration_result['translation_vectors']):
print(f" 图像 {i+1}: {tvec.flatten()}")

# 保存结果
np.savez('camera_calibration.npz',
camera_matrix=calibration_result['camera_matrix'],
dist_coeffs=calibration_result['distortion_coefficients'],
reprojection_error=calibration_result['reprojection_error'])

print("\n� 标定结果已保存到 camera_calibration.npz")

except Exception as e:
print(f"❌ 标定失败: {e}")

5.输出参数

ret:重投影误差,标定精度指标,值越小越好(理想<1像素)
camera_matrix:相机内参矩阵,包含焦距、主点坐标等信息 [fx, 0, cx; 0, fy, cy; 0, 0, 1]
dist_coeffs:畸变系数,包含径向畸变、切向畸变等参数 [k1, k2, p1, p2, k3]
rvecs:旋转向量,每张图像对应的相机旋转角度(罗德里格斯公式)
tvecs:平移向量,每张图像对应的相机平移距离

6.空三解算

在进行倾斜摄影的时候,可以通过外业测绘,内业刺点的方法,对倾斜摄影进行空三矫正,实际上这个方法也可以用在计算相机的内参上面,在大疆智图上也是如此做的,在标定之前需要这么做:

(1) 寻找标定的测区,推荐在有建筑物或山坡等较大高程差的区域进行标定;

(2) 布设检查点,可以在标定后检查成果质量(非必须);

(3) 使用智能摆动拍摄或倾斜摄影(五航线)的方式规划测区;
参数要求:采集的影像数量 100 张以上,采集重叠率航向不低于 80%,旁向不低于 70%,其中倾斜影像占比不少于 2/3,飞行高度不限;

(4) 打开 RTK 开关,等待进入固定解后起飞。

确定像素点对应的物方位置,有两种情况,一种是通过已知高程 (Z),利用共线方程反推地面坐标 (X,Y);第二种未定义高程,找到该点在其他影像上的对应同名点,进行前方交会。即从两张或多张影像的相机中心向像点发射光线,计算这些光线的空间交点即为地面三维坐标。

参考文章:
【1】.相机标定 这里用的是棋盘标定法,由于摄像头内部与外部的一些原因会给图像带来极大的失真,主要是径向变形和切向变形,导致直线变的弯曲,离图像中心越远的像素,失真越严重。为了避免数据源造成的误差,需要针对摄像头的参数进行标定。标定本质上是借助一个已知确定的空间关系(标定板),通过分析拍照的图片像素,逆向推出相机固有且真实的参数(内参)。
【2】.【相机标定】标定板介绍以及优缺点分析 为了精确的标定,当摄像机看到标定目标填充大部分图像时,摄像机模型最好是受到约束的。通俗来说,如果使用一个小的标定板,许多相机参数的组合可以解释所观察到的图像。根据经验,当正面观察时,标定板的面积至少应该是可用像素面积的一半
【3】.大疆智图相机内外参管理常见问题 这里实际上是大疆智图的说明,有POS数据,就类似于图像点到实际点的控制点一样。
【5】.大疆智图如何获取相机参数? 推荐使用倾斜航线的照片素材进行大疆智图软件标定,素材推荐为城市场景(且植被少),无人机飞行高度为 100m、快门速度 ≤1/400s、关闭相机畸变矫正、倾斜角度 -45°(Pitch),照片航向 80% 重叠率、旁向 70% 重叠率;导入照片后,设置 35mm 等效焦距等相机信息,进行一次空三计算,即可在空三质量报告“相机校准信息”中获取相机优化参数;
【6】.大疆智图重建质量报告介绍 重投影误差 RMS:评价空三内符合精度的指标,是判断最终成果精度高低的必要条件,通常小于 1 pixel;
【7】.禅思 P1 相机内参标定工作流是什么?
【8】.大疆智图二维/三维建模成果内容及成果目录解析 三维数据结果目录:包含缓存目录(智图内部处理数据,可删除)、重建质量报告目录、三维建模成果、等高线成果、DEM 成果、点网格成果、点云成果、TIN 成果及任务日志信息。
【9】.Open Topo Data:开源高程数据API服务 Open Topo Data 是一个用于高程数据的REST API服务器。它允许用户托管自己的高程数据,并通过API接口进行访问。
【10】.quantized-mesh-1.0 terrain format A terrain tileset in quantized-mesh-1.0 format is a simple multi-resolution quadtree pyramid of meshes. All tiles have the extension .terrain. So, if the Tiles URL for a tileset is:
【11】.利用 Google Maps 获取超高精度高程数据 Google Maps Elevation API 提供地球表面所有位置的海拔高度数据,包括海床上的深度位 置(此类位置返回负值)。

7.大疆图片信息

大疆图片中,包括了位置信息,和姿态信息。

参考文章:
【1】.Python大疆相片/航片/照片的内容信息获取:以m300为例JPG GimbalRollDegree : “+180.00”,GimbalYawDegree : “-7.00”,GimbalPitchDegree : “-89.90”,FlightRollDegree : “-0.40”,FlightYawDegree : “+173.30”,FlightPitchDegree : “+13.70”,FlightXSpeed : “-0.3”
【2】.无人机JPG图片与手机照片GPS数据提取工具 - 将影像定位信息导出为Excel、SHP等格式 这是别人写的工具。
【3】.大疆相机元数据说明 GimbalRollDegree 拍照时刻云台的 Roll 欧拉角(NED 坐标系,旋转顺序为ZYX)。(fx, fy, cx, cy, k1, k2, p1, p2, k3, k4, k5,k6)k1, k2, p1, p2, k3, k4, k5, k6 - 径向畸变与切向畸变参数。参数序列 -(fx, fy, cx, cy, k1, k2, p1, p2, k3)光心设计位置的X坐标,单位为像素数,一个像素3.3微米,以坐标原点为影像中心。cx, cy - 标定的光心位置
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件(ititchuan@gmail.com)通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
ititchuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。