python双目三维重建_OpenCV+OpenGL 双目立体视觉三维重建
绪论
本文旨在探讨双目立体视觉的核心目标:三维空间重构。文中详细阐述了实现三维重建的关键步骤。双目立体视觉的整体流程包括:图像采集过程、相机标定技术、特征提取(在稠密特征匹配中这一步可作为可选环节)、深度信息提取过程以及相应的三维重建运算。在实际应用中,我重点关注的是深度信息提取这一关键环节中的三角剖分算法与纹理映射技术相结合,并利用OpenCV框架与OpenGL技术实现相应的三维重建过程。
1.视差计算
1.1基于视差信息的三维重建
特征提取
采用双目立体视觉技术进行三维重建时,首先需完成的是立体匹配过程。该过程的核心任务是通过计算两幅图像中对应点之间的视差来实现深度信息的提取。在OpenCV软件库中,默认安装了features2d模块这一功能包,在该模块中提供了多种经典的算法可供使用。其中用于特征点定位的算法包括FAST、SIFT、SURF、MSER以及Harris等方法;而用于特征描述的算法主要有SURF和SIFT等方法;此外还有若干种基于上述不同模块的经典组合型匹配算法可供选择使用。需要注意的是这三个步骤均可独立选择,并支持任意组合配置以适应不同的应用需求。经过实验研究发现,在速度、提取的特征数量以及精度方面表现较为均衡且优化效果显著的是FAST角点检测器配合SURF特征描述子,并结合FLANN(Fast Library for Approximate Nearest Neighbors)进行快速近邻搜索匹配。
在匹配过程中需要采取一些措施来过滤误匹配。一种常用的方法是通过比较第一匹配结果与第二匹配结果之间的得分差异来判断是否存在足够的差异以避免误配对。这种方法有助于筛选出由于相似性导致的误配对。另一种方法则是利用已有的匹配点,并通过RANSAC算法计算出两幅图像之间的单应矩阵。然后将左视图中的坐标P应用单应矩阵映射至右视图中的Q点,并观察映射后的Q与预期配对结果Q’之间的欧氏距离是否较小。值得注意的是,在图像中存在深度信息的事实意味着Q和其预期配对点Q’之间必定会存在一定的差异。因此,在设置距离阈值时可以适当放宽范围以减少误判的可能性。我采用了这两种过滤策略以提高配准效果
另外,在图像中某些区域具有较多纹理细节而另一些区域则较为平滑缺乏丰富的纹理特征造成特征点分布不够均衡从而影响三维重建的效果为此我采用了以下方法限制特征点过于密集即当新加入的特征点与现有某一个特征点之间的距离过小时会被舍弃最终匹配效果如图所示其在精度和平滑度方面都有较好的表现
代码:
// select the corresponding point pairs in the stereo image pair to perform 3D reconstruction
void GetPair( Mat &imgL, Mat &imgR, vector &ptsL, vector &ptsR )
{
Mat descriptorsL, descriptorsR;
double tt = (double)getTickCount();
Ptr detector = FeatureDetector::create( DETECTOR_TYPE ); // factory mode
vector keypointsL, keypointsR;
detector->detect( imgL, keypointsL );
detector->detect( imgR, keypointsR );
Ptr de = DescriptorExtractor::create( DESCRIPTOR_TYPE );
//SurfDescriptorExtractor de(4,2,true);
de->compute( imgL, keypointsL, descriptorsL );
de->compute( imgR, keypointsR, descriptorsR );
tt = ((double)getTickCount() - tt) / getTickFrequency(); // 在处理620×555像素图像时表现出色,在SURF算法中仅需约2秒,在SIFT算法中则需约120秒
Ptr matcher = DescriptorMatcher::create( MATCHER_TYPE );
vector> matches;
matcher->knnMatch( descriptorsL, descriptorsR, matches, 2 ); // L:query, R:train
vector passedMatches; // save for drawing
DMatch m1, m2;
vector ptsRtemp, ptsLtemp;
for( size_t i = 0; i < matches.size(); i++ )
{
m1 = matches[i][0];
m2 = matches[i][1];
if (m1.distance < MAXM_FILTER_TH * m2.distance)
{
ptsRtemp.push_back(keypointsR[m1.trainIdx].pt);
ptsLtemp.push_back(keypointsL[i].pt);
passedMatches.push_back(m1);
}
}
Mat HLR;
HLR = findHomography( Mat(ptsLtemp), Mat(ptsRtemp), CV_RANSAC, 3 );
cout<
Mat ptsLt;
perspectiveTransform(Mat(ptsLtemp), ptsLt, HLR);
vector matchesMask( passedMatches.size(), 0 );
int cnt = 0;
for( size_t i1 = 0; i1 < ptsLtemp.size(); i1++ )
{
Point2f prjPtR = ptsLt.at((int)i1,0); // prjx = ptsLt.at((int)i1,0), prjy = ptsLt.at((int)i1,1);
// inlier
if( abs(ptsRtemp[i1].x - prjPtR.x) < HOMO_FILTER_TH &&
abs(ptsRtemp[i1].y - prjPtR.y) < 2) // restriction on y is more strict
{
vector::iterator iter = ptsL.begin();
for (;iter!=ptsL.end();iter++)
{
Point2f diff = *iter - ptsLtemp[i1];
float dist = abs(diff.x)+abs(diff.y);
if (dist < NEAR_FILTER_TH) break;
}
if (iter != ptsL.end()) continue;
ptsL.push_back(ptsLtemp[i1]);
ptsR.push_back(ptsRtemp[i1]);
cnt++;
if (cnt%1 == 0) matchesMask[i1] = 1; // don't want to draw to many matches
}
}
Mat outImg;
drawMatches(imgL, keypointsL, imgR, keypointsR, passedMatches, outImg,
Eigen::All::all(-1), Eigen::All::all(-1), matchesMask, DrawMatchesFlags NOT DRAW_SINGLE_POINTS);
char title[50];
sprintf_s(title, 50, "%.3f s, %d matches, %d passed", tt, matches.size(), cnt);
imshow(title, outImg);
waitKey();
}
p.s. 项目代码库中的基于特征点的视差运算存在缺陷,在此阶段仍处于调试阶段。期待经验丰富的同行提供帮助。
1.2基于块匹配的视差计算
在提取特征点的过程中,默认忽略了某个辅助信息:即对应的点应位于对应的极线上所限定的一个区间内。借助这一信息可以显著简化对应点的配准过程;具体来说,在一个像素周围选取的一个块内使用L1距离计算匹配距离即可;这正是OpenCV中所采用的块匹配算法的核心思路;与基于特征点的稀疏配准方法不同的是,在此方法中配准是“稠密”进行的;这种配准方法能够达到足够的精度水平;在图中使用浅色区域表示较大的视差程度;对应的深度相对较浅的位置;左侧存在一块区域,在左、右视图之间并未有重叠部分;因此在此区域内无法进行有效的视差计算。
经分析可知,在视差计算结果中存在较多噪声。值得注意的是,在纹理较为平滑的区域内以及左右视图中存在不同遮挡的情况。基于此考虑,在修复过程中我采用了最近邻插值方法配合数学形态学去噪技术(见cvFuncs2.cpp中的FixDisparity函数):
// roughly smooth the glitches on the disparity map
void FixDisparity( Mat_ & disp, int numberOfDisparities )
{
Mat_ disp1;
float lastPixel = 10;
float minDisparity = 23;// algorithm parameters that can be modified
for (int i = 0; i < disp.rows; i++)
{
for (int j = numberOfDisparities; j < disp.cols; j++)
{
if (disp(i,j) <= minDisparity) disp(i,j) = lastPixel;
else lastPixel = disp(i,j);
}
}
int an = 4; // algorithm parameters that can be modified
copyMakeBorder(disp, disp1, an,an,an,an, BORDER_REPLICATE);
Mat element = getStructuringElement(MORPH_ELLIPSE, Size(an2+1, an2+1));
morphologyEx(disp1, disp1, CV_MOP_OPEN, element);
morphologyEx(disp1, disp1, CV_MOP_CLOSE, element);
disp = disp1(Range(an, disp.rows-an), Range(an, disp.cols-an)).clone();
}
对应点的选取
为了获得较为理想的重构效果,在深度变化显著的区域选取特征点更为合适。基于这一猜想,在处理该视差图时首先对其进行梯度计算,并找出具有最大梯度值的主要像素点位置及其方向特性。如果观察到主要沿x轴方向发生较大变化,则应在该最大值所在位置附近左右各取几个相邻像素作为候选采样点;否则则应在上下方附近选取若干个候选采样位置来进行分析比较。根据所选两个候选采样位置处计算得到的视差值即可推导出相应另一个视角下的对应采样坐标位置信息。为了保证所选样本间的分布均匀性,在每次记录下一对符合要求的特征采样位置后都需要将这些周围一圈像素范围内的梯度值置零以避免重复提取同一类别的样本信息;随后再从剩余未被覆盖的位置中继续寻找下一个最大幅值的位置并以此类推直至满足所需的总特征样本数量要求
特征点不一定全部选取在急剧变化的地方;此外,在平缓的变化区域内也可以选取部分特征点。最终所选的特征点如图所示。
其中,在较为平缓的区域中获取到了紫色标记点;其余颜色则是在边界区域获取到。这些算法的具体实现可参考选择关键点函数ChooseKeyPointsBM
2.计算世界坐标
通常在双目立体视觉实验中使用的图像都是通过外极线矫正处理的。计算3D坐标相对容易实现。实际上,借助外极线约束以及其他约束条件的帮助,可以显著地降低立体匹配所需的计算量。如图所示:
如果(x1,y1),(x2,y2)以各自图像上的像素坐标表示;L和(X,Y,Z)以毫米为单位表示;f采用像素作为单位的话,则可以通过应用相似三角形的知识来推导出结果。
其中W、H分别代表图像宽度与高度(以像素计),y被定义为y1与y2的平均值。为了遵循右手坐标系的标准,Z轴方向需取反;而Y轴取反的原因在于图像成像时上下方向被颠倒了。三维空间原点设置在左摄像机的焦点位置,并且计算的具体实现可参考cvFunc.cpp中的StereoTo3D函数实现。
// calculate 3d coordinates.
// for rectified stereos: pointLeft.y == pointRight.y
// the origin for both image is the top-left corner of the left image.
// the x-axis points to the right and the y-axis points downward on the image.
// the origin for the 3d real world is the optical center of the left camera
// object -> optical center -> image, the z value decreases.
void StereoTo3D( vector ptsL, vector ptsR, vector &pts3D,
float focalLenInPixel, float baselineInMM, Mat img,
Point³F ¢er³D, Vec³F &size³D) // 输出变量及其对应的中心位置和尺寸大小
{
vector::iterator iterL = ptsL.begin(),
iterR = ptsR.begin();
float xl, xr, ylr;
float imgH = float(img.rows), imgW = float(img.cols);
Point3f pt3D;
float minX = 1e9, maxX = -1e9;
float minY = 1e9, maxY = -1e9;
float minZ = 1e9, maxZ = -1e9;
Mat imgShow = img.clone();
char str[100];
int ptCnt = ptsL.size(), showPtNum = 30, cnt = 0;
int showIntv = max(ptCnt/showPtNum, 1);
for ( ; iterL != ptsL.end(); iterL++, iterR++)
{
xl = iterL->x;
xr = iterR->x; // need not add baseline
ylr = (iterL->y + iterR->y)/2;
// if (yl - yr > 5 || yr - yl > 5) // 可能有错误对应的情况, 丢弃. 但是向量在迭代过程中无法被更改.
//{}
pt3D.z = -focalLenInPixel * baselineInMM / (xl - xr); // xl must be larger than xr, provided that xl is shot by the left camera.
pt3D.y = -(-ylr + imgH/2) * pt3D.z / focalLenInPixel;
pt3D.x = (imgW/2 - xl) * pt3D.z / focalLenInPixel;
minX = min(minX, pt3D.x); maxX = max(maxX, pt3D.x);
minY = min(minY, pt3D.y); maxY = max(maxY, pt3D.y);
minZ = min(minZ, pt3D.z); maxZ = max(maxZ, pt3D.z);
pts3D.push_back(pt3D);
if ((cnt++)%showIntv == 0)
{
Scalar color = CV_RGB(rand()&64,rand()&64,rand()&64);
sprintf_s(str, 100, "%.0f,%.0f,%.0f", pt3D.x, pt3D.y, pt3D.z);
putText(imgShow, str, Point(xl-13,ylr-3), FONT_HERSHEY_SIMPLEX, .3, color);
circle(imgShow, *iterL, 2, color, 3);
}
}
imshow("back project", imgShow);
waitKey();
center3D.x = (minX+maxX)/2;
center3D.y = (minY+maxY)/2;
center3D.z = (minZ+maxZ)/2;
size3D[0] = maxX-minX;
size3D[1] = maxY-minY;
size3D[2] = maxZ-minZ;
}
3.三角剖分
3.1 三角剖分简介
为了后续进行纹理贴图的过程, 我调用了OpenCV库中的Delaunay triangulation函数来实现这一过程. 该算法能够保证生成的所有三角形都具有最大的最小角. 割除过程如下所示:
基于Delaunay算法,在这种情况下将平面划分为包含所有分割点的小三角形区域,并不断迭代以完成计算过程。在这种情况中, 对偶划分即为输入二维点集对应的Voronoi图。这种划分方法可被用来实现对平面进行三维分段变换, 形态变换以及实现平面点的关键定位, 同时还可以用于建立特定图结构(如NNG,RNG)等关键应用
从表中可以看出
3.2 Bowyer-Watson算法
当前采用逐点插入法生成Delaunay三角网的算法主要依赖于Bowyer-Watson算法;该算法的主要步骤如下:
初始化过程如下:
- 初始化三角网格:基于给定点集V,确定一个包含所有这些点的矩形区域R,并将其定义为辅助窗口。沿着矩形的一条对角线绘制线条,从而分割出两个三角形区域作为初始Delaunay三角网格.
当我们在现有Delaunay三角网格T中加入一个新的点P时
- 移除辅助区域R: 如同步骤2所述, 当在所有点V均被成功插入至三角形网格之后, 删除所有包含在辅助区域R内的顶点
在这些步骤中,在每一个新数据点被插入时都会依次执行以下操作:迅速定位该数据点所属的三角形区域、识别其对邻居区域的影响并生成相应的Delaunay空腔结构。当新增的数据点数量增多时,在这种情况下形成的三角网格数量会呈现快速增长的趋势。因此必须设法减少这两个步骤所需的时间投入以便提升整体算法运行效率
算法执行图示如下:
3.3 三角剖分代码分析
在cvFuncs.cpp文件中找到实现三角剖分的函数名为TriSubDiv,在这个函数内部会生成相应的几何分割结果;为了便于后续处理运算需求,在程序运行过程中会将提取出来的特征点按照一定的规则有序地存放在一个标准容器类vector容器变量中;同样的处理流程也会对计算得到的结果进行精确存放;其中Vec3i是一个整型数组类型容器变量名,在这个数组内部会被保存下三个用于标识三角形顶点号的具体整数值
我们需要记录Delaunay内存区域以及一个包围盒(该包围盒用于生成虚拟三角形的边界框)。
该区域用于存储Delaunay细分的架构,在三角剖分中发挥关键作用
//
CvRect rect = { 0, 0, 600, 600 }; //Our outer bounding box //我们的外接边界盒子
CvMemStorage* storage; //用来存储Delaunay三角剖分 //该代码段声明了一个CvMemStorage类型的指针变量storage,并用于实现Delaunay三角剖分过程
storage = cvCreateMemStorage(0); //Initialize the storage //初始化存储器
CvSubdiv2D* subdiv; //The subdivision itself // 细分
subdiv = init_delaunay( storage, rect); //该函数的功能描述:下面将详细介绍该函数的实现过程. //函数返回CvSubdiv类型指针
初始化Delaunay三角剖分函数的定义如下:这是一个由多个OpenCV相关功能模块组成的软件包,并且其中包含了实现Delaunay三角剖分算法的过程。该软件包中的一个关键功能就是init_delaunay函数本身。
// INITIATE an ease-of-use function for Delaunay triangulation // facilitates the initialization of a mesh in Delaunay triangulation
//
CvSubdiv2D* init_delaunay(CvMemStorage* storage,CvRect rect) {
CvSubdiv2D* subdiv;
subdiv = cvCreateSubdivision2D(CVSubdivType, sizeof subdiv, sizeof CvSubdivVertex, sizeof CvQuadEdge, storage);//为数据申请空间
cvInitSubdivDelaunay2D( subdiv, rect ); //rect sets the bounds
return subdiv;//返回申请空间的指针
}
该种方法可视为对离散点集合进行处理的过程;由此可知,在确定了离散点集合后即可得到其对应的三角剖分;那么如何实现这一过程呢?
这些点必须是32位浮点型,并通过下面的方式插入点:
CvPoint2D32f fp; //This is our point holder//这是我们点的持有者(容器)
for( i = 0; i < as_many_points_as_you_want; i++ ) {
// 无论您如何设置点
If our point set is not 32-bit, here we convert it to CvPoint2D32f with two specific implementation approaches.
//
fp = your_32f_point_list[i];
cvSubdivDelaunay2DInsert( subdiv, fp );
}
转换为CvPoint2D32f的两种方法:
1)通过宏cvPoint2D32f(double x,double y)
该库中的cvPointTo32f函数可将整数坐标点轻松转换为32位浮点数坐标。
当通过输入一个散点集合来实现Delaunay三角剖分时,在此之后我们将利用两个特定函数来配置并清除相关区域的Voronoi划分。
cvCalcSubdivVoronoi2D(subdivision); // 在细分结构中填充与之相关的Voronoi数据 //
cvCallFunction(cvClearSubdivVoronoi2D, subdiv); // 调用函数cvClearSubdivVoronoi2D来处理参数subdiv // 清除从subdiv中获取的Voronoi区域
CvSubdiv2D结构如下:
#define CV_SUBDIV2D_FIELDS() \
CV_GRAPH_FIELDS() \
int quad_edges; \
int is_geometry_valid; \
CvSubdiv2DEdge recent_edge; \
CvPoint2D32f topleft; \
CvPoint2D32f bottomright;
typedef struct CvSubdiv2D
{
CV_SUBDIV2D_FIELDS()
}
CvSubdiv2D;
#define CV_GRAPH_FIELDS() \
CV_SET_FIELDS() /* set of vertices */ \
CvSet edges; / set of edges */
#define CV_SET_FIELDS() \
CV_SEQUENCE_FIELDS() /*inherits from [#CvSeq CvSeq] */ \
struct CvSetElem* free_elems; /*list of free nodes */
整体代码如下:
void TriSubDiv( vector &pts, Mat &img, vector &tri )
{
CvSubdiv2D* subdiv;//The subdivision itself // 细分
CvMemStorage* storage = cvCreateMemStorage(0); // 用于生成Delaunay三角划分
Assign the top-left corner of the rectangle to the origin point (0, 0) and specify its width and height as the dimensions of the input image. // our outer bounding box used for image processing purposes.
subdiv = cvCreateSubdiv2D( CV_SEQ_KIND_SUBDIV2D, sizeof(*subdiv),
sizeof(CvSubdiv2DPoint),
sizeof(CvQuadEdge2D),
storage );//为数据申请空间
cvInitSubdivDelaunay2D( subdiv, rc );//rect sets the bounds
//如果我们的点集未采用32位精度,在本处将该点集转换为CvPoint2D32f类型的具体实现方式有两种
for (size_t i = 0; i < pts.size(); i++)
{
CvSubdiv2DPoint *pt = cvSubdivDelaunay2DInsert( subdiv, pts[i] );
pt->id = i;
}
CvSeqReader reader;
int total = subdiv->edges->total;
int elem_size = subdiv->edges->elem_size;
cvStartReadSeq( (CvSeq*)(subdiv->edges), &reader, 0 );
Point buf[3];
const Point *pBuf = buf;
Vec3i verticesIdx;
Mat imgShow = img.clone();
srand( (unsigned)time( NULL ) );
for( int i = 0; i < total; i++ )
{
CvQuadEdge2D* edge = (CvQuadEdge2D*)(reader.ptr);
if( CV_IS_SET_ELEM( edge ))
{
CvSubdiv2DEdge t = (CvSubdiv2DEdge)edge;
int iPointNum = 3;
Scalar color = CV_RGB(rand()&255,rand()&255,rand()&255);
//bool isNeg = false;
int j;
for(j = 0; j < iPointNum; j++ )
{
CvSubdiv2DPoint* pt = cvSubdiv2DEdgeOrg( t );
if( !pt ) break;
buf[j] = pt->pt;
//if (pt->id == -1) isNeg = true;
verticesIdx[j] = pt->id;
t = cvSubdiv2DGetEdge( t, CV_NEXT_AROUND_LEFT );
}
if (j != iPointNum) continue;
if (isGoodTri(verticesIdx, tri))
{
//tri.push_back(verticesIdx);
polylines( imgShow, &pBuf, &iPointNum,
1, true, color,
1, CV_AA, 0);
//printf("(%d, %d)-(%d, %d)-(%d, %d)\n", buf[0].x, buf[0].y, buf[1].x, buf[1].y, buf[2].x, buf[2].y);
//printf("%d\t%d\t%d\n", verticesIdx[0], verticesIdx[1], verticesIdx[2]);
//imshow("Delaunay", imgShow);
//waitKey();
}
t = (CvSubdiv2DEdge)edge+2;
for(j = 0; j < iPointNum; j++ )
{
CvSubdiv2DPoint* pt = cvSubdiv2DEdgeOrg( t );
if( !pt ) break;
buf[j] = pt->pt;
verticesIdx[j] = pt->id;
t = cvSubdiv2DGetEdge( t, CV_NEXT_AROUND_LEFT );
}
if (j != iPointNum) continue;
if (isGoodTri(verticesIdx, tri))
{
//tri.push_back(verticesIdx);
polylines( imgShow, &pBuf, &iPointNum,
1, true, color,
1, CV_AA, 0);
//printf("(%d, %d)-(%d, %d)-(%d, %d)\n", buf[0].x, buf[0].y, buf[1].x, buf[1].y, buf[2].x, buf[2].y);
//printf("%d\t%d\t%d\n", verticesIdx[0], verticesIdx[1], verticesIdx[2]);
//imshow("Delaunay", imgShow);
//waitKey();
}
}
CV_NEXT_SEQ_ELEM( elem_size, reader );
}
//RemoveDuplicate(tri);
char title[100];
sprintf_s(title, 100, "Delaunay: %d Triangles", tri.size());
imshow(title, imgShow);
waitKey();
}
平面分割是将一个平面划分为多个不重叠且能完全覆盖该平面的区域集合。该结构CvSubdiv2D基于二维点集构建了分区系统,在这些点之间相互连接形成一个平面图形,并通过结合外部划分点(称为凸形点)形成的边线将整个区域划分为多个小部分。
每个划分操作都对应一个其对应的对偶划分。这种对应关系的核心在于小区域与划分数值顶点之间发生转换关系,在构建其对应的对偶划分数值时,默认将每个原始的小区域视为一个新的虚拟顶点,并将原来的划分数值视为新的基本单元。如图所示,在这种结构下绘制出的图形中,默认将每个原始的小区域视为一个新的虚拟顶点(称为虚拟点),并将原来的划分数值作为新的基本单元进行绘制处理。其中实线用于表示原始划分数值范围
4.三维重构
为了确保三维重建的效果, 通常需要对深度图像进行后续处理. 从深度图像中恢复出高质量的视差图, 对其的要求包括:
为了确保三维重建的效果, 通常需要对深度图像进行后续处理. 从深度图像中恢复出高质量的视差图, 其对原始数据的要求包括:
①深度图像中,物体的边界必需与图像中物体的边界对齐;
②在场景图中,深度图像要尽可能均勻和平滑,即对图像进行平滑处理。
三维重构的核心思路较为直接:通过OpenGL中的纹理贴图技术,在计算出的三维坐标位置上逐一将平面图像中的三角形进行映射处理即可完成建模过程。为了便于直观观察3D效果,在构建完成之后还可以附加交互功能:通过方向键实现模型的不同方向旋转,在线调节缩放比例;此外还支持使用gluLookAt函数来模拟视角变化的效果。所有关于三维重构的具体代码实现均包含在glFuncs.cpp文件中
纹理贴图:
GLuint Create3DTexture( Mat &img, vector &tri,
vector pts2DTex, vector &pts3D,
Point3f center3D, Vec3f size3D )
{
GLuint tex = glGenLists(1);
int error = glGetError();
if (error != GL_NO_ERROR)
cout << "An OpenGL error has occured: " << gluErrorString(error) << endl;
if (tex == 0) return 0;
Mat texImg;
cvtColor(img, img, CV_BGR2RGB);
resize(img, texImg, Size(512,512)); // seems no need to do this
glNewList(tex, GL_COMPILE);
vector::iterator iterTri = tri.begin();
//vector::iterator iterPts3D = pts3D.begin();
Point2f pt2D[3];
Point3f pt3D[3];
glDisable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
for ( ; iterTri != tri.end(); iterTri++)
{
Vec3i &vertices = *iterTri;
int ptIdx;
for (int i = 0; i < 3; i++)
{
ptIdx = vertices[i];
if (ptIdx == -1) break;
//else cout<
pt2D[i].x = pts2DTex[ptIdx].x / img.cols;
pt2D[i].y = pts2DTex[ptIdx].y / img.rows;
pt3D[i] = (pts3D[ptIdx] - center3D) * (1.f / max(size3D[0],size3D[1]));
//pt3D[i].z -= offset;
}
if (ptIdx != -1)
{
MapTexTri(texImg, pt2D, pt3D);
//cout<
}
}
glDisable(GL_TEXTURE_2D);
glEndList();
return tex;
}
效果展示及不足
Cloth图像是重构效果比较好的一组:
虽然无法直接将文本复制粘贴到此处,并生成相应的Markdown格式输出,请稍后尝试重新组织您的文本以满足Markdown语法需求后再提交。您可以尝试将文本按照以下格式进行整理
深入探究导致这种效果的原因方面,在3D坐标计算上存在一定的偏差。然而左右视图可能出现因遮挡或偏移等因素而造成的差异性问题;因此匹配到的特征点实际上并非来自同一三维世界的同一点;这种无法消除的误差是不可避免的;而造成最终效果显著下降的主要原因还是图像中深度变化显著;尽管在正前方观察时还能保持相对正常;但一旦发生旋转就会导致纹理变形扭曲的问题出现;为了解决这一问题应当尝试将特征点选取集中在深度变化较为剧烈的位置;通常选择图像边界区域作为采集目标;然而目前采用的特征点检测算法主要倾向于捕捉角点及纹理密集区域;因此可以考虑在对应点匹配方法上进行改进
为了进一步提升效果, 可以通过先对视差图像进行分割, 把图像划分为几个视差较为连续的区域分别进行贴图处理; 对于那些出现剧烈变化而发生严重变形的地方, 素质较低或者出现断裂的情况, 就无需将那些发生严重变形的纹理进行贴图处理。我在尝试不同的分割方法时取得了不错的成效, 如图所示, 这可能会带来更佳的效果; 不过受时间和精力限制, 因此就无法继续推进下去了。
为了开发上述实现的两种视差计算方法,在main函数之前配置了变量g_algo以便于切换不同的算法
参考文献:
代码下载
