<think>我们之前已经生成了核线影像,现在需要利用核线影像进行立体匹配得到视差图,然后将视差图转换为深度图,最后生成三维点云。
步骤:
1. 立体匹配:使用SGBM(Semi-Global Block Matching)算法计算视差图。
2. 深度图计算:利用视差图和投影矩阵(或校正后的投影矩阵)计算深度图。
3. 生成点云:将深度图转换为三维点云,并保存为PLY格式。
注意:我们之前生成了三种类型的核线影像(水平、竖直、接近原始图像)。这里我们选择一种进行后续处理。通常选择接近原始图像的核线影像(original_like)进行立体匹配,因为其变形较小,保留的有效区域较多。
具体步骤:
1. 读取校正后的左右图像(original_like_left和original_like_right)。
2. 将图像转换为灰度图(立体匹配算法通常处理灰度图像)。
3. 设置SGBM参数,计算视差图。
4. 将视差图归一化以便显示和保存(可选,但实际深度计算需要原始视差值)。
5. 计算深度图:深度 = (焦距 * 基线) / 视差。这里需要知道基线距离和焦距。我们可以从校正后的投影矩阵P1_o和P2_o中提取基线(即平移向量)和焦距(通常为内参矩阵中的fx)。
6. 生成点云:遍历深度图的每个像素,利用内参矩阵和深度值计算三维坐标。注意:校正后的图像对应的投影矩阵为P1_o和P2_o,其中P1_o是左视图的投影矩阵,其内参矩阵K1_o和旋转矩阵R1_o可以通过分解得到,但OpenCV的reprojectImageTo3D函数可以直接使用Q矩阵(由stereoRectify计算得到)来将视差图转换为三维点云。
但是,在之前的代码中,我们计算了Q矩阵(Q_o)用于重投影。因此,我们可以使用reprojectImageTo3D函数将视差图转换为三维点云。
然而,注意:在生成接近原始图像的核线影像时,我们调用了stereoRectify,并得到了Q_o矩阵。这个Q矩阵就是用于重投影的。
因此,步骤5和6可以合并为:
- 使用reprojectImageTo3D(disparity, pointCloud, Q_o, ...)函数直接生成三维点云。
但是,我们也可以手动计算深度图,然后生成点云。这里为了简便和准确,我们使用OpenCV的reprojectImageTo3D函数。
代码结构:
1. 读取之前保存的接近原始图像的核线影像(左右视图)。
2. 转换为灰度图。
3. 使用SGBM计算视差图(注意:左右视图必须输入正确,左图是参考图)。
4. 对视差图进行后处理(可选,例如滤波)。
5. 使用Q_o矩阵和reprojectImageTo3D生成三维点云。
6. 保存点云为PLY文件。
注意:原始图像可能是16位,但立体匹配通常使用8位图像。因此,如果原始图像是16位,我们需要将其转换为8位(在立体匹配之前)。或者,我们可以使用归一化方法将图像缩放到0-255范围,但要注意保留对比度。
由于我们之前保存校正后的图像为8位(在saveImageWithCheck函数中保存为8位),但这里我们重新读取的是8位图像。但注意:在之前的代码中,我们保存了校正后的图像(original_like_left.jpg等)为8位,所以这里直接读取8位图像即可。
但是,为了获得更好的匹配效果,我们可以在立体匹配之前进行一些预处理(如直方图均衡化)以提高对比度。
另外,SGBM参数需要根据实际情况调整。
实现:
我们将编写一个函数来执行立体匹配(SGBM),然后使用reprojectImageTo3D生成点云。
注意:视差图是16位有符号整数(SGBM的结果),每个像素的视差值需要除以16(因为SGBM算法返回的视差是16倍整数)。
步骤:
1. 读取左右图像(original_like_left.jpg和original_like_right.jpg)。
2. 转换为灰度图。
3. 创建SGBM对象,设置参数。
4. 计算视差图。
5. 将视差图转换为8位或32位浮点图(用于显示和深度计算)。
6. 使用Q_o矩阵和reprojectImageTo3D函数生成三维点云(32位浮点)。
7. 保存点云:遍历三维点云矩阵,将有效点(深度在合理范围内)保存到PLY文件。
注意:点云数据量很大,我们可能需要过滤掉无效点(例如,视差为0的点)。
由于我们之前已经计算了Q_o矩阵,所以在主函数中已经存在Q_o。但是,我们之前没有保存它,所以需要重新计算?或者我们可以在生成校正映射时保存Q_o矩阵。
实际上,在之前的代码中,我们调用了stereoRectify来生成Q_o矩阵,但是该矩阵只在函数内部有效。我们需要将其保存下来,以便在后续步骤中使用。
因此,我们需要修改之前的代码,将Q_o矩阵保存为文件,或者在这里重新计算。但为了代码的连续性,我们可以在主函数中增加一个步骤,将Q_o矩阵保存下来(例如,保存为文件或全局变量)。但是,由于我们现在的任务是继续完成后续步骤,我们可以重新读取校正后的图像,然后重新计算Q_o矩阵?这样并不好,因为Q_o矩阵是由投影矩阵计算得到的,我们也可以从投影矩阵重新计算。
然而,为了简化,我们可以在之前的代码中保存Q_o矩阵(例如,保存到文件)。但是,由于我们现在的任务是接着之前的代码写,所以我们可以直接使用之前计算得到的Q_o矩阵。
但是,我们之前没有保存Q_o矩阵,而且它是在生成接近原始图像的核线影像时计算的。所以,我们可以在后续步骤中重新计算一次,或者将Q_o矩阵保存下来。
这里,我们选择将Q_o矩阵保存为文件(例如,YAML文件),然后在后续步骤中读取。或者,我们可以在同一个程序中完成所有步骤,这样Q_o矩阵还在内存中。
由于我们是在同一个main函数中完成,所以我们可以直接使用Q_o矩阵。
因此,我们将在生成接近原始图像的核线影像后,继续执行立体匹配和点云生成。
但是,由于我们之前已经保存了校正后的图像(original_like_left.jpg和original_like_right.jpg),所以我们可以直接读取它们。但是,为了利用内存中已经存在的校正后图像(original_like_left和original_like_right),我们可以避免重新读取,直接使用。
所以,我们不需要重新读取,直接使用内存中的Mat对象即可。
因此,我们可以在主函数中生成校正后的图像后,立即进行立体匹配和点云生成。
修改主函数:在生成接近原始图像的核线影像后,添加立体匹配和点云生成的代码。
但是,由于校正后的图像是彩色图像(3通道),而立体匹配需要单通道,所以我们需要转换为灰度图。
另外,注意:我们之前保存的校正后图像是8位,但内存中的Mat对象(original_like_left和original_like_right)可能是16位或8位?在之前的处理中,我们处理图像时保持了原始位深,但是在保存时转换为8位。但是,在内存中,original_like_left和original_like_right的位深与原始图像相同(因为remap函数会保持原图像位深)。但是,原始图像可能是16位,而立体匹配算法通常处理8位图像。
因此,我们需要将校正后的图像转换为8位灰度图。
步骤:
1. 将original_like_left和original_like_right转换为灰度图(如果原始是16位,则先归一化到8位?或者直接转换为8位灰度图?)
2. 进行立体匹配。
由于我们不知道原始图像的位深,我们可以先将其转换为8位(如果当前是16位):
- 使用convertTo函数,并缩放(1/256.0)将16位转换为8位。
但是,为了通用性,我们可以先检查图像位深,然后进行转换。
另一种方法是:在立体匹配之前,将图像归一化到0-255范围(8位)。我们可以这样做:
- 如果图像是16位,则使用convertTo转换为8位(乘以1/256.0)。
- 如果是8位,则直接使用。
- 如果是浮点,则归一化。
但是,我们这里假设原始图像是16位,所以需要转换。
我们也可以使用之前保存的8位图像(从文件读取),但这样会损失精度。所以,我们使用内存中的Mat对象,并转换为8位灰度图。
具体步骤:
1. 转换为灰度图(如果已经是灰度图则跳过,但我们的处理函数将图像转换为了3通道BGR,所以需要灰度转换)。
2. 转换为8位(如果当前是16位)。
代码:
在主函数中,生成校正后的图像后(original_like_left和original_like_right),我们进行以下操作:
// 转换为灰度图
Mat gray_left, gray_right;
cvtColor(original_like_left, gray_left, COLOR_BGR2GRAY);
cvtColor(original_like_right, gray_right, COLOR_BGR2GRAY);
// 如果位深是16位,则转换为8位
if (gray_left.depth() == CV_16U) {
Mat gray_left_8u, gray_right_8u;
gray_left.convertTo(gray_left_8u, CV_8U, 1.0/256.0);
gray_right.convertTo(gray_right_8u, CV_8U, 1.0/256.0);
gray_left = gray_left_8u;
gray_right = gray_right_8u;
}
// 如果是其他位深,也转换为8位(例如32位浮点)
else if (gray_left.depth() != CV_8U) {
gray_left.convertTo(gray_left, CV_8U);
gray_right.convertTo(gray_right, CV_8U);
}
3. 然后进行立体匹配。
立体匹配(SGBM)参数设置:
int minDisparity = 0;
int numDisparities = 16*5; // 必须是16的倍数
int blockSize = 5; // 匹配块大小,奇数
int P1 = 8 * blockSize * blockSize;
int P2 = 32 * blockSize * blockSize;
int disp12MaxDiff = 1;
int preFilterCap = 63;
int uniquenessRatio = 10;
int speckleWindowSize = 100;
int speckleRange = 32;
int mode = StereoSGBM::MODE_SGBM; // 或者MODE_HH
Ptr<StereoSGBM> sgbm = StereoSGBM::create(minDisparity, numDisparities, blockSize,
P1, P2, disp12MaxDiff, preFilterCap, uniquenessRatio, speckleWindowSize, speckleRange, mode);
Mat disparity_sgbm;
sgbm->compute(gray_left, gray_right, disparity_sgbm);
// 视差图是16位有符号整数,实际视差 = 视差值/16.0
Mat disparity;
disparity_sgbm.convertTo(disparity, CV_32F, 1.0/16.0);
4. 显示和保存视差图(归一化到0-255以便可视化)
Mat disp8u;
normalize(disparity, disp8u, 0, 255, NORM_MINMAX, CV_8U);
imwrite(output_dir + "disparity.jpg", disp8u);
5. 使用reprojectImageTo3D生成点云
Mat pointCloud;
reprojectImageTo3D(disparity_sgbm, pointCloud, Q_o, false, CV_32F); // 注意:这里输入的是16位视差图
6. 保存点云为PLY文件
// 遍历点云,将有效点(非无穷远,且深度在合理范围内)保存
// 注意:点云矩阵是3通道浮点矩阵,每个像素对应一个三维点
// 我们过滤掉视差为0的点(无效点)
FILE* fp = fopen((output_dir + "point_cloud.ply").c_str(), "w");
if (!fp) {
cerr << "无法打开点云文件进行写入" << endl;
return -1;
}
// PLY文件头
fprintf(fp, "ply\n");
fprintf(fp, "format ascii 1.0\n");
fprintf(fp, "element vertex %d\n", ...); // 先计算有效点数量
fprintf(fp, "property float x\n");
fprintf(fp, "property float y\n");
fprintf(fp, "property float z\n");
fprintf(fp, "end_header\n");
// 遍历点云矩阵,写入有效点
// 注意:点云矩阵的尺寸与图像相同
int count = 0;
for (int i = 0; i < pointCloud.rows; i++) {
for (int j = 0; j < pointCloud.cols; j++) {
Vec3f point = pointCloud.at<Vec3f>(i, j);
// 过滤无效点(视差为0的点,在SGBM视差图中0表示无效,但reprojectImageTo3D会将视差为0的点映射为无穷远?)
// 在reprojectImageTo3D中,如果视差为0,则点坐标会是很大的负数(-10000)?具体看Q矩阵
if (fabs(point[2] + 10000) < 1e-6) // 无穷远点(视差为0)会被映射为(-10000,-10000,-10000)?或者检查z值是否在合理范围
continue;
// 或者我们直接检查视差图:如果视差图(disparity_sgbm)在(i,j)处的值小于某个阈值(如0),则跳过
// 这里我们直接检查视差图(16位整数视差图)
if (disparity_sgbm.at<short>(i, j) <= 0)
continue;
count++;
}
}
// 重新写入文件头(因为需要知道点数)
// 由于我们不知道点数,所以先遍历一次计数,然后再写入点,这样效率较低。我们可以先写入头(预留点数),然后写入点,最后再修改头。或者先计数再写入。
// 我们重新打开文件,先写入头,再写入点。
// 重新打开
fclose(fp);
fp = fopen((output_dir + "point_cloud.ply").c_str(), "w");
fprintf(fp, "ply\n");
fprintf(fp, "format ascii 1.0\n");
fprintf(fp, "element vertex %d\n", count);
fprintf(fp, "property float x\n");
fprintf(fp, "property float y\n");
fprintf(fp, "property float z\n");
fprintf(fp, "end_header\n");
for (int i = 0; i < pointCloud.rows; i++) {
for (int j = 0; j < pointCloud.cols; j++) {
Vec3f point = pointCloud.at<Vec3f>(i, j);
if (disparity_sgbm.at<short>(i, j) <= 0)
continue;
fprintf(fp, "%f %f %f\n", point[0], point[1], point[2]);
}
}
fclose(fp);
注意:点云坐标是相对于左相机坐标系的。
但是,上述方法会生成很大的文件。我们也可以使用二进制格式保存以减小文件大小。
另外,我们还可以保存颜色信息(如果有的话)。但是,这里我们使用的是灰度图进行匹配,所以没有颜色。如果我们想保存颜色,可以从原始校正后的彩色图像中获取颜色。
因此,我们可以修改点云保存,添加颜色属性:
// 在头中添加:
fprintf(fp, "property uchar red\n");
fprintf(fp, "property uchar green\n");
fprintf(fp, "property uchar blue\n");
// 然后在写入每个点时,从校正后的左图(original_like_left)中获取颜色
Vec3b color = original_like_left.at<Vec3b>(i, j);
fprintf(fp, "%f %f %f %d %d %d\n", point[0], point[1], point[2], color[2], color[1], color[0]); // OpenCV是BGR顺序,PLY通常用RGB
但是,由于我们之前将图像转换为8位,所以颜色值在0-255之间。
注意:点云保存时,我们使用的是校正后的图像坐标,所以颜色和点是一一对应的。
因此,我们修改点云保存函数,添加颜色。
但是,由于点云数据量很大,我们可能只保存每个有效点的颜色。
修改后的保存点云代码(带颜色):
// 先计数
int count = 0;
for (int i = 0; i < pointCloud.rows; i++) {
for (int j = 0; j < pointCloud.cols; j++) {
if (disparity_sgbm.at<short>(i, j) <= 0)
continue;
count++;
}
}
// 写入文件头
fprintf(fp, "ply\n");
fprintf(fp, "format ascii 1.0\n");
fprintf(fp, "element vertex %d\n", count);
fprintf(fp, "property float x\n");
fprintf(fp, "property float y\n");
fprintf(fp, "property float z\n");
fprintf(fp, "property uchar red\n");
fprintf(fp, "property uchar green\n");
fprintf(fp, "property uchar blue\n");
fprintf(fp, "end_header\n");
// 写入点
for (int i = 0; i < pointCloud.rows; i++) {
for (int j = 0; j < pointCloud.cols; j++) {
if (disparity_sgbm.at<short>(i, j) <= 0)
continue;
Vec3f point = pointCloud.at<Vec3f>(i, j);
Vec3b color = original_like_left.at<Vec3b>(i, j);
fprintf(fp, "%f %f %f %d %d %d\n", point[0], point[1], point[2], color[2], color[1], color[0]); // RGB顺序
}
}
注意:original_like_left是校正后的彩色图像(8位3通道),所以我们可以直接获取颜色。
但是,如果校正后的图像是16位,那么我们需要转换为8位才能获取颜色?但我们在保存时已经转换为8位,并且内存中的original_like_left在保存后并没有改变,所以它可能是16位或8位?我们需要确保original_like_left是8位。
在之前的保存函数中,我们保存时转换为了8位,但内存中的original_like_left并没有被改变。所以,如果原始图像是16位,那么original_like_left也是16位。因此,我们需要在获取颜色之前将其转换为8位吗?不,因为点云颜色通常用8位表示。我们可以这样:
// 创建一个8位的彩色校正图像
Mat color_left_8u;
if (original_like_left.depth() == CV_16U) {
original_like_left.convertTo(color_left_8u, CV_8U, 1.0/256.0);
} else {
color_left_8u = original_like_left;
}
// 然后在循环中使用color_left_8u获取颜色
但是,我们也可以在主函数中,在生成校正图像后,就将其转换为8位(为了后续使用颜色)。这样,我们只需要转换一次。
因此,我们在生成校正图像后,就创建8位彩色图像:
Mat original_like_left_8u, original_like_right_8u;
if (original_like_left.depth() == CV_16U) {
original_like_left.convertTo(original_like_left_8u, CV_8U, 1.0/256.0);
original_like_right.convertTo(original_like_right_8u, CV_8U, 1.0/256.0);
} else {
original_like_left_8u = original_like_left;
original_like_right_8u = original_like_right;
}
然后,在立体匹配时,我们使用original_like_left_8u和original_like_right_8u转换为灰度图。
这样,我们保存点云时使用的颜色就是8位的。
综上所述,我们修改主函数,在生成校正图像后,转换为8位彩色图像(如果原始是16位),然后进行灰度转换和立体匹配。
最后,保存点云。
注意:我们也可以使用OpenCV的viz模块来显示点云,但这里我们只保存为PLY文件。
由于代码较长,我们将上述步骤整合到主函数中。
注意:立体匹配参数需要根据实际场景调整,这里给出的参数是示例,可能需要调整。
另外,视差图计算可能需要较长时间,尤其是高分辨率图像。
我们开始编写代码。</think>### 将视差图转为深度图和生成三维点云
以下是基于您提供的核线影像生成代码,添加视差图计算、深度图转换和点云生成的完整实现。代码使用OpenCV 4.5.5和C++,兼容VS2022开发环境。
```cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/utils/filesystem.hpp>
#include <fstream>
#include <windows.h>
using namespace cv;
using namespace std;
// 投影矩阵结构体
struct ProjectionMatrix {
double data[3][4];
};
// 读取投影矩阵
ProjectionMatrix readProjectionMatrix() {
ProjectionMatrix P;
P.data[0][0] = 5526.4864457012472; P.data[0][1] = 6363.7183421283298; P.data[0][2] = -4131.0482340017224; P.data[0][3] = -24448406087.5781860000000;
P.data[1][0] = 6172.8397894815880; P.data[1][1] = -5588.8214670529014; P.data[1][2] = -3384.5650572725663; P.data[1][3] = 15591209748.2791840000000;
P.data[2][0] = -0.0319353657001; P.data[2][1] = 0.0213700694115; P.data[2][2] = -0.9992614535500; P.data[2][3] = -54202.4500385644710;
return P;
}
ProjectionMatrix readProjectionMatrix2() {
ProjectionMatrix P;
P.data[0][0] = 5544.3514690660313; P.data[0][1] = 6327.0017140488953; P.data[0][2] = -4163.3807394948353; P.data[0][3] = -24333678727.6017230000000;
P.data[1][0] = 6125.8241039199602; P.data[1][1] = -5628.1100012630295; P.data[1][2] = -3404.8221606458665; P.data[1][3] = 15747662227.6377830000000;
P.data[2][0] = -0.0362355312611; P.data[2][1] = 0.0203282471373; P.data[2][2] = -0.9991365015065; P.data[2][3] = -48380.1370322157820;
return P;
}
// 图像处理函数
Mat processImage(const Mat& input) {
Mat output = input.clone();
if (output.channels() == 1) {
cvtColor(output, output, COLOR_GRAY2BGR);
}
else if (output.channels() == 4) {
cvtColor(output, output, COLOR_BGRA2BGR);
}
return output;
}
// 保存图像并验证
bool saveImageWithCheck(const string& path, const Mat& image, const string& name) {
if (image.empty()) {
cerr << "错误: " << name << " 图像为空!" << endl;
return false;
}
double minVal, maxVal;
minMaxLoc(image, &minVal, &maxVal);
cout << name << " 像素范围: " << minVal << " - " << maxVal << endl;
if (minVal == maxVal && minVal == 0) {
cerr << "警告: " << name << " 可能是全黑图像!" << endl;
}
Mat saveImg;
if (image.depth() == CV_16U) {
image.convertTo(saveImg, CV_8U, 1.0 / 256.0);
}
else {
image.convertTo(saveImg, CV_8U);
}
return imwrite(path, saveImg);
}
// 保存深度图
bool saveDepthMap(const string& path, const Mat& depthMap) {
if (depthMap.empty()) {
cerr << "错误: 深度图为空!" << endl;
return false;
}
// 归一化深度图以便可视化
Mat normalizedDepth;
normalize(depthMap, normalizedDepth, 0, 255, NORM_MINMAX, CV_8U);
return imwrite(path, normalizedDepth);
}
// 保存点云到PLY文件
void savePointCloudToPLY(const string& filename, const Mat& pointCloud, const Mat& colors) {
ofstream plyFile(filename);
if (!plyFile.is_open()) {
cerr << "无法创建PLY文件: " << filename << endl;
return;
}
// 计算有效点数量
int validPointCount = 0;
for (int i = 0; i < pointCloud.rows; i++) {
for (int j = 0; j < pointCloud.cols; j++) {
Vec3f point = pointCloud.at<Vec3f>(i, j);
if (point[2] > 0 && point[2] < 10000) { // 过滤无效点
validPointCount++;
}
}
}
// 写入PLY文件头
plyFile << "ply\n";
plyFile << "format ascii 1.0\n";
plyFile << "element vertex " << validPointCount << "\n";
plyFile << "property float x\n";
plyFile << "property float y\n";
plyFile << "property float z\n";
plyFile << "property uchar red\n";
plyFile << "property uchar green\n";
plyFile << "property uchar blue\n";
plyFile << "end_header\n";
// 写入点数据
for (int i = 0; i < pointCloud.rows; i++) {
for (int j = 0; j < pointCloud.cols; j++) {
Vec3f point = pointCloud.at<Vec3f>(i, j);
if (point[2] > 0 && point[2] < 10000) { // 有效深度范围
Vec3b color = colors.at<Vec3b>(i, j);
plyFile << point[0] << " " << point[1] << " " << point[2] << " "
<< (int)color[2] << " " << (int)color[1] << " " << (int)color[0] << "\n";
}
}
}
plyFile.close();
cout << "点云已保存至: " << filename << " (点数: " << validPointCount << ")" << endl;
}
int main() {
// 输入文件路径
string img1_path = "D:/data1/data1/017ER030.tif";
string img2_path = "D:/data1/data1/017ER031.tif";
string output_dir = "D:/data1/shuchu/";
// 确保输出目录存在
if (!utils::fs::createDirectory(output_dir)) {
cerr << "无法创建输出目录: " << output_dir << endl;
return -1;
}
// 读取图像
cout << "加载图像..." << endl;
Mat img1 = imread(img1_path, IMREAD_ANYCOLOR | IMREAD_ANYDEPTH);
Mat img2 = imread(img2_path, IMREAD_ANYCOLOR | IMREAD_ANYDEPTH);
if (img1.empty() || img2.empty()) {
cerr << "错误: 无法加载图像!" << endl;
return -1;
}
Size originalSize = img1.size();
cout << "原始图像尺寸: " << originalSize << endl;
// 处理图像
Mat img1_processed = processImage(img1);
Mat img2_processed = processImage(img2);
// 保存预处理图像
saveImageWithCheck(output_dir + "processed_img1.jpg", img1_processed, "预处理图像1");
saveImageWithCheck(output_dir + "processed_img2.jpg", img2_processed, "预处理图像2");
// 读取投影矩阵
ProjectionMatrix P1_struct = readProjectionMatrix();
ProjectionMatrix P2_struct = readProjectionMatrix2();
Mat P1(3, 4, CV_64F);
Mat P2(3, 4, CV_64F);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
P1.at<double>(i, j) = P1_struct.data[i][j];
P2.at<double>(i, j) = P2_struct.data[i][j];
}
}
// 分解投影矩阵
Mat K1(3, 3, CV_64F), R1(3, 3, CV_64F), t1(3, 1, CV_64F);
Mat M = P1(Rect(0, 0, 3, 3));
RQDecomp3x3(M, K1, R1);
t1 = K1.inv() * P1.col(3);
Mat K2(3, 3, CV_64F), R2(3, 3, CV_64F), t2(3, 1, CV_64F);
M = P2(Rect(0, 0, 3, 3));
RQDecomp3x3(M, K2, R2);
t2 = K2.inv() * P2.col(3);
// 计算相对位姿
Mat rel_R = R2 * R1.t();
Mat rel_t = t2 - rel_R * t1;
// ====== 生成接近原始图像的核线影像 ======
cout << "\n==== 生成接近原始图像的核线影像 ====" << endl;
Mat R1_o, R2_o, P1_o, P2_o, Q_o;
stereoRectify(K1, noArray(), K2, noArray(), originalSize,
rel_R, rel_t, R1_o, R2_o, P1_o, P2_o, Q_o,
CALIB_ZERO_DISPARITY, 0.5, originalSize);
// 生成映射
Mat map1x_o, map1y_o, map2x_o, map2y_o;
initUndistortRectifyMap(K1, noArray(), R1_o, P1_o, originalSize, CV_32FC1, map1x_o, map1y_o);
initUndistortRectifyMap(K2, noArray(), R2_o, P2_o, originalSize, CV_32FC1, map2x_o, map2y_o);
Mat original_like_left, original_like_right;
remap(img1_processed, original_like_left, map1x_o, map1y_o, INTER_LINEAR);
remap(img2_processed, original_like_right, map2x_o, map2y_o, INTER_LINEAR);
// 保存接近原始图像的核线影像
saveImageWithCheck(output_dir + "original_like_epipolar_left.jpg", original_like_left, "接近原始影像(左)");
saveImageWithCheck(output_dir + "original_like_epipolar_right.jpg", original_like_right, "接近原始影像(右)");
// ====== 立体匹配生成视差图 ======
cout << "\n==== 生成视差图 ====" << endl;
// 转换为灰度图
Mat gray_left, gray_right;
cvtColor(original_like_left, gray_left, COLOR_BGR2GRAY);
cvtColor(original_like_right, gray_right, COLOR_BGR2GRAY);
// 如果原始是16位图像,转换为8位
if (gray_left.depth() == CV_16U) {
gray_left.convertTo(gray_left, CV_8U, 1.0 / 256.0);
gray_right.convertTo(gray_right, CV_8U, 1.0 / 256.0);
}
// 创建SGBM对象
Ptr<StereoSGBM> sgbm = StereoSGBM::create(
0, // minDisparity
128, // numDisparities (必须是16的倍数)
5, // blockSize
8 * 5 * 5, // P1
32 * 5 * 5, // P2
1, // disp12MaxDiff
15, // preFilterCap
10, // uniquenessRatio
100, // speckleWindowSize
32, // speckleRange
StereoSGBM::MODE_SGBM_3WAY
);
// 计算视差图
Mat disparity_sgbm;
sgbm->compute(gray_left, gray_right, disparity_sgbm);
// 转换为浮点视差图 (实际视差 = disparity_sgbm / 16.0)
Mat disparity;
disparity_sgbm.convertTo(disparity, CV_32F, 1.0 / 16.0);
// 保存视差图
Mat disp_viz;
normalize(disparity, disp_viz, 0, 255, NORM_MINMAX, CV_8U);
imwrite(output_dir + "disparity_map.jpg", disp_viz);
cout << "视差图已保存: " << output_dir + "disparity_map.jpg" << endl;
// ====== 生成深度图 ======
cout << "\n==== 生成深度图 ====" << endl;
Mat depthMap(disparity.size(), CV_32F, Scalar(0));
// 从Q矩阵提取焦距和基线
double fx = abs(Q_o.at<double>(2, 3)); // |Q(2,3)| = 焦距
double baseline = 1.0 / Q_o.at<double>(3, 2); // 1/Q(3,2) = 基线长度
// 计算深度: depth = (f * baseline) / disparity
for (int i = 0; i < disparity.rows; i++) {
for (int j = 0; j < disparity.cols; j++) {
float disp_val = disparity.at<float>(i, j);
if (disp_val > 0) {
depthMap.at<float>(i, j) = (fx * baseline) / disp_val;
}
}
}
// 保存深度图
saveDepthMap(output_dir + "depth_map.jpg", depthMap);
cout << "深度图已保存: " << output_dir + "depth_map.jpg" << endl;
// ====== 生成三维点云 ======
cout << "\n==== 生成三维点云 ====" << endl;
Mat pointCloud;
reprojectImageTo3D(disparity_sgbm, pointCloud, Q_o, true);
// 保存点云
savePointCloudToPLY(output_dir + "point_cloud.ply", pointCloud, original_like_left);
cout << "\n==== 处理完成 ====" << endl;
cout << "输出文件目录: " << output_dir << endl;
cout << "生成文件: " << endl;
cout << " - 视差图: disparity_map.jpg" << endl;
cout << " - 深度图: depth_map.jpg" << endl;
cout << " - 点云文件: point_cloud.ply" << endl;
return 0;
}
```
### 关键实现说明
1. **立体匹配**:
- 使用StereoSGBM算法计算视差图
- 参数优化:`numDisparities=128`, `blockSize=5`, 使用`MODE_SGBM_3WAY`加速
2. **深度图计算**:
```cpp
depth = (f * baseline) / disparity
```
- 从Q矩阵提取焦距(f)和基线(baseline)
- 过滤无效视差值(disparity <= 0)
3. **点云生成**:
- 使用`reprojectImageTo3D`函数直接生成点云
- 保存为PLY格式,包含XYZ坐标和RGB颜色
- 过滤无效点(深度值<=0或过大)
4. **文件保存**:
- 视差图:归一化后保存为JPG
- 深度图:归一化后保存为JPG
- 点云:保存为ASCII格式的PLY文件
### 使用说明
1. **输入要求**:
- 左右视图核线影像(已校正)
- 投影矩阵(已在代码中定义)
2. **输出文件**:
- `disparity_map.jpg`:视差图(可视化)
- `depth_map.jpg`:深度图(可视化)
- `point_cloud.ply`:三维点云(可用MeshLab查看)
3. **参数调整**:
- 修改`StereoSGBM`参数以适应不同场景
- 调整深度过滤阈值(当前为0-10000)
- 修改点云保存格式(当前为ASCII)