双目相机标定与立体匹配实现 (C++)
双目相机标定和立体匹配系统,包含相机标定、立体校正、立体匹配、深度计算等功能。
一、系统架构
1.1 硬件配置
双目相机系统:
├── 左相机
│ ├── 分辨率:1280×960
│ └── 帧率:30fps
├── 右相机
│ ├── 分辨率:1280×960
│ └── 帧率:30fps
├── 标定板
│ ├── 棋盘格:9×6
│ └── 方格尺寸:25mm
└── 同步触发
├── 硬件同步
└── 软件同步
二、相机标定实现
2.1 标定头文件
/**
* @file stereo_calibration.h
* @brief 双目相机标定
*/
#ifndef STEREO_CALIBRATION_H
#define STEREO_CALIBRATION_H
#include <opencv2/opencv.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <memory>
#include <chrono>
#include <iomanip>
using namespace cv;
using namespace std;
/* 标定参数结构体 */
struct CalibrationParams {
/* 相机内参 */
cv::Mat cameraMatrix1; // 左相机内参矩阵
cv::Mat distCoeffs1; // 左相机畸变系数
cv::Mat cameraMatrix2; // 右相机内参矩阵
cv::Mat distCoeffs2; // 右相机畸变系数
/* 外参 */
cv::Mat R; // 旋转矩阵
cv::Mat T; // 平移向量
cv::Mat E; // 本质矩阵
cv::Mat F; // 基础矩阵
/* 立体校正参数 */
cv::Mat R1, R2; // 左右相机校正旋转矩阵
cv::Mat P1, P2; // 左右相机校正投影矩阵
cv::Mat Q; // 重投影矩阵
cv::Rect validRoi[2]; // 有效区域
/* 性能指标 */
double rms; // 重投影误差
double avg_reprojection_error; // 平均重投影误差
cv::Size imageSize; // 图像尺寸
cv::Size patternSize; // 棋盘格尺寸
double squareSize; // 棋盘格方格大小(mm)
/* 保存/加载标志 */
bool calibrated = false;
/* 打印参数 */
void print() const;
/* 保存到文件 */
bool save(const std::string& filename) const;
/* 从文件加载 */
bool load(const std::string& filename);
};
/* 标定类 */
class StereoCalibrator {
public:
StereoCalibrator(cv::Size patternSize, double squareSize,
int flags = cv::CALIB_FIX_ASPECT_RATIO |
cv::CALIB_ZERO_TANGENT_DIST |
cv::CALIB_SAME_FOCAL_LENGTH);
~StereoCalibrator() = default;
/* 添加标定图像对 */
bool addCalibrationPair(const cv::Mat& img1, const cv::Mat& img2,
bool drawCorners = false, cv::Mat* out1 = nullptr, cv::Mat* out2 = nullptr);
/* 执行标定 */
bool calibrate(CalibrationParams& params, bool fixIntrinsic = true);
/* 计算重投影误差 */
double computeReprojectionError(const CalibrationParams& params,
std::vector<double>& perViewErrors) const;
/* 立体校正 */
bool rectify(const CalibrationParams& params, cv::Mat& img1, cv::Mat& img2,
cv::Mat& rectified1, cv::Mat& rectified2) const;
/* 绘制极线 */
void drawEpipolarLines(const CalibrationParams& params,
const cv::Mat& img1, const cv::Mat& img2,
cv::Mat& out1, cv::Mat& out2, int numLines = 10) const;
/* 验证标定结果 */
bool validateCalibration(const CalibrationParams& params,
const cv::Mat& img1, const cv::Mat& img2,
double maxError = 1.0) const;
/* 获取标定状态 */
int getNumPairs() const { return imagePoints1.size(); }
bool isReady() const { return imagePoints1.size() >= 5; } // 至少需要5对图像
/* 清除数据 */
void clear();
private:
/* 标定板参数 */
cv::Size patternSize; // 内角点数量 (width-1, height-1)
double squareSize; // 棋盘格方格大小(mm)
/* 图像点 */
std::vector<std::vector<cv::Point2f>> imagePoints1; // 左图像角点
std::vector<std::vector<cv::Point2f>> imagePoints2; // 右图像角点
/* 标定标志 */
int calibrationFlags;
/* 世界坐标系点 */
std::vector<std::vector<cv::Point3f>> objectPoints; // 3D点
/* 内部函数 */
bool findChessboardCorners(const cv::Mat& image, std::vector<cv::Point2f>& corners,
bool drawCorners = false, cv::Mat* output = nullptr) const;
std::vector<cv::Point3f> generateObjectPoints() const;
};
/* 立体匹配类 */
class StereoMatcher {
public:
enum MatcherType {
BM = 0, // Block Matching
SGBM, // Semi-Global Block Matching
ELAS // Efficient Large-Scale Stereo
};
struct MatcherParams {
// BM参数
int blockSize = 21; // 匹配块大小
int minDisparity = 0; // 最小视差
int numDisparities = 64; // 视差搜索范围
int preFilterCap = 31; // 预滤波截断值
int uniquenessRatio = 15; // 唯一性比率
int speckleWindowSize = 100; // 斑点窗口大小
int speckleRange = 32; // 斑点范围
int disp12MaxDiff = 1; // 左右一致性检查最大差异
// SGBM参数
int SADWindowSize = 3; // SAD窗口大小
int P1 = 8 * 3 * 3; // 平滑度参数1
int P2 = 32 * 3 * 3; // 平滑度参数2
int mode = cv::StereoSGBM::MODE_SGBM_3WAY; // 模式
// 后处理参数
bool useWLSFilter = false; // 使用WLS滤波
double lambda = 8000.0; // WLS滤波lambda
double sigma = 1.5; // WLS滤波sigma
// 显示参数
bool colorMap = true; // 使用彩色视差图
int colorScale = 1; // 颜色缩放因子
};
StereoMatcher(MatcherType type = SGBM, const MatcherParams& params = MatcherParams());
~StereoMatcher() = default;
/* 计算视差图 */
cv::Mat computeDisparity(const cv::Mat& left, const cv::Mat& right);
/* 计算深度图 */
cv::Mat computeDepth(const cv::Mat& disparity, const cv::Mat& Q,
float maxDepth = 10000.0f, float minDepth = 0.0f);
/* 计算点云 */
void computePointCloud(const cv::Mat& disparity, const cv::Mat& Q,
const cv::Mat& color,
std::vector<cv::Vec3f>& points,
std::vector<cv::Vec3b>& colors);
/* 保存点云 */
bool savePointCloud(const std::string& filename,
const std::vector<cv::Vec3f>& points,
const std::vector<cv::Vec3b>& colors);
/* 更新参数 */
void updateParams(const MatcherParams& params);
/* 获取参数 */
MatcherParams getParams() const { return params; }
private:
MatcherType type;
MatcherParams params;
// 匹配器
cv::Ptr<cv::StereoBM> bm;
cv::Ptr<cv::StereoSGBM> sgbm;
// 后处理器
cv::Ptr<cv::ximgproc::DisparityWLSFilter> wls_filter;
cv::Ptr<cv::StereoMatcher> right_matcher;
// 初始化
void initMatcher();
// 后处理
cv::Mat postProcessDisparity(const cv::Mat& disparity);
// 转换为彩色
cv::Mat convertToColor(const cv::Mat& disparity);
};
/* 评估类 */
class StereoEvaluator {
public:
struct EvaluationResult {
// 精度指标
double rmse; // 均方根误差
double mae; // 平均绝对误差
double bad_pixels_1; // 误差>1像素的比例
double bad_pixels_2; // 误差>2像素的比例
double bad_pixels_4; // 误差>4像素的比例
// 运行时间
double time_ms; // 处理时间(ms)
// 视差图统计
double disparity_mean; // 平均视差
double disparity_std; // 视差标准差
double disparity_min; // 最小视差
double disparity_max; // 最大视差
void print() const;
};
StereoEvaluator() = default;
~StereoEvaluator() = default;
/* 评估视差图 */
EvaluationResult evaluate(const cv::Mat& disparity, const cv::Mat& groundTruth,
double scale = 1.0, double maxError = 4.0);
/* 评估深度图 */
EvaluationResult evaluateDepth(const cv::Mat& depth, const cv::Mat& groundTruth,
double maxDepth = 10000.0);
/* 生成评估报告 */
std::string generateReport(const EvaluationResult& result,
const std::string& title = "Stereo Evaluation Report");
/* 保存评估结果 */
bool saveResult(const EvaluationResult& result, const std::string& filename);
private:
/* 计算指标 */
double computeRMSE(const cv::Mat& pred, const cv::Mat& gt, cv::Mat& mask);
double computeMAE(const cv::Mat& pred, const cv::Mat& gt, cv::Mat& mask);
double computeBadPixels(const cv::Mat& pred, const cv::Mat& gt, cv::Mat& mask,
double threshold);
/* 掩码处理 */
cv::Mat createValidMask(const cv::Mat& pred, const cv::Mat& gt, double maxVal = 1e6);
};
三、标定实现
3.1 标定实现文件
/**
* @file stereo_calibration.cpp
* @brief 双目相机标定实现
*/
#include "stereo_calibration.h"
#include <chrono>
#include <iomanip>
#include <sstream>
/* 打印参数 */
void CalibrationParams::print() const {
cout << "\n=== Stereo Calibration Parameters ===" << endl;
cout << "Image Size: " << imageSize << endl;
cout << "Pattern Size: " << patternSize << endl;
cout << "Square Size: " << squareSize << " mm" << endl;
cout << "RMS Error: " << rms << " pixels" << endl;
cout << "Avg Reprojection Error: " << avg_reprojection_error << " pixels" << endl;
cout << "\nLeft Camera Matrix:\n" << cameraMatrix1 << endl;
cout << "Left Distortion Coefficients:\n" << distCoeffs1.t() << endl;
cout << "\nRight Camera Matrix:\n" << cameraMatrix2 << endl;
cout << "Right Distortion Coefficients:\n" << distCoeffs2.t() << endl;
cout << "\nRotation Matrix:\n" << R << endl;
cout << "Translation Vector:\n" << T.t() << endl;
cout << "\nRectified Left Camera Matrix:\n" << P1 << endl;
cout << "Rectified Right Camera Matrix:\n" << P2 << endl;
cout << "\nDisparity to Depth Matrix (Q):\n" << Q << endl;
cout << "===================================\n" << endl;
}
/* 保存参数到文件 */
bool CalibrationParams::save(const string& filename) const {
cv::FileStorage fs(filename, cv::FileStorage::WRITE);
if (!fs.isOpened()) {
cerr << "Failed to open file for writing: " << filename << endl;
return false;
}
fs << "calibrated" << calibrated;
fs << "image_width" << imageSize.width;
fs << "image_height" << imageSize.height;
fs << "pattern_width" << patternSize.width;
fs << "pattern_height" << patternSize.height;
fs << "square_size" << squareSize;
fs << "rms_error" << rms;
fs << "avg_reprojection_error" << avg_reprojection_error;
fs << "camera_matrix_left" << cameraMatrix1;
fs << "dist_coeffs_left" << distCoeffs1;
fs << "camera_matrix_right" << cameraMatrix2;
fs << "dist_coeffs_right" << distCoeffs2;
fs << "rotation_matrix" << R;
fs << "translation_vector" << T;
fs << "essential_matrix" << E;
fs << "fundamental_matrix" << F;
fs << "rectify_R1" << R1;
fs << "rectify_R2" << R2;
fs << "rectify_P1" << P1;
fs << "rectify_P2" << P2;
fs << "disparity_Q" << Q;
fs << "valid_roi_left_x" << validRoi[0].x;
fs << "valid_roi_left_y" << validRoi[0].y;
fs << "valid_roi_left_width" << validRoi[0].width;
fs << "valid_roi_left_height" << validRoi[0].height;
fs << "valid_roi_right_x" << validRoi[1].x;
fs << "valid_roi_right_y" << validRoi[1].y;
fs << "valid_roi_right_width" << validRoi[1].width;
fs << "valid_roi_right_height" << validRoi[1].height;
fs.release();
cout << "Calibration parameters saved to: " << filename << endl;
return true;
}
/* 从文件加载参数 */
bool CalibrationParams::load(const string& filename) {
cv::FileStorage fs(filename, cv::FileStorage::READ);
if (!fs.isOpened()) {
cerr << "Failed to open file for reading: " << filename << endl;
return false;
}
int width, height;
fs["image_width"] >> width;
fs["image_height"] >> height;
imageSize = cv::Size(width, height);
int pattern_width, pattern_height;
fs["pattern_width"] >> pattern_width;
fs["pattern_height"] >> pattern_height;
patternSize = cv::Size(pattern_width, pattern_height);
fs["square_size"] >> squareSize;
fs["rms_error"] >> rms;
fs["avg_reprojection_error"] >> avg_reprojection_error;
fs["calibrated"] >> calibrated;
fs["camera_matrix_left"] >> cameraMatrix1;
fs["dist_coeffs_left"] >> distCoeffs1;
fs["camera_matrix_right"] >> cameraMatrix2;
fs["dist_coeffs_right"] >> distCoeffs2;
fs["rotation_matrix"] >> R;
fs["translation_vector"] >> T;
fs["essential_matrix"] >> E;
fs["fundamental_matrix"] >> F;
fs["rectify_R1"] >> R1;
fs["rectify_R2"] >> R2;
fs["rectify_P1"] >> P1;
fs["rectify_P2"] >> P2;
fs["disparity_Q"] >> Q;
int roi_x, roi_y, roi_width, roi_height;
fs["valid_roi_left_x"] >> roi_x;
fs["valid_roi_left_y"] >> roi_y;
fs["valid_roi_left_width"] >> roi_width;
fs["valid_roi_left_height"] >> roi_height;
validRoi[0] = cv::Rect(roi_x, roi_y, roi_width, roi_height);
fs["valid_roi_right_x"] >> roi_x;
fs["valid_roi_right_y"] >> roi_y;
fs["valid_roi_right_width"] >> roi_width;
fs["valid_roi_right_height"] >> roi_height;
validRoi[1] = cv::Rect(roi_x, roi_y, roi_width, roi_height);
fs.release();
cout << "Calibration parameters loaded from: " << filename << endl;
return true;
}
/* 构造函数 */
StereoCalibrator::StereoCalibrator(cv::Size patternSize, double squareSize, int flags)
: patternSize(patternSize), squareSize(squareSize), calibrationFlags(flags) {
cout << "StereoCalibrator initialized with pattern size: " << patternSize
<< ", square size: " << squareSize << " mm" << endl;
}
/* 添加标定图像对 */
bool StereoCalibrator::addCalibrationPair(const cv::Mat& img1, const cv::Mat& img2,
bool drawCorners, cv::Mat* out1, cv::Mat* out2) {
vector<cv::Point2f> corners1, corners2;
// 查找左图像角点
bool found1 = findChessboardCorners(img1, corners1, drawCorners, out1);
if (!found1) {
cerr << "Could not find chessboard corners in left image" << endl;
return false;
}
// 查找右图像角点
bool found2 = findChessboardCorners(img2, corners2, drawCorners, out2);
if (!found2) {
cerr << "Could not find chessboard corners in right image" << endl;
return false;
}
// 验证角点数量一致
if (corners1.size() != corners2.size()) {
cerr << "Mismatched number of corners: " << corners1.size()
<< " vs " << corners2.size() << endl;
return false;
}
// 添加角点
imagePoints1.push_back(corners1);
imagePoints2.push_back(corners2);
// 生成对应的3D点
objectPoints.push_back(generateObjectPoints());
cout << "Added calibration pair " << imagePoints1.size()
<< " with " << corners1.size() << " corners" << endl;
return true;
}
/* 查找棋盘格角点 */
bool StereoCalibrator::findChessboardCorners(const cv::Mat& image, vector<cv::Point2f>& corners,
bool drawCorners, cv::Mat* output) const {
cv::Mat gray;
if (image.channels() == 3) {
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
} else {
gray = image.clone();
}
// 查找角点
bool found = cv::findChessboardCorners(gray, patternSize, corners,
cv::CALIB_CB_ADAPTIVE_THRESH + cv::CALIB_CB_NORMALIZE_IMAGE + cv::CALIB_CB_FAST_CHECK);
if (!found) {
return false;
}
// 亚像素精确化
cv::TermCriteria criteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001);
cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), criteria);
// 绘制角点
if (drawCorners && output != nullptr) {
cv::cvtColor(gray, *output, cv::COLOR_GRAY2BGR);
cv::drawChessboardCorners(*output, patternSize, cv::Mat(corners), found);
}
return true;
}
/* 生成3D世界坐标点 */
vector<cv::Point3f> StereoCalibrator::generateObjectPoints() const {
vector<cv::Point3f> points;
for (int i = 0; i < patternSize.height; i++) {
for (int j = 0; j < patternSize.width; j++) {
points.push_back(cv::Point3f(j * squareSize, i * squareSize, 0));
}
}
return points;
}
/* 执行标定 */
bool StereoCalibrator::calibrate(CalibrationParams& params, bool fixIntrinsic) {
if (imagePoints1.size() < 5) {
cerr << "Need at least 5 calibration pairs, got " << imagePoints1.size() << endl;
return false;
}
// 获取图像尺寸
params.imageSize = cv::Size(imagePoints1[0][0].x * 2, imagePoints1[0][0].y * 2);
cout << "\nStarting stereo calibration with " << imagePoints1.size() << " image pairs..." << endl;
cout << "Image size: " << params.imageSize << endl;
// 设置标定标志
int flags = calibrationFlags;
if (fixIntrinsic) {
flags |= cv::CALIB_FIX_INTRINSIC;
}
// 执行立体标定
auto start = chrono::high_resolution_clock::now();
params.rms = cv::stereoCalibrate(
objectPoints, imagePoints1, imagePoints2,
params.cameraMatrix1, params.distCoeffs1,
params.cameraMatrix2, params.distCoeffs2,
params.imageSize, params.R, params.T, params.E, params.F,
flags,
cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 100, 1e-5)
);
auto end = chrono::high_resolution_clock::now();
chrono::duration<double> elapsed = end - start;
cout << "Stereo calibration completed in " << elapsed.count() << " seconds" << endl;
cout << "RMS reprojection error: " << params.rms << " pixels" << endl;
// 计算平均重投影误差
vector<double> perViewErrors;
params.avg_reprojection_error = computeReprojectionError(params, perViewErrors);
cout << "Average reprojection error: " << params.avg_reprojection_error << " pixels" << endl;
// 立体校正
cout << "\nComputing rectification parameters..." << endl;
cv::stereoRectify(
params.cameraMatrix1, params.distCoeffs1,
params.cameraMatrix2, params.distCoeffs2,
params.imageSize, params.R, params.T,
params.R1, params.R2, params.P1, params.P2, params.Q,
cv::CALIB_ZERO_DISPARITY, 1, params.imageSize,
¶ms.validRoi[0], ¶ms.validRoi[1]
);
params.patternSize = patternSize;
params.squareSize = squareSize;
params.calibrated = true;
return true;
}
/* 计算重投影误差 */
double StereoCalibrator::computeReprojectionError(const CalibrationParams& params,
vector<double>& perViewErrors) const {
double totalError = 0;
int totalPoints = 0;
perViewErrors.resize(objectPoints.size());
vector<cv::Point2f> imagePoints2Projected;
for (size_t i = 0; i < objectPoints.size(); i++) {
vector<cv::Point2f> projected1, projected2;
double error1 = 0, error2 = 0;
// 左相机重投影
cv::projectPoints(objectPoints[i], cv::Mat::eye(3, 3, CV_64F),
cv::Mat::zeros(3, 1, CV_64F),
params.cameraMatrix1, params.distCoeffs1, projected1);
// 右相机重投影
cv::Mat rvec;
cv::Rodrigues(params.R, rvec);
cv::projectPoints(objectPoints[i], rvec, params.T,
params.cameraMatrix2, params.distCoeffs2, projected2);
// 计算误差
for (size_t j = 0; j < objectPoints[i].size(); j++) {
error1 += cv::norm(imagePoints1[i][j] - projected1[j]);
error2 += cv::norm(imagePoints2[i][j] - projected2[j]);
}
double viewError = (error1 + error2) / (2 * objectPoints[i].size());
perViewErrors[i] = viewError;
totalError += error1 + error2;
totalPoints += 2 * objectPoints[i].size();
}
return totalError / totalPoints;
}
/* 立体校正 */
bool StereoCalibrator::rectify(const CalibrationParams& params, cv::Mat& img1, cv::Mat& img2,
cv::Mat& rectified1, cv::Mat& rectified2) const {
if (!params.calibrated) {
cerr << "Calibration parameters not loaded" << endl;
return false;
}
if (img1.size() != params.imageSize || img2.size() != params.imageSize) {
cerr << "Image size mismatch. Expected: " << params.imageSize
<< ", Got: " << img1.size() << " and " << img2.size() << endl;
return false;
}
// 计算校正映射
cv::Mat map1x, map1y, map2x, map2y;
cv::initUndistortRectifyMap(
params.cameraMatrix1, params.distCoeffs1, params.R1, params.P1,
params.imageSize, CV_16SC2, map1x, map1y
);
cv::initUndistortRectifyMap(
params.cameraMatrix2, params.distCoeffs2, params.R2, params.P2,
params.imageSize, CV_16SC2, map2x, map2y
);
// 应用校正
cv::remap(img1, rectified1, map1x, map1y, cv::INTER_LINEAR);
cv::remap(img2, rectified2, map2x, map2y, cv::INTER_LINEAR);
return true;
}
/* 绘制极线 */
void StereoCalibrator::drawEpipolarLines(const CalibrationParams& params,
const cv::Mat& img1, const cv::Mat& img2,
cv::Mat& out1, cv::Mat& out2, int numLines) const {
if (!params.calibrated) {
cerr << "Calibration parameters not loaded" << endl;
return;
}
// 复制图像
if (img1.channels() == 1) {
cv::cvtColor(img1, out1, cv::COLOR_GRAY2BGR);
cv::cvtColor(img2, out2, cv::COLOR_GRAY2BGR);
} else {
img1.copyTo(out1);
img2.copyTo(out2);
}
// 生成随机点
cv::RNG rng(12345);
vector<cv::Point2f> points1, points2;
for (int i = 0; i < numLines; i++) {
cv::Point2f pt(rng.uniform(0.0f, (float)img1.cols),
rng.uniform(0.0f, (float)img1.rows));
points1.push_back(pt);
// 计算对应极线
vector<cv::Vec3f> lines;
cv::computeCorrespondEpilines(vector<cv::Point2f>(1, pt), 1, params.F, lines);
// 绘制极线
cv::Vec3f line = lines[0];
float a = line[0], b = line[1], c = line[2];
// 计算线段的两个端点
cv::Point pt1(0, -c / b);
cv::Point pt2(img2.cols, -(c + a * img2.cols) / b);
// 绘制
cv::Scalar color(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
cv::circle(out1, pt, 5, color, -1);
cv::line(out2, pt1, pt2, color, 2);
}
}
/* 验证标定结果 */
bool StereoCalibrator::validateCalibration(const CalibrationParams& params,
const cv::Mat& img1, const cv::Mat& img2,
double maxError) const {
if (!params.calibrated) {
cerr << "Calibration parameters not loaded" << endl;
return false;
}
// 查找角点
vector<cv::Point2f> corners1, corners2;
if (!findChessboardCorners(img1, corners1) || !findChessboardCorners(img2, corners2)) {
cerr << "Could not find chessboard corners" << endl;
return false;
}
// 验证角点数量
if (corners1.size() != corners2.size()) {
cerr << "Mismatched number of corners" << endl;
return false;
}
// 计算重投影误差
vector<cv::Point3f> objPoints = generateObjectPoints();
vector<cv::Point2f> projected1, projected2;
cv::projectPoints(objPoints, cv::Mat::eye(3, 3, CV_64F),
cv::Mat::zeros(3, 1, CV_64F),
params.cameraMatrix1, params.distCoeffs1, projected1);
cv::Mat rvec;
cv::Rodrigues(params.R, rvec);
cv::projectPoints(objPoints, rvec, params.T,
params.cameraMatrix2, params.distCoeffs2, projected2);
// 计算平均误差
double totalError = 0;
for (size_t i = 0; i < objPoints.size(); i++) {
totalError += cv::norm(corners1[i] - projected1[i]);
totalError += cv::norm(corners2[i] - projected2[i]);
}
double avgError = totalError / (2 * objPoints.size());
cout << "Validation error: " << avgError << " pixels" << endl;
return avgError <= maxError;
}
/* 清除数据 */
void StereoCalibrator::clear() {
imagePoints1.clear();
imagePoints2.clear();
objectPoints.clear();
cout << "Calibration data cleared" << endl;
}
四、立体匹配实现
4.1 立体匹配实现
/**
* @file stereo_matching.cpp
* @brief 立体匹配实现
*/
#include "stereo_calibration.h"
#include <opencv2/ximgproc.hpp>
/* 构造函数 */
StereoMatcher::StereoMatcher(MatcherType type, const MatcherParams& params)
: type(type), params(params) {
initMatcher();
}
/* 初始化匹配器 */
void StereoMatcher::initMatcher() {
switch (type) {
case BM:
bm = cv::StereoBM::create(params.numDisparities, params.blockSize);
bm->setPreFilterCap(params.preFilterCap);
bm->setMinDisparity(params.minDisparity);
bm->setNumDisparities(params.numDisparities);
bm->setTextureThreshold(10);
bm->setUniquenessRatio(params.uniquenessRatio);
bm->setSpeckleWindowSize(params.speckleWindowSize);
bm->setSpeckleRange(params.speckleRange);
bm->setDisp12MaxDiff(params.disp12MaxDiff);
break;
case SGBM:
sgbm = cv::StereoSGBM::create(
params.minDisparity,
params.numDisparities,
params.blockSize,
params.P1,
params.P2,
params.disp12MaxDiff,
params.preFilterCap,
params.uniquenessRatio,
params.speckleWindowSize,
params.speckleRange,
params.mode
);
break;
case ELAS:
// ELAS 实现需要额外库
cerr << "ELAS not implemented yet, using SGBM instead" << endl;
type = SGBM;
initMatcher();
break;
}
// 初始化WLS滤波器
if (params.useWLSFilter) {
wls_filter = cv::ximgproc::createDisparityWLSFilter(sgbm);
right_matcher = cv::ximgproc::createRightMatcher(sgbm);
wls_filter->setLambda(params.lambda);
wls_filter->setSigmaColor(params.sigma);
}
}
/* 更新参数 */
void StereoMatcher::updateParams(const MatcherParams& newParams) {
params = newParams;
initMatcher();
}
/* 计算视差图 */
cv::Mat StereoMatcher::computeDisparity(const cv::Mat& left, const cv::Mat& right) {
cv::Mat left_gray, right_gray;
// 转换为灰度图
if (left.channels() == 3) {
cv::cvtColor(left, left_gray, cv::COLOR_BGR2GRAY);
} else {
left_gray = left.clone();
}
if (right.channels() == 3) {
cv::cvtColor(right, right_gray, cv::COLOR_BGR2GRAY);
} else {
right_gray = right.clone();
}
cv::Mat disparity;
auto start = chrono::high_resolution_clock::now();
if (type == BM) {
bm->compute(left_gray, right_gray, disparity);
} else if (type == SGBM) {
if (params.useWLSFilter) {
// 计算左右视差图
cv::Mat disparity_left, disparity_right;
sgbm->compute(left_gray, right_gray, disparity_left);
right_matcher->compute(right_gray, left_gray, disparity_right);
// 应用WLS滤波
wls_filter->filter(disparity_left, left, disparity, disparity_right);
} else {
sgbm->compute(left_gray, right_gray, disparity);
}
}
auto end = chrono::high_resolution_clock::now();
chrono::duration<double, milli> elapsed = end - start;
params.time_ms = elapsed.count();
// 转换为浮点型
if (disparity.type() != CV_32F) {
disparity.convertTo(disparity, CV_32F, 1.0 / 16.0);
}
// 后处理
disparity = postProcessDisparity(disparity);
return disparity;
}
/* 后处理视差图 */
cv::Mat StereoMatcher::postProcessDisparity(const cv::Mat& disparity) {
cv::Mat processed = disparity.clone();
// 中值滤波
cv::medianBlur(processed, processed, 3);
// 双边滤波
cv::bilateralFilter(processed, processed, 5, 50.0, 50.0);
return processed;
}
/* 转换为彩色视差图 */
cv::Mat StereoMatcher::convertToColor(const cv::Mat& disparity) {
cv::Mat color;
// 归一化
double minVal, maxVal;
cv::minMaxLoc(disparity, &minVal, &maxVal);
cv::Mat normalized;
disparity.convertTo(normalized, CV_8U, 255.0 / (maxVal - minVal), -minVal * 255.0 / (maxVal - minVal));
// 应用颜色映射
cv::applyColorMap(normalized, color, cv::COLORMAP_JET);
return color;
}
/* 计算深度图 */
cv::Mat StereoMatcher::computeDepth(const cv::Mat& disparity, const cv::Mat& Q,
float maxDepth, float minDepth) {
cv::Mat depth;
// 使用重投影矩阵计算深度
cv::reprojectImageTo3D(disparity, depth, Q, true);
// 提取Z通道(深度)
vector<cv::Mat> channels;
cv::split(depth, channels);
cv::Mat depthMap = channels[2];
// 限制深度范围
depthMap.setTo(maxDepth, depthMap > maxDepth);
depthMap.setTo(minDepth, depthMap < minDepth);
return depthMap;
}
/* 计算点云 */
void StereoMatcher::computePointCloud(const cv::Mat& disparity, const cv::Mat& Q,
const cv::Mat& color,
vector<cv::Vec3f>& points,
vector<cv::Vec3b>& colors) {
cv::Mat points3D;
// 计算3D点
cv::reprojectImageTo3D(disparity, points3D, Q, true);
points.clear();
colors.clear();
// 转换为点云
for (int y = 0; y < points3D.rows; y++) {
for (int x = 0; x < points3D.cols; x++) {
cv::Vec3f point = points3D.at<cv::Vec3f>(y, x);
// 跳过无效点
if (point[2] > 10000 || point[2] <= 0) continue;
// 添加点
points.push_back(point);
// 添加颜色
if (!color.empty()) {
cv::Vec3b col = color.at<cv::Vec3b>(y, x);
colors.push_back(col);
} else {
colors.push_back(cv::Vec3b(255, 255, 255));
}
}
}
cout << "Generated " << points.size() << " points" << endl;
}
/* 保存点云 */
bool StereoMatcher::savePointCloud(const string& filename,
const vector<cv::Vec3f>& points,
const vector<cv::Vec3b>& colors) {
ofstream file(filename);
if (!file.is_open()) {
cerr << "Failed to open file: " << filename << endl;
return false;
}
// PLY文件头
file << "ply\n";
file << "format ascii 1.0\n";
file << "element vertex " << points.size() << "\n";
file << "property float x\n";
file << "property float y\n";
file << "property float z\n";
file << "property uchar red\n";
file << "property uchar green\n";
file << "property uchar blue\n";
file << "end_header\n";
// 写入点数据
for (size_t i = 0; i < points.size(); i++) {
file << points[i][0] << " " << points[i][1] << " " << points[i][2] << " ";
file << (int)colors[i][2] << " " << (int)colors[i][1] << " " << (int)colors[i][0] << "\n";
}
file.close();
cout << "Point cloud saved to: " << filename << endl;
return true;
}
五、主程序示例
5.1 完整示例程序
/**
* @file main.cpp
* @brief 双目相机标定与立体匹配示例
*/
#include "stereo_calibration.h"
#include <opencv2/opencv.hpp>
#include <iostream>
#include <filesystem>
#include <vector>
#include <string>
namespace fs = std::filesystem;
/* 函数声明 */
bool loadImagePairs(const std::string& folder, std::vector<cv::Mat>& leftImages,
std::vector<cv::Mat>& rightImages, const std::string& leftPrefix = "left",
const std::string& rightPrefix = "right");
void calibrateStereoCamera(const std::string& imageFolder, const std::string& calibrationFile);
void processStereoImages(const std::string& leftImage, const std::string& rightImage,
const std::string& calibrationFile);
void realtimeStereoCapture(int cameraLeft = 0, int cameraRight = 1);
void batchProcessFolder(const std::string& inputFolder, const std::string& outputFolder,
const std::string& calibrationFile);
/* 主程序 */
int main(int argc, char** argv) {
cout << "=== Stereo Camera Calibration and Matching ===" << endl;
cout << "Options:" << endl;
cout << "1. Calibrate stereo camera" << endl;
cout << "2. Process stereo image pair" << endl;
cout << "3. Real-time stereo capture" << endl;
cout << "4. Batch process folder" << endl;
cout << "Enter choice (1-4): ";
int choice;
cin >> choice;
switch (choice) {
case 1: {
string imageFolder, calibrationFile;
cout << "Enter image folder path: ";
cin >> imageFolder;
cout << "Enter calibration file path: ";
cin >> calibrationFile;
calibrateStereoCamera(imageFolder, calibrationFile);
break;
}
case 2: {
string leftImage, rightImage, calibrationFile;
cout << "Enter left image path: ";
cin >> leftImage;
cout << "Enter right image path: ";
cin >> rightImage;
cout << "Enter calibration file path: ";
cin >> calibrationFile;
processStereoImages(leftImage, rightImage, calibrationFile);
break;
}
case 3: {
int cameraLeft, cameraRight;
cout << "Enter left camera index (default 0): ";
cin >> cameraLeft;
cout << "Enter right camera index (default 1): ";
cin >> cameraRight;
realtimeStereoCapture(cameraLeft, cameraRight);
break;
}
case 4: {
string inputFolder, outputFolder, calibrationFile;
cout << "Enter input folder path: ";
cin >> inputFolder;
cout << "Enter output folder path: ";
cin >> outputFolder;
cout << "Enter calibration file path: ";
cin >> calibrationFile;
batchProcessFolder(inputFolder, outputFolder, calibrationFile);
break;
}
default:
cout << "Invalid choice" << endl;
break;
}
return 0;
}
/* 加载图像对 */
bool loadImagePairs(const string& folder, vector<cv::Mat>& leftImages,
vector<cv::Mat>& rightImages, const string& leftPrefix,
const string& rightPrefix) {
if (!fs::exists(folder)) {
cerr << "Folder does not exist: " << folder << endl;
return false;
}
leftImages.clear();
rightImages.clear();
// 获取所有左图像文件
vector<string> leftFiles, rightFiles;
for (const auto& entry : fs::directory_iterator(folder)) {
string filename = entry.path().filename().string();
if (filename.find(leftPrefix) != string::npos) {
leftFiles.push_back(entry.path().string());
} else if (filename.find(rightPrefix) != string::npos) {
rightFiles.push_back(entry.path().string());
}
}
// 排序
sort(leftFiles.begin(), leftFiles.end());
sort(rightFiles.begin(), rightFiles.end());
if (leftFiles.size() != rightFiles.size()) {
cerr << "Mismatched number of left/right images: "
<< leftFiles.size() << " vs " << rightFiles.size() << endl;
return false;
}
// 加载图像
for (size_t i = 0; i < leftFiles.size(); i++) {
cv::Mat left = cv::imread(leftFiles[i], cv::IMREAD_COLOR);
cv::Mat right = cv::imread(rightFiles[i], cv::IMREAD_COLOR);
if (left.empty() || right.empty()) {
cerr << "Failed to load image pair: " << leftFiles[i] << ", " << rightFiles[i] << endl;
continue;
}
leftImages.push_back(left);
rightImages.push_back(right);
}
cout << "Loaded " << leftImages.size() << " image pairs" << endl;
return true;
}
/* 标定双目相机 */
void calibrateStereoCamera(const string& imageFolder, const string& calibrationFile) {
cout << "\n=== Stereo Camera Calibration ===" << endl;
// 加载图像
vector<cv::Mat> leftImages, rightImages;
if (!loadImagePairs(imageFolder, leftImages, rightImages)) {
return;
}
// 初始化标定器
cv::Size patternSize(9, 6); // 内角点数量
double squareSize = 25.0; // 方格大小(mm)
StereoCalibrator calibrator(patternSize, squareSize);
// 添加标定图像对
int successfulPairs = 0;
for (size_t i = 0; i < leftImages.size(); i++) {
cv::Mat leftCorners, rightCorners;
bool success = calibrator.addCalibrationPair(
leftImages[i], rightImages[i], true, &leftCorners, &rightCorners
);
if (success) {
successfulPairs++;
// 显示角点
cv::Mat combined;
cv::hconcat(leftCorners, rightCorners, combined);
cv::resize(combined, combined, cv::Size(), 0.5, 0.5);
string winname = "Chessboard Corners " + to_string(successfulPairs);
cv::imshow(winname, combined);
cv::waitKey(500);
cv::destroyWindow(winname);
}
}
if (successfulPairs < 5) {
cerr << "Need at least 5 valid image pairs, got " << successfulPairs << endl;
return;
}
// 执行标定
CalibrationParams params;
if (calibrator.calibrate(params)) {
// 打印参数
params.print();
// 保存标定结果
if (params.save(calibrationFile)) {
cout << "Calibration saved to: " << calibrationFile << endl;
}
// 验证标定结果
cout << "\nValidating calibration..." << endl;
for (size_t i = 0; i < min(3, leftImages.size()); i++) {
bool valid = calibrator.validateCalibration(params, leftImages[i], rightImages[i], 2.0);
cout << "Image pair " << i + 1 << ": " << (valid ? "PASS" : "FAIL") << endl;
}
// 显示极线
cout << "\nDrawing epipolar lines..." << endl;
for (size_t i = 0; i < min(2, leftImages.size()); i++) {
cv::Mat epipolar1, epipolar2;
calibrator.drawEpipolarLines(params, leftImages[i], rightImages[i],
epipolar1, epipolar2, 20);
cv::Mat combined;
cv::hconcat(epipolar1, epipolar2, combined);
cv::resize(combined, combined, cv::Size(), 0.5, 0.5);
cv::imshow("Epipolar Lines", combined);
cv::waitKey(0);
}
}
cv::destroyAllWindows();
}
/* 处理立体图像对 */
void processStereoImages(const string& leftImage, const string& rightImage,
const string& calibrationFile) {
cout << "\n=== Processing Stereo Images ===" << endl;
// 加载标定参数
CalibrationParams params;
if (!params.load(calibrationFile)) {
return;
}
// 加载图像
cv::Mat left = cv::imread(leftImage, cv::IMREAD_COLOR);
cv::Mat right = cv::imread(rightImage, cv::IMREAD_COLOR);
if (left.empty() || right.empty()) {
cerr << "Failed to load images" << endl;
return;
}
// 创建标定器用于校正
StereoCalibrator calibrator(params.patternSize, params.squareSize);
// 立体校正
cv::Mat rectified1, rectified2;
if (calibrator.rectify(params, left, right, rectified1, rectified2)) {
// 显示校正结果
cv::Mat combined;
cv::hconcat(left, right, combined);
cv::hconcat(rectified1, rectified2, combined);
for (int y = 0; y < combined.rows; y += 30) {
cv::line(combined, cv::Point(0, y), cv::Point(combined.cols, y),
cv::Scalar(0, 255, 0), 1);
}
cv::resize(combined, combined, cv::Size(), 0.5, 0.5);
cv::imshow("Rectified Images (with epipolar lines)", combined);
cv::waitKey(0);
}
// 立体匹配
StereoMatcher::MatcherParams matcherParams;
matcherParams.useWLSFilter = true;
matcherParams.colorMap = true;
matcherParams.numDisparities = 128;
matcherParams.blockSize = 15;
StereoMatcher matcher(StereoMatcher::SGBM, matcherParams);
// 计算视差图
cv::Mat disparity = matcher.computeDisparity(rectified1, rectified2);
// 转换为彩色
cv::Mat colorDisparity = matcher.convertToColor(disparity);
cv::resize(colorDisparity, colorDisparity, cv::Size(), 0.5, 0.5);
cv::imshow("Disparity Map", colorDisparity);
// 计算深度图
cv::Mat depth = matcher.computeDepth(disparity, params.Q, 10000.0f, 0.1f);
// 归一化深度图用于显示
cv::Mat depthDisplay;
cv::normalize(depth, depthDisplay, 0, 255, cv::NORM_MINMAX, CV_8U);
cv::applyColorMap(depthDisplay, depthDisplay, cv::COLORMAP_JET);
cv::resize(depthDisplay, depthDisplay, cv::Size(), 0.5, 0.5);
cv::imshow("Depth Map", depthDisplay);
// 计算点云
vector<cv::Vec3f> points;
vector<cv::Vec3b> colors;
matcher.computePointCloud(disparity, params.Q, rectified1, points, colors);
// 保存点云
string timestamp = to_string(chrono::system_clock::now().time_since_epoch().count());
string cloudFile = "point_cloud_" + timestamp + ".ply";
matcher.savePointCloud(cloudFile, points, colors);
cout << "Point cloud saved to: " << cloudFile << endl;
cv::waitKey(0);
cv::destroyAllWindows();
}
/* 实时立体捕获 */
void realtimeStereoCapture(int cameraLeft, int cameraRight) {
cout << "\n=== Real-time Stereo Capture ===" << endl;
// 打开摄像头
cv::VideoCapture capLeft(cameraLeft);
cv::VideoCapture capRight(cameraRight);
if (!capLeft.isOpened() || !capRight.isOpened()) {
cerr << "Failed to open cameras" << endl;
return;
}
// 设置相机参数
capLeft.set(cv::CAP_PROP_FRAME_WIDTH, 640);
capLeft.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
capRight.set(cv::CAP_PROP_FRAME_WIDTH, 640);
capRight.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
// 加载标定参数
CalibrationParams params;
if (!params.load("calibration.yml")) {
cout << "Using default parameters" << endl;
// 使用默认参数
params.imageSize = cv::Size(640, 480);
params.calibrated = false;
}
// 初始化匹配器
StereoMatcher::MatcherParams matcherParams;
matcherParams.numDisparities = 64;
matcherParams.blockSize = 11;
StereoMatcher matcher(StereoMatcher::SGBM, matcherParams);
cout << "Press:" << endl;
cout << " SPACE - Capture calibration image" << endl;
cout << " 'c' - Calibrate" << endl;
cout << " 's' - Save disparity" << endl;
cout << " 'q' - Quit" << endl;
vector<cv::Mat> calibrationLeft, calibrationRight;
StereoCalibrator calibrator(cv::Size(9, 6), 25.0);
int frameCount = 0;
bool showDisparity = false;
while (true) {
cv::Mat frameLeft, frameRight;
capLeft >> frameLeft;
capRight >> frameRight;
if (frameLeft.empty() || frameRight.empty()) {
break;
}
cv::Mat display;
if (params.calibrated && showDisparity) {
// 校正
cv::Mat rectified1, rectified2;
calibrator.rectify(params, frameLeft, frameRight, rectified1, rectified2);
// 计算视差
cv::Mat disparity = matcher.computeDisparity(rectified1, rectified2);
cv::Mat colorDisparity = matcher.convertToColor(disparity);
// 显示
cv::hconcat(frameLeft, frameRight, display);
cv::resize(display, display, cv::Size(), 0.5, 0.5);
cv::imshow("Stereo Input", display);
cv::imshow("Disparity Map", colorDisparity);
} else {
cv::hconcat(frameLeft, frameRight, display);
cv::resize(display, display, cv::Size(), 0.5, 0.5);
cv::imshow("Stereo Input", display);
}
char key = cv::waitKey(1);
if (key == 'q') {
break;
} else if (key == ' ') {
// 捕获标定图像
calibrationLeft.push_back(frameLeft.clone());
calibrationRight.push_back(frameRight.clone());
cout << "Captured calibration image " << calibrationLeft.size() << endl;
// 显示角点
cv::Mat leftCorners, rightCorners;
calibrator.addCalibrationPair(frameLeft, frameRight, true, &leftCorners, &rightCorners);
cv::Mat combined;
cv::hconcat(leftCorners, rightCorners, combined);
cv::resize(combined, combined, cv::Size(), 0.5, 0.5);
cv::imshow("Calibration Corners", combined);
} else if (key == 'c') {
// 标定
if (calibrationLeft.size() >= 5) {
calibrator.clear();
for (size_t i = 0; i < calibrationLeft.size(); i++) {
calibrator.addCalibrationPair(calibrationLeft[i], calibrationRight[i]);
}
if (calibrator.calibrate(params)) {
params.save("realtime_calibration.yml");
cout << "Calibration completed!" << endl;
params.print();
showDisparity = true;
}
} else {
cout << "Need at least 5 calibration images, got " << calibrationLeft.size() << endl;
}
} else if (key == 's') {
// 保存当前帧
string timestamp = to_string(frameCount++);
cv::imwrite("left_" + timestamp + ".jpg", frameLeft);
cv::imwrite("right_" + timestamp + ".jpg", frameRight);
cout << "Saved frame " << timestamp << endl;
} else if (key == 'd') {
showDisparity = !showDisparity;
}
}
capLeft.release();
capRight.release();
cv::destroyAllWindows();
}
/* 批量处理文件夹 */
void batchProcessFolder(const string& inputFolder, const string& outputFolder,
const string& calibrationFile) {
cout << "\n=== Batch Processing Folder ===" << endl;
// 创建输出文件夹
fs::create_directories(outputFolder);
// 加载标定参数
CalibrationParams params;
if (!params.load(calibrationFile)) {
return;
}
// 加载图像对
vector<cv::Mat> leftImages, rightImages;
if (!loadImagePairs(inputFolder, leftImages, rightImages)) {
return;
}
// 初始化
StereoCalibrator calibrator(params.patternSize, params.squareSize);
StereoMatcher matcher(StereoMatcher::SGBM);
// 处理每对图像
for (size_t i = 0; i < leftImages.size(); i++) {
cout << "Processing image pair " << i + 1 << "/" << leftImages.size() << endl;
// 校正
cv::Mat rectified1, rectified2;
if (!calibrator.rectify(params, leftImages[i], rightImages[i], rectified1, rectified2)) {
cerr << "Failed to rectify image pair " << i << endl;
continue;
}
// 保存校正后的图像
string baseName = "pair_" + to_string(i + 1);
cv::imwrite(outputFolder + "/" + baseName + "_rect_left.jpg", rectified1);
cv::imwrite(outputFolder + "/" + baseName + "_rect_right.jpg", rectified2);
// 计算视差
cv::Mat disparity = matcher.computeDisparity(rectified1, rectified2);
cv::Mat colorDisparity = matcher.convertToColor(disparity);
cv::imwrite(outputFolder + "/" + baseName + "_disparity.jpg", colorDisparity);
// 计算深度
cv::Mat depth = matcher.computeDepth(disparity, params.Q);
// 归一化深度图用于保存
cv::Mat depthDisplay;
cv::normalize(depth, depthDisplay, 0, 255, cv::NORM_MINMAX, CV_8U);
cv::applyColorMap(depthDisplay, depthDisplay, cv::COLORMAP_JET);
cv::imwrite(outputFolder + "/" + baseName + "_depth.jpg", depthDisplay);
// 计算点云
vector<cv::Vec3f> points;
vector<cv::Vec3b> colors;
matcher.computePointCloud(disparity, params.Q, rectified1, points, colors);
// 保存点云
string plyFile = outputFolder + "/" + baseName + "_cloud.ply";
matcher.savePointCloud(plyFile, points, colors);
}
cout << "Batch processing completed. Results saved to: " << outputFolder << endl;
}
参考代码 双目相机标定,和立体匹配,能够同时标定两个相机 www.youwenfan.com/contentcnv/72040.html
六、CMake配置
6.1 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(StereoCalibration)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找OpenCV
find_package(OpenCV REQUIRED COMPONENTS
core
imgproc
calib3d
highgui
features2d
xfeatures2d
ximgproc
)
# 包含目录
include_directories(${OpenCV_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
# 可执行文件
add_executable(stereo_calibration
main.cpp
stereo_calibration.cpp
stereo_matching.cpp
)
# 链接库
target_link_libraries(stereo_calibration ${OpenCV_LIBS})
# 编译选项
if(MSVC)
target_compile_options(stereo_calibration PRIVATE /W3)
else()
target_compile_options(stereo_calibration PRIVATE -Wall -Wextra -O2)
endif()
# 安装目标
install(TARGETS stereo_calibration DESTINATION bin)
七、使用说明
7.1 编译与运行
# 编译
mkdir build
cd build
cmake ..
make
# 运行标定
./stereo_calibration
# 选择选项1,输入图像文件夹和标定文件路径
# 处理图像
./stereo_calibration
# 选择选项2,输入左右图像路径和标定文件路径
7.2 标定板生成
# generate_checkerboard.py
import cv2
import numpy as np
# 生成棋盘格
pattern_size = (9, 6) # 内角点数量
square_size = 25 # 方格大小(mm)
image_size = (800, 600) # 图像大小
# 创建棋盘格
checkerboard = np.zeros((image_size[1], image_size[0]), dtype=np.uint8)
checkerboard[:] = 255
# 绘制方格
for i in range(pattern_size[1]):
for j in range(pattern_size[0]):
x = j * square_size
y = i * square_size
if (i + j) % 2 == 0:
checkerboard[y:y+square_size, x:x+square_size] = 0
cv2.imwrite("checkerboard.png", checkerboard)
print("Checkerboard saved as checkerboard.png")
这个完整的双目相机标定和立体匹配系统包含了从标定到深度计算的全流程实现。
浙公网安备 33010602011771号