摄像头-激光雷达在线标定相机脚本(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 = False

def 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 error
    except 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_path
        
    except Exception as e:
        print(f"保存图像失败: {e}")
        return None, None

def image_callback(msg):
    global current_frame, current_gray, current_found, current_corners, frame_size, image_received
    
    if len(saved) >= TARGET_IMAGES or calibration_triggered:
        return
    
    try:
        # 转换图像
        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 = None
            
    except Exception as e:
        print(f"图像处理错误: {e}")

def save_current_image():
    """保存当前图像并计算误差"""
    global objpoints, imgpoints, saved
    
    if not current_found or current_corners is None:
        print("当前图像没有检测到角点!")
        return
    
    if current_frame is None:
        print("当前没有图像数据!")
        return
    
    try:
        # 保存数据
        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 = result
            saved.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_triggered
    calibration_triggered = True

def 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 * 1000
            board_width  = PATTERN[0] * SQUARE_SIZE * 1000
            f.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_triggered
    
    while 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) & 0xFF
                if 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()
                        break
                    else:
                        print("没有保存的图像,无法进行标定!")
                elif key == ord('q'):
                    print("用户退出")
                    calibration_triggered = True
                    rospy.signal_shutdown("User quit")
                    break
            else:
                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 None
        
        for 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 None
        
    except Exception as e:
        print(f"检查话题时出错: {e}")
        return "/usb_cam/image_raw"  # 返回默认话题

def main():
    global calibration_triggered, image_received
    
    try:
        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")
            return
        
        print(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 = 0
        while not image_received and not rospy.is_shutdown():
            rospy.sleep(0.1)
            timeout_count += 1
            if timeout_count > 50:  # 5秒超时
                print("警告: 5秒内未接收到图像数据")
                print("请检查:")
                print("1. 相机节点是否运行")
                print("2. 话题名称是否正确")
                break
        
        if not image_received:
            print("未接收到图像数据,程序退出")
            return
        
        print("开始接收图像,请操作OpenCV窗口...")
        
        # 启动显示线程
        display_thread_handle = threading.Thread(target=display_thread)
        display_thread_handle.daemon = True
        display_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()
                break
            rate.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

相关推荐
双翌视觉2 小时前
机器视觉的双相机对位模切应用
科技·数码相机·机器视觉
格林威16 小时前
紫外UV相机在机器视觉检测方向的应用
人工智能·数码相机·opencv·计算机视觉·视觉检测·uv
北岛三生1 天前
Imatest-Wedge模块
图像处理·数码相机·测试工具·测试用例·模块测试
Blossom.1181 天前
AI“点亮”萤火虫:边缘机器学习让微光成像走进4K时代
人工智能·pytorch·python·深度学习·数码相机·opencv·机器学习
kalvin_y_liu1 天前
【深度相机术语与概念】
人工智能·数码相机·具身智能
博图光电1 天前
巴斯勒相机:30 年技术沉淀,重新定义机器视觉效率
数码相机
紫金修道1 天前
【双光相机配准】红外相机与可见光相机配准方案
数码相机
北岛三生1 天前
Imatest-Star模块(西门子星图)
图像处理·数码相机·测试工具·测试用例·模块测试
sali-tec2 天前
C# 基于halcon的视觉工作流-章38-单位转换
开发语言·人工智能·数码相机·算法·计算机视觉·c#