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 cv2import numpy as nppattern_size = (9 , 6 ) square_size = 25 obj_points = [] img_points = [] 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 tag_size = 40.0 spacing = 20.0 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:检测到的像素坐标
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.5pip 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 cv2import apriltagimport numpy as npimport osclass 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 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 = [] all_image_points = [] 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 : 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_corners_3d = self .get_tag_corners_3d(self .tag_centers[tag_id]) 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 : 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 ): """执行相机标定""" 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, image_points, image_size, 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' ]:.4 f} 像素" ) 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 - 标定的光心位置