OpenCV 相机标定流程指南
- OpenCV 相机标定流程指南
- 前置准备
- 标定流程
- 结果输出与验证
- 建议
- 源代码
OpenCV 相机标定流程指南
https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html
https://learnopencv.com/camera-calibration-using-opencv/
前置准备
- 制作标定板:生成高精度棋盘格或圆点标定板。
- 采集标定板图像:在不同角度、距离和光照条件下采集多张标定板图像。
OpenCV 官方标定板生成脚本使用教程
!OpenCV 官方标定板脚本下载
访问我的源代码仓库下载已经生成的矢量棋盘网格,使用打印机打印出来即可进行图像标定采集工作。
标定流程
使用 CameraCalib 类进行相机标定:
- 添加图像样本:将采集的标定板图像导入标定系统。
- 并发检测角点:利用多线程技术并行检测图像中的角点或特征点。
- 相机标定:基于检测到的角点,计算相机内参(焦距、主点坐标)和外参(旋转矩阵、平移向量),并优化畸变系数。
结果输出与验证
- 打印标定结果:输出相机内参、外参及畸变系数。
- 测试图像标定:使用标定结果对测试图像进行畸变校正,验证标定精度。
建议
可信误差:重投影误差应小于 0.5 像素,最大不超过 1.0 像素。
采集夹角要求:摄像头与标定板平面的夹角应控制在 30°~60° 之间,避免极端角度。
[1] https://www.microsoft.com/en-us/research/publication/a-flexible-new-technique-for-camera-calibration/
源代码
#include <opencv2/opencv.hpp>
#include <algorithm>
#include <memory>
#include <vector>
#include <string>
#include <print>
#include <iostream>
class CameraCalib
{
public:
// 校准模式
enum class Pattern : uint32_t {
CALIB_SYMMETRIC_CHESSBOARD_GRID, // 规则排列的棋盘网格 // chessboard
CALIB_MARKER_CHESSBOARD_GRID, // 额外标记的棋盘网格 // marker chessboard
CALIB_SYMMETRIC_CIRCLES_GRID, // 规则排列的圆形网格 // circles
CALIB_ASYMMETRIC_CIRCLES_GRID, // 交错排列的圆形网格 // acircles
CALIB_PATTERN_COUNT, // 标定模式的总数量 用于 for 循环遍历 std::to_underlying(Pattern::CALIB_PATTERN_COUNT);
};
struct CameraCalibrationResult {
cv::Mat cameraMatrix; // 相机矩阵(内参数)
cv::Mat distortionCoefficients; // 畸变系数
double reprojectionError; // 重投影误差(标定精度指标)
std::vector<cv::Mat> rotationVectors; // 旋转向量(外参数)
std::vector<cv::Mat> translationVectors; // 平移向量(外参数)
};
explicit CameraCalib(int columns, int rows, double square_size /*mm*/, Pattern pattern)
: patternSize_(columns, rows)
, squareSize_(square_size)
, pattern_(pattern) {
// 构造一个与标定板对应的真实的世界角点数据
for(int y = 0; y < patternSize_.height; ++y) {
for(int x = 0; x < patternSize_.width; ++x) {
realCorners_.emplace_back(x * square_size, y * square_size, 0.0f);
}
}
}
void addImageSample(const cv::Mat &image) { samples_.emplace_back(image); }
void addImageSample(const std::string &filename) {
cv::Mat mat = cv::imread(filename, cv::IMREAD_COLOR);
if(mat.empty()) {
std::println(stderr, "can not load filename: {}", filename);
return;
}
addImageSample(mat);
}
bool detectCorners(const cv::Mat &image, std::vector<cv::Point2f> &corners) {
bool found;
switch(pattern_) {
using enum Pattern;
case CALIB_SYMMETRIC_CHESSBOARD_GRID: detectSymmetricChessboardGrid(image, corners, found); break;
case CALIB_MARKER_CHESSBOARD_GRID: detectMarkerChessboardGrid(image, corners, found); break;
case CALIB_SYMMETRIC_CIRCLES_GRID: detectSymmetricCirclesGrid(image, corners, found); break;
case CALIB_ASYMMETRIC_CIRCLES_GRID: detectAsymmetricCirclesGrid(image, corners, found); break;
default: break;
}
return found;
}
std::vector<std::vector<cv::Point2f>> detect() {
std::vector<std::vector<cv::Point2f>> detectedCornerPoints;
std::mutex mtx; // 使用 mutex 来保护共享资源
std::atomic<int> count;
std::for_each(samples_.cbegin(), samples_.cend(), [&](const cv::Mat &image) {
std::vector<cv::Point2f> corners;
bool found = detectCorners(image, corners);
if(found) {
count++;
std::lock_guard<std::mutex> lock(mtx); // 使用 lock_guard 来保护共享资源
detectedCornerPoints.push_back(corners);
}
});
std::println("Detection successful: {} corners, total points: {}", int(count), detectedCornerPoints.size());
return detectedCornerPoints;
}
std::unique_ptr<CameraCalibrationResult> calib(std::vector<std::vector<cv::Point2f>> detectedCornerPoints, int width, int height) {
// 准备真实角点的位置
std::vector<std::vector<cv::Point3f>> realCornerPoints;
for(size_t i = 0; i < detectedCornerPoints.size(); ++i) {
realCornerPoints.emplace_back(realCorners_);
}
cv::Size imageSize(width, height);
// 初始化相机矩阵和畸变系数
cv::Mat cameraMatrix = cv::Mat::eye(3, 3, CV_64F);
cv::Mat distCoeffs = cv::Mat::zeros(5, 1, CV_64F);
std::vector<cv::Mat> rvecs, tvecs;
// 进行相机标定
double reproError = cv::calibrateCamera(realCornerPoints, detectedCornerPoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, cv::CALIB_FIX_K1 + cv::CALIB_FIX_K2 + cv::CALIB_FIX_K3 + cv::CALIB_FIX_K4 + cv::CALIB_FIX_K5);
// 将标定结果存储到结构体中
auto result = std::make_unique<CameraCalibrationResult>();
result->cameraMatrix = cameraMatrix;
result->distortionCoefficients = distCoeffs;
result->reprojectionError = reproError;
result->rotationVectors = rvecs;
result->translationVectors = tvecs;
return result;
}
// 打印标定结果
void print(const std::unique_ptr<CameraCalibrationResult> &result) {
std::cout << "重投影误差: " << result->reprojectionError << std::endl;
std::cout << "相机矩阵:\n" << result->cameraMatrix << std::endl;
std::cout << "畸变系数:\n" << result->distortionCoefficients << std::endl;
}
// 进行畸变校正测试
void test(const std::string &filename, const std::unique_ptr<CameraCalibrationResult> ¶m) {
// 读取一张测试图像
cv::Mat image = cv::imread(filename);
if(image.empty()) {
std::println("can not load filename");
return;
}
cv::Mat undistortedImage;
cv::undistort(image, undistortedImage, param->cameraMatrix, param->distortionCoefficients);
// 显示原图和校准后的图
cv::namedWindow("Original Image", cv::WINDOW_NORMAL);
cv::namedWindow("Undistorted Image", cv::WINDOW_NORMAL);
cv::imshow("Original Image", image);
cv::imshow("Undistorted Image", undistortedImage);
// 等待用户输入任意键
cv::waitKey(0);
}
private:
void dbgView(const cv::Mat &image, const std::vector<cv::Point2f> &corners, bool &found) {
if(!found) {
std::println("Cannot find corners in the image");
}
// Debug and view detected corner points in images
if constexpr(false) {
cv::drawChessboardCorners(image, patternSize_, corners, found);
cv::namedWindow("detectCorners", cv::WINDOW_NORMAL);
cv::imshow("detectCorners", image);
cv::waitKey(0);
cv::destroyAllWindows();
}
}
void detectSymmetricChessboardGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
if(found = cv::findChessboardCorners(image, patternSize_, image_corners); found) {
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::cornerSubPix(gray, image_corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01));
dbgView(image, image_corners, found);
}
}
void detectMarkerChessboardGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
if(found = cv::findChessboardCornersSB(image, patternSize_, image_corners); found) {
dbgView(image, image_corners, found);
}
}
void detectSymmetricCirclesGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
if(found = cv::findCirclesGrid(image, patternSize_, image_corners, cv::CALIB_CB_SYMMETRIC_GRID); found) {
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::cornerSubPix(gray, image_corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01));
dbgView(image, image_corners, found);
}
}
void detectAsymmetricCirclesGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
cv::SimpleBlobDetector::Params params;
params.minThreshold = 8;
params.maxThreshold = 255;
params.filterByArea = true;
params.minArea = 50; // 适当降低,以便检测小圆点
params.maxArea = 5000; // 适当降低,以避免误检大区域
params.minDistBetweenBlobs = 10; // 调小以适应紧密排列的圆点
params.filterByCircularity = false; // 允许更圆的形状
params.minCircularity = 0.7; // 只有接近圆的目标才被识别
params.filterByConvexity = true;
params.minConvexity = 0.8; // 只允许较凸的形状
params.filterByInertia = true;
params.minInertiaRatio = 0.1; // 适应不同形状
params.filterByColor = false; // 关闭颜色过滤,避免黑白检测问题
auto blobDetector = cv::SimpleBlobDetector::create(params);
if(found = cv::findCirclesGrid(image, patternSize_, image_corners, cv::CALIB_CB_ASYMMETRIC_GRID | cv::CALIB_CB_CLUSTERING, blobDetector); found) {
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::cornerSubPix(gray, image_corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01));
dbgView(image, image_corners, found);
}
}
private:
cv::Size patternSize_;
double squareSize_;
Pattern pattern_;
std::vector<cv::Point3f> realCorners_;
std::vector<cv::Mat> samples_;
};
// 测试函数
static void test_CameraCalib() {
// 创建一个 CameraCalib 对象,指定标定板大小、每个方格的边长和校准模式
CameraCalib calib(14, 9, 12.1, CameraCalib::Pattern::CALIB_MARKER_CHESSBOARD_GRID);
// 加载图像样本
std::vector<cv::String> result;
cv::glob("calibration_images/*.png", result, false);
for (auto &&filename : result) {
calib.addImageSample(filename);
}
// 检测角点
auto detectedCornerPoints = calib.detect();
// 进行相机标定
std::string filename = "calibration_images/checkerboard_radon.png";
cv::Mat image = cv::imread(filename);
if (image.empty()) {
std::println("can not load image");
return;
}
auto param = calib.calib(detectedCornerPoints, image.cols, image.cols);
// 打印标定结果
calib.print(param);
// 测试函数
calib.test(filename, param);
}
运行测试函数,输出结果如下所示:
Detection successful: 2 corners, total points: 2
重投影误差: 0.0373256
相机矩阵:
[483030.3184975122, 0, 1182.462802265994;
0, 483084.13533141, 1180.358683128085;
0, 0, 1]
畸变系数:
[0;
0;
-0.002454905573938355;
9.349667940808669e-05;
0]
// 保存标定结果
cv::FileStorage fs("calibration_result.yml", cv::FileStorage::WRITE);
fs << "camera_matrix" << result.cameraMatrix;
fs << "distortion_coefficients" << result.distCoeffs;
fs << "image_size" << result.imageSize;
fs.release();