本文是藉助openMesh庫進行三維重建的孔洞填補的。openMesh裏面有非常優秀的三維的數據結構。
核心:
找到三維模型當中所有的空洞,對於每一個孔洞,找出其所有的半邊然後對其進行排序;對排序過的所有的半邊,找到角度最小的兩條邊,增加第三條邊,形成新的三角面片;如此迭代。具體的每一步的算法如下:
半邊排序算法:
對於一個孔洞,找出其中的一條半邊作爲起始邊,遍歷所有其他的半邊集合,找到以該條半邊的尾點作爲起點的半邊;如此迭代,直到找到某一條邊的尾點是第一條邊的起點爲止。計算角度:
對於上面這兩種孔洞,箭頭處可能角度都比較的小,但是圖2 當中就是不能填補的。因此,計算的角度是和他們的方向是有關的。
計算的方法是:首先求出這個角度的大小(利用向量求出來一個0~180度之間的一個值angle,然後再確定由這兩條邊組成的一個面的法向量是否和第一條邊對面的邊所在的面的法向量一致,一致則輸出角度,不一致則輸出: 360-angle;計算距離:
兩點的歐拉距離計算公式;
- 單個孔洞填補算法:
如下圖所示:
如果 兩點之間的距離過長(圖3),則考慮在中間插入一個點(實際上是替換點),增加兩個三角面片,用最外側的兩個半邊句柄更新整個半邊句柄;否則直接增加一個三角面片,更新句柄(圖4)。
關鍵代碼:
// 找到一個洞的半邊集合, 排序
void DFS(int nNum, int n)// 在這n條半邊當中找到並排序到 有序邊對列的第 nNum 條處。 但這個只能是對一個孔洞
{
if (toTemp[nNum - 1] == fromTemp[0])// 首尾相接的意思吧,就是說找到最後一個了。 也就是完成了所有的邊的排序
{
if (nNum < vNum)
{
vNum = nNum;//原來分配的空間大了, 這是這個環裏面的邊的條數, ******** 而n表示的是總的半邊的數目
for (int i = 0; i < vNum;++i )
{
fromV[i] = fromTemp[i];// 直到找到了所有的半邊後,纔將所有的半邊放入有序的半邊集合,這樣節省空間
toV[i] = toTemp[i];
}
}
return;
}
for (int i = 1; i < n;i ++)// n是給定的值,半邊的個數
{
if (!vis[i] && toTemp[nNum - 1] == fromVetex[i])// 找到沒有存儲的且以 上一個尾點 爲 起點的邊
{
fromTemp[nNum] = fromVetex[i];
toTemp[nNum] = toVetex[i];
vis[i] = true;// 找到後,在空洞的半邊集合的標記爲true
DFS(nNum + 1, n);
vis[i] = false;// 這個好像有點問題 ?????????????? 不可能出現一對多的情況,不理解爲什麼要做這個
}
}
}
//修復孔洞
void CMeshFillHole::DoRepairHole(ICLTriMesh* mesh)
{
std::vector<ICLTriMesh::VertexHandle> AddedFace;
int i, edge_num = 0;
if (mesh == nullptr) {return;}
// 獲取孔洞半邊集合
for (ICLTriMesh::HalfedgeIter it = mesh->halfedges_begin(); it != mesh->halfedges_end(); ++it)// 讀入mesh 當中的所有半邊
{
ICLTriMesh::HalfedgeHandle he = it.handle();// 半邊的句柄
if(mesh->is_boundary(he))//is_boundary()非常有效,找到所有的孔洞半邊集合
{
fromVetex[edge_num] = mesh->from_vertex_handle(he);// 找到孔洞的每一條半邊
toVetex[edge_num++] = mesh->to_vertex_handle(he);
}
}
if (edge_num == 0) {return;}
// 整理孔洞半邊集合
memset (vis, false, sizeof (vis));//在一段內存塊中填充某個給定的值,它是對較大的結構體或數組進行清零操作的一種最快方法
//srand((unsigned)time(0));//這個也是關鍵,是放入隨機數種子,通常都是放系統當前時間的。
//int index= rand()%edge_num;//edge_num
//cout<<"the index is: "<<index<<endl;
fromTemp[0] = fromVetex[0];// 將第一條半邊賦給temp, test: 換爲index
toTemp[0] = toVetex[0];
//vis[index]=true;
vis[0] = true;
vNum = 10001;
DFS(1, edge_num);// edge_num得到的是最終的外邊的個數
cout<<"the total number of edges: "<<edge_num<<endl;
cout<<"The number in this hole is : "<<vNum<<endl;
// 計算孔洞的平均長度??? 這裏是在計算半邊的平均長度吧。
double sum = 0;
for (i = 0; i < vNum;i++)
{
auto s = mesh->point(fromV[i]);//
auto e = mesh->point(toV[i]);
sum += cal_dis(s, e);
}
double avg_len = sum / vNum;
// 補三角片
while (vNum > 3)
{
// 計算出每個夾角,並找到最小的
double min_ang = 360;
int pos = 0, nxt;
ICLTriMesh::VertexHandle vVetex0, vVetex1, vVetex2;
for (i = 0; i < vNum;i ++)
{
nxt = (i + 1) % vNum;// 這個vNum 是排序後的半邊的個數
auto s1 = mesh->point(fromV[i]);
auto e1 = mesh->point(toV[i]);
auto s2 = mesh->point(fromV[nxt]);
auto e2 = mesh->point(toV[nxt]);
//求法矢
ICLTriMesh::Normal v1(s1.data()[0]-e1.data()[0], s1.data()[1]-e1.data()[1], s1.data()[2]-e1.data()[2]);
ICLTriMesh::Normal v2(e2.data()[0]-s2.data()[0], e2.data()[1]-s2.data()[1], e2.data()[2]-s2.data()[2]);// ****非常好,這裏一定要反過來計算
// 找出最小角所在半邊
ICLTriMesh::HalfedgeHandle minPointHaleAge;
for(ICLTriMesh::HalfedgeIter itx = mesh->halfedges_begin(); itx != mesh->halfedges_end(); ++itx) // 這裏爲什麼不寫auto了
{
ICLTriMesh::HalfedgeHandle tmp = itx.handle();// 再一次回到mesh當中去找,這個有點。。。。。
if(mesh->from_vertex_handle(tmp) == fromV[i] && mesh->to_vertex_handle(tmp) == toV[i]) // 其實可以在第一個找孔洞半邊的時候就應該把 CLTriMesh::HalfedgeHandle存下來,這樣就不用再到mesh整體當中去遍歷了
{
minPointHaleAge = tmp;//可以從isBoundray()的半邊裏面去找
break;
}
}
double angle = cal_ang(v1, v2, mesh, minPointHaleAge);
if(angle < min_ang)// 在所有的邊當中找到角度最小的 角度最小的進行擴充
{
min_ang = angle;
pos = i;
vVetex0 = fromV[i];
vVetex1 = toV[i];
vVetex2 = toV[nxt];
}
}
// 計算第三邊長度dis
ICLTriMesh::Point p0 = mesh->point(vVetex0);
ICLTriMesh::Point p1 = mesh->point(vVetex1);
ICLTriMesh::Point p2 = mesh->point(vVetex2);
double dis = cal_dis(p1, p2);// p0-p1構成的那一條邊本來就是有的
// 當 dis > 2 * avg_len 時加兩個三角形, 加的位置就是這p0 和 p2 兩個點的中間
if (dis > 2 * avg_len)
{
ICLTriMesh::Point newPoint((p0.data()[0]+p2.data()[0])/2, (p0.data()[1]+p2.data()[1])/2, (p0.data()[2]+p2.data()[2])/2);
ICLTriMesh::VertexHandle newVertexHandle = mesh->add_vertex(newPoint);
toV[pos] = newVertexHandle;
fromV[(pos + 1)%vNum] = newVertexHandle;// 下一次則從新加入的第二條半邊入手
AddedFace.clear();
AddedFace.push_back(vVetex0);
AddedFace.push_back(vVetex1);
AddedFace.push_back(newVertexHandle);
mesh->add_face(AddedFace);
AddedFace.clear();
AddedFace.push_back(vVetex1);
AddedFace.push_back(vVetex2);
AddedFace.push_back(newVertexHandle);
mesh->add_face(AddedFace);
cout<<"加入了兩個: "<<i<<endl;
}
else // 否則加一個三角形
{
AddedFace.clear();
AddedFace.push_back(vVetex0);
AddedFace.push_back(vVetex1);
AddedFace.push_back(vVetex2);
mesh->add_face(AddedFace);
toV[pos] = toV[(pos + 1) % vNum];
if (pos + 1 == vNum)
{pos = -1;}
for (i = pos + 1; i < vNum - 1;i ++)
{
fromV[i] = fromV[i + 1];
toV[i] = toV[i + 1];//意思是從下 兩個節點開始
}
vNum --;
cout<<"vNum: "<<vNum<<endl;
}
}
if (vNum <= 3)//如果是這樣直接組成一個面(這裏是有問題的, 如果這裏多個空始終達不到這個條件)
{
if (vNum != 0)
{
ICLTriMesh::VertexHandle vVetex1 = fromV[0];// 怎麼只有 from勒?因爲to也是一樣的
ICLTriMesh::VertexHandle vVetex0 = fromV[1];
ICLTriMesh::VertexHandle vVetex2 = fromV[2];
AddedFace.clear();
AddedFace.push_back(vVetex1);
AddedFace.push_back(vVetex0);
AddedFace.push_back(vVetex2);
mesh->add_face(AddedFace);
}
return;
}
}
以上是兩個關鍵的代碼,但是隻適合單孔的情況。
效果展示
該算法還有很多可以優化的點,以至於提高運行的速度。