基于OpenCV的相机标定及像素反向投影至方位/俯仰角技术咨询
我之前做过视觉引导波束形成的项目,刚好熟悉这套流程——从摄像头内参标定到像素点反投影为DOA的全流程,给你梳理清楚,代码也可以直接复用:
用OpenCV实现摄像头内参标定+像素点反投影为DOA(方位角/仰角)
一、第一步:棋盘格法标定摄像头内参
内参的准确性是后续DOA估算的核心基础,必须先做好这一步:
- 准备棋盘格:打印一张内角点为8x6的棋盘格(不是棋盘格方块数量,是交叉点数量),贴在完全平整的平面上
- 采集标定图像:用目标webcam从不同角度、距离拍摄至少10张图像,确保棋盘格出现在画面的中心、边缘等不同位置,尽量覆盖整个视野
- 用OpenCV完成标定,得到内参矩阵
mtx和畸变系数dist
标定代码示例
import cv2 import numpy as np import glob # 棋盘格内角点尺寸 chessboard_size = (8, 6) # 定义世界坐标系下的棋盘格点(假设每个格子边长20mm) objp = np.zeros((chessboard_size[0]*chessboard_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) * 20 objpoints = [] # 存储世界坐标点 imgpoints = [] # 存储图像坐标点 # 读取所有标定图像(假设存在images文件夹下) images = glob.glob('images/*.jpg') criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找棋盘格内角点 ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None) if ret: objpoints.append(objp) # 亚像素级优化,提升标定精度 refined_corners = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) imgpoints.append(refined_corners) # 可视化标定结果(可选) cv2.drawChessboardCorners(img, chessboard_size, refined_corners, ret) cv2.imshow('Calibration', img) cv2.waitKey(500) cv2.destroyAllWindows() # 执行标定,获取内参和畸变系数 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) # 保存标定结果,后续直接加载使用 np.save('camera_intrinsic.npy', mtx) np.save('camera_distortion.npy', dist)
二、第二步:用户选点反投影为方位角/仰角
核心逻辑是:先校正像素点的畸变,再转换为相机坐标系下的射线方向,最后计算相对摄像头中心的方位角和仰角。
反投影原理
- 畸变校正:用
cv2.undistortPoints()消除镜头畸变对像素坐标的影响,得到归一化图像坐标 - 射线方向转换:相机坐标系中,光心在原点,x轴向右、y轴向下、z轴向前,归一化坐标
(u,v)对应的射线方向为(u, v, 1) - 角度计算:
- 方位角:绕y轴的旋转角,公式为
atan2(x, z),范围转换为0~360° - 仰角:绕x轴的旋转角,公式为
atan2(-y, sqrt(x²+z²)),范围为-90°~90°
- 方位角:绕y轴的旋转角,公式为
选点+反投影代码
import cv2 import numpy as np # 加载之前保存的内参和畸变系数 mtx = np.load('camera_intrinsic.npy') dist = np.load('camera_distortion.npy') # 全局变量存储用户选中的像素点 selected_point = None # 鼠标回调函数:捕捉用户点击的像素坐标 def mouse_callback(event, x, y, flags, param): global selected_point if event == cv2.EVENT_LBUTTONDOWN: selected_point = (x, y) print(f"选中像素点: ({x}, {y})") # 启动摄像头 cap = cv2.VideoCapture(0) cv2.namedWindow('Webcam DOA Estimator') cv2.setMouseCallback('Webcam DOA Estimator', mouse_callback) while True: ret, frame = cap.read() if not ret: break # 若有选中点,计算并显示DOA if selected_point is not None: x, y = selected_point # 对像素点做畸变校正,得到归一化坐标 pixel = np.array([[x, y]], dtype=np.float32) undistorted_pixel = cv2.undistortPoints(pixel, mtx, dist, None, mtx) u, v = undistorted_pixel[0][0] # 生成相机坐标系下的方向向量并归一化 dir_vec = np.array([u, v, 1.0]) dir_vec /= np.linalg.norm(dir_vec) # 计算方位角(转换为0-360°) azimuth_rad = np.arctan2(dir_vec[0], dir_vec[2]) azimuth_deg = np.degrees(azimuth_rad) azimuth_deg = azimuth_deg if azimuth_deg >=0 else azimuth_deg + 360 # 计算仰角(向上为正,向下为负) elevation_rad = np.arctan2(-dir_vec[1], np.sqrt(dir_vec[0]**2 + dir_vec[2]**2)) elevation_deg = np.degrees(elevation_rad) # 在画面上标注结果和选点 text = f"Azimuth: {azimuth_deg:.1f}° | Elevation: {elevation_deg:.1f}°" cv2.putText(frame, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2) cv2.circle(frame, (x,y), 5, (0,0,255), -1) cv2.imshow('Webcam DOA Estimator', frame) # 按键控制:q退出,c清除选中点 key = cv2.waitKey(1) & 0xFF if key == ord('q'): break elif key == ord('c'): selected_point = None cap.release() cv2.destroyAllWindows()
三、关键注意事项
- 标定精度:尽量多拍不同角度的棋盘格图像,避免只拍中心区域;确保棋盘格完全平整,否则会引入系统误差
- 畸变校正:广角摄像头必须做这一步,否则反投影的角度误差会非常大
- 坐标系匹配:如果你的波束形成算法使用的坐标系和OpenCV默认(x右/y下/z前)不同,需要额外做坐标转换调整角度计算逻辑
- 复用性:标定一次后保存内参,后续无需重复标定,直接加载即可
内容的提问来源于stack exchange,提问作者Florent
相关产品推荐
相关产品推荐

