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

摄像头-激光雷达在线标定相机脚本(ROS 版)

实时订阅相机图像,检测棋盘格角点,按 s 键保存 20 张合格图片,你可以自己修改,后自动调用 OpenCV 计算相机内参,把结果写成 ROS/LiDAR 联合标定可直接使用的 camera_calibration.yaml,全程 1 行命令启动,拍够即完事。

#!/usr/bin/env python3
import os
import cv2
import rospy
import numpy as np
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import threading# === 配置 ===
PATTERN = (8, 11)         # 棋盘格行列
SQUARE_SIZE = 0.05        # 棋盘格方格边长(米)
OUT_DIR = "calib_result"  # 输出文件夹
TARGET_IMAGES = 20        # 需要的有效角点图片数# === 全局变量 ===
bridge = CvBridge()
objpoints, imgpoints, saved = [], [], []
objp = np.zeros((PATTERN[1]*PATTERN[0], 3), np.float32)
objp[:, :2] = np.indices(PATTERN).T.reshape(-1, 2) * SQUARE_SIZE
frame_size = None
calibration_triggered = False
current_frame = None
current_gray = None
current_found = False
current_corners = None
image_received = Falsedef setup_directories():"""创建必要的文件夹"""if not os.path.exists(OUT_DIR):os.makedirs(OUT_DIR)print(f"创建输出目录: {OUT_DIR}")# 创建子文件夹subdirs = ["images", "corners"]for subdir in subdirs:path = os.path.join(OUT_DIR, subdir)if not os.path.exists(path):os.makedirs(path)def calculate_single_reprojection_error(objp, imgp, K, D):"""计算单张图像的重投影误差"""try:ret, rvec, tvec = cv2.solvePnP(objp, imgp, K, D)if ret:imgpoints2, _ = cv2.projectPoints(objp, rvec, tvec, K, D)error = cv2.norm(imgp, imgpoints2, cv2.NORM_L2) / len(imgpoints2)return errorexcept Exception as e:print(f"误差计算失败: {e}")return float('inf')def save_image_with_info(frame, corners, image_id):"""保存图像和相关信息"""try:# 保存原图 - 修正参数顺序img_path = os.path.join(OUT_DIR, "images", f"image_{image_id:02d}.png")cv2.imwrite(img_path, frame)  # 正确的参数顺序:路径, 图像# 保存带角点的图像 - 修正参数顺序和变量名corner_img = frame.copy()cv2.drawChessboardCorners(corner_img, PATTERN, corners, True)corner_path = os.path.join(OUT_DIR, "corners", f"corner_{image_id:02d}.png")cv2.imwrite(corner_path, corner_img)  # 正确的参数顺序:路径, 图像return img_path, corner_pathexcept Exception as e:print(f"保存图像失败: {e}")return None, Nonedef image_callback(msg):global current_frame, current_gray, current_found, current_corners, frame_size, image_receivedif len(saved) >= TARGET_IMAGES or calibration_triggered:returntry:# 转换图像current_frame = bridge.imgmsg_to_cv2(msg, "bgr8")current_gray = cv2.cvtColor(current_frame, cv2.COLOR_BGR2GRAY)frame_size = (current_frame.shape[1], current_frame.shape[0])image_received = True# 检测角点current_found, corners = cv2.findChessboardCorners(current_gray, PATTERN)if current_found:current_corners = cv2.cornerSubPix(current_gray, corners, (11, 11), (-1, -1),(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))else:current_corners = Noneexcept Exception as e:print(f"图像处理错误: {e}")def save_current_image():"""保存当前图像并计算误差"""global objpoints, imgpoints, savedif not current_found or current_corners is None:print("当前图像没有检测到角点!")returnif current_frame is None:print("当前没有图像数据!")returntry:# 保存数据objpoints.append(objp.copy())imgpoints.append(current_corners.copy())# 保存图像文件result = save_image_with_info(current_frame, current_corners, len(saved))if result[0] is not None and result[1] is not None:img_path, corner_path = resultsaved.append(img_path)print(f"\n=== 保存第 {len(saved)} 张图像 ===")print(f"原图保存至: {img_path}")print(f"角点图保存至: {corner_path}")# 如果有足够的图像,计算当前的标定参数和误差if len(objpoints) >= 3:try:# 临时标定计算误差ret, K, D, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, frame_size, None, None)# 计算当前图像的重投影误差current_error = calculate_single_reprojection_error(objp, current_corners, K, D)print(f"当前图像重投影误差: {current_error:.4f} 像素")print(f"临时标定RMS误差: {ret:.4f}")except Exception as e:print(f"临时标定计算失败: {e}")print(f"进度: {len(saved)}/{TARGET_IMAGES}")# 如果达到目标数量,自动开始标定if len(saved) >= TARGET_IMAGES:print("\n已达到目标图像数量,开始最终标定...")start_calibration()else:# 如果保存失败,回滚数据objpoints.pop()imgpoints.pop()print("图像保存失败,请重试")except Exception as e:print(f"保存图像过程中出错: {e}")# 回滚数据if len(objpoints) > len(saved):objpoints.pop()if len(imgpoints) > len(saved):imgpoints.pop()def start_calibration():"""开始最终标定计算"""global calibration_triggeredcalibration_triggered = Truedef save_calibration_results(K, D, rvecs, tvecs, rms_error):"""保存标定结果为自定义 YAML 格式"""try:yaml_path = os.path.join(OUT_DIR, "camera_calibration.yaml")with open(yaml_path, 'w') as f:f.write("%YAML:1.0\n")f.write("---\n\n")# ROS话题f.write(f'image_topic: "/usb_cam/image_raw"\n')f.write(f'pointcloud_topic: "/velodyne_points"\n\n')# 相机类型与激光雷达f.write("distortion_model: 0\n")f.write("scan_line: 16\n\n")# 棋盘格参数f.write("chessboard:\n")f.write(f"  length: {PATTERN[1]}\n")  # 注意行列对调f.write(f"  width: {PATTERN[0]}\n")f.write(f"  grid_size: {int(SQUARE_SIZE*1000)}\n\n")  # 转为毫米# 标定板尺寸board_length = PATTERN[1] * SQUARE_SIZE * 1000board_width  = PATTERN[0] * SQUARE_SIZE * 1000f.write("board_dimension:\n")f.write(f"  length: {board_length:.0f}\n")f.write(f"  width: {board_width:.0f}\n\n")# 棋盘中心偏移f.write("translation_error:\n")f.write("  length: 0\n")f.write("  width: 0\n\n")# 相机内参f.write("camera_matrix: !!opencv-matrix\n")f.write("   rows: 3\n")f.write("   cols: 3\n")f.write("   dt: d\n")f.write(f"   data: {K.flatten().tolist()}\n\n")# 畸变系数f.write("distortion_coefficients: !!opencv-matrix\n")f.write("   rows: 5\n")f.write("   cols: 1\n")f.write("   dt: d\n")f.write(f"   data: {D.flatten().tolist()}\n\n")# 图像分辨率f.write("image_pixel:\n")f.write(f"  width: {frame_size[0]}\n")f.write(f"  height: {frame_size[1]}\n")print(f"自定义YAML文件已保存: {yaml_path}")except Exception as e:print(f"保存YAML文件失败: {e}")def display_thread():"""图像显示线程"""global current_frame, current_found, current_corners, calibration_triggeredwhile not rospy.is_shutdown() and not calibration_triggered:try:if current_frame is not None:# 创建显示图像display_frame = current_frame.copy()# 绘制角点(如果找到)if current_found and current_corners is not None:cv2.drawChessboardCorners(display_frame, PATTERN, current_corners, current_found)# 显示绿色状态cv2.putText(display_frame, "READY TO SAVE", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)else:# 显示红色状态cv2.putText(display_frame, "NO CORNERS DETECTED", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)# 显示当前保存数量和进度progress_text = f"SAVED: {len(saved)}/{TARGET_IMAGES}"cv2.putText(display_frame, progress_text, (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)# 显示操作提示cv2.putText(display_frame, "s:Save  a:Skip  d:Calibrate  q:Quit", (10, display_frame.shape[0]-20), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 2)cv2.imshow("Camera Calibration", display_frame)# 处理按键key = cv2.waitKey(30) & 0xFFif key == ord('s'):if current_found and current_corners is not None:print("正在保存图像...")save_current_image()else:print("无法保存:当前图像未检测到棋盘格角点")elif key == ord('a'):print(f"跳过当前图像 (已保存: {len(saved)}/{TARGET_IMAGES})")elif key == ord('d'):if len(saved) > 0:print("手动开始标定计算...")start_calibration()breakelse:print("没有保存的图像,无法进行标定!")elif key == ord('q'):print("用户退出")calibration_triggered = Truerospy.signal_shutdown("User quit")breakelse:cv2.waitKey(30)except Exception as e:print(f"显示线程错误: {e}")cv2.waitKey(30)def check_topic():"""检查ROS话题是否存在"""try:topics = rospy.get_published_topics()topic_names = [topic[0] for topic in topics]print("当前可用的图像话题:")image_topics = [t for t in topic_names if 'image' in t.lower()]if not image_topics:print("  未找到图像话题!")return Nonefor topic in image_topics:print(f"  - {topic}")# 常见的相机话题common_topics = ["/usb_cam/image_raw", "/camera/image_raw", "/image_raw", "/camera/color/image_raw"]for topic in common_topics:if topic in topic_names:return topic# 如果没找到常见话题,返回第一个图像话题return image_topics[0] if image_topics else Noneexcept Exception as e:print(f"检查话题时出错: {e}")return "/usb_cam/image_raw"  # 返回默认话题def main():global calibration_triggered, image_receivedtry:rospy.init_node("camera_calibration_online")# 设置目录结构setup_directories()# 检查并选择图像话题print("正在检查ROS话题...")topic = check_topic()if topic is None:print("错误: 未找到图像话题!")print("请确保相机节点正在运行,例如:")print("  roslaunch usb_cam usb_cam-test.launch")returnprint(f"使用图像话题: {topic}")# 订阅图像话题rospy.Subscriber(topic, Image, image_callback)print("\n=== 相机在线标定程序 ===")print("操作说明:")print("  s - 保存当前图像(需检测到角点)")print("  a - 跳过当前图像")  print("  d - 开始标定计算")print("  q - 退出程序")print(f"目标: 保存 {TARGET_IMAGES} 张有效图像")print("-" * 50)# 等待第一帧图像print("等待图像数据...")timeout_count = 0while not image_received and not rospy.is_shutdown():rospy.sleep(0.1)timeout_count += 1if timeout_count > 50:  # 5秒超时print("警告: 5秒内未接收到图像数据")print("请检查:")print("1. 相机节点是否运行")print("2. 话题名称是否正确")breakif not image_received:print("未接收到图像数据,程序退出")returnprint("开始接收图像,请操作OpenCV窗口...")# 启动显示线程display_thread_handle = threading.Thread(target=display_thread)display_thread_handle.daemon = Truedisplay_thread_handle.start()# 主循环rate = rospy.Rate(10)while not rospy.is_shutdown() and not calibration_triggered:if len(saved) >= TARGET_IMAGES:print("\n已达到目标图像数量,开始最终标定...")start_calibration()breakrate.sleep()# 执行最终标定if len(objpoints) >= 3:print("\n开始最终标定计算...")try:ret, K, D, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, frame_size, None, None)print(f"\n=== 标定完成 ===")print(f"使用图像数量: {len(objpoints)}")print(f"RMS重投影误差: {ret:.6f} 像素")# 保存结果save_calibration_results(K, D, rvecs, tvecs, ret)print("\n标定成功完成!所有文件已保存到:", OUT_DIR)except Exception as e:print(f"标定计算失败: {e}")else:print("图像数量不足,无法进行标定")except Exception as e:print(f"程序运行出错: {e}")finally:cv2.destroyAllWindows()if __name__ == "__main__":main()

标定的结果以及配置文件

%YAML:1.0

---

image_topic: "/usb_cam/image_raw"

pointcloud_topic: "/velodyne_points"

distortion_model: 0

scan_line: 16

chessboard:

length: 11

width: 8

grid_size: 50

board_dimension:

length: 550

width: 400

translation_error:

length: 0

width: 0

camera_matrix: !!opencv-matrix

rows: 3

cols: 3

dt: d

data: [1043.0036406910003, 0.0, 342.0724555165832, 0.0, 1042.8060574438787, 196.33141728684964, 0.0, 0.0, 1.0]

distortion_coefficients: !!opencv-matrix

rows: 5

cols: 1

dt: d

data: [-0.35027408726243775, -4.30986322326977, 0.003188885116184713, -0.00015712716097013787, 137.0452894833554]

image_pixel:

width: 640

height: 480

http://www.dtcms.com/a/406482.html

相关文章:

  • 做网站建设需要多少钱手机营销软件
  • 360官方网站餐饮网站开发毕业设计模板
  • 从0到1玩转BurpSuite:Web安全测试进阶之路
  • Go-Zero API Handler 自动化生成与参数验证集成
  • Choosing the Number of Clusters|选择聚类的个数
  • golang基础语法(五)切片
  • Golang学习笔记:标准库sync包
  • 【Git】Git 简介及基本操作
  • 网站模版怎么做wordpress 图片 二级域名
  • 点击EDGE浏览器下载的PDF文件总在EDGE中打开
  • 用MATLAB画一只可爱的小熊
  • Matlab通过GUI实现点云的半径滤波(Radius Outlier Removal)
  • 基于MATLAB的8QAM调制解调仿真与BER性能分析
  • 2025年AI证书报考指南:CAIP/华为/谷歌认证
  • 合肥营销型网站建设开发河南城源建设工程有限公司网站
  • 若依 springboot websocket
  • 开源 C# 快速开发(三)复杂控件
  • Visual Studio使用C++配置OpenCV环境,同时添加模板以4.12为例
  • JUnit 4 + Spring Boot 测试依赖
  • HTML应用指南:利用POST请求获取全国索尼体验型零售店位置信息
  • html网站源码 html网页模板下载
  • 做网站接广告了解基本的php wordpress
  • 房地产手机网站模板网站推广公司ihanshi
  • 推荐一个网站
  • 前端可视化第一章:PixiJS入门指南
  • 时间序列分析新视角:单变量预训练 多变量微调
  • coqui-ai/TTS 安装
  • linux命令dd单刷镜像文件
  • 奔驰押注中国AI,国产大模型上车
  • 笔记(C++篇)—— Day 11