多視角三維模型紋理對映 01

SimpleTriangle 發表於 2022-01-26

本片文章算是作為上一篇文章【G2O與多視角點雲全域性配準優化】 的延續。
從上篇文章槓已知,現在目前手頭已經有了配準好的全域性點雲、每塊點雲的變換矩陣以及對應每塊點雲的RGB影像。接下來,自然而然的便是:對全域性點雲重建三維網格,並對重建後的三維網格進行紋理貼圖。
已知:三維模型(三角面片,格式任意),多視角RGB圖片(紋理)、變換矩陣;求:為三維模型貼上紋理圖。
針對上述待求解紋理,熟悉的小夥伴肯定直到,其實在求解多視角立體匹配的最後一步--表面重建與紋理生成。
接上篇,為完成自己的目的,首先需要重構三角網格,從點雲重建三角網格,與相機、紋理圖片等沒有任何關係,可直接使用pcl中的重建介面或者cgal甚至meshlab等軟體,直接由三維點雲重建三角網格。
在完成重建三角網格之前,還有很重要--但是可以酌情省略的一個關鍵步驟:多視角三維點雲融合!下面繼續使用自己之前所拍攝的8個點雲片段為例進行示例記錄。(注意,此處強調8個點雲片段,並不表示8個視角的點雲,後續說明)。

多視角點雲融合

將多個點雲片段拼接之後,無法避免的存在多個視角下點雲相互重疊的情況。如下圖所示,在重疊區域的點雲疏密程度一般都大於非重疊區域點雲。進行點雲融合的目的一是為了點雲去重,降低點雲資料量;二是為了進一步平滑點雲,提高精度,為後續的其他計算(如測量等)提供高質量的資料。

重疊

恰巧,自己對移動最小二乘演算法有一定了解,在pcl中,N年之前【pcl之MLS平滑】和【pcl之MLS演算法計算並統一法向量】也做過一些小測試。在我的印象中,移動最小二乘是這樣工作的:針對深入資料,計算目標點及其鄰域,這些待計算的區域性資料便構成了目標點的緊支域,在緊支域內有某個函式對目標點進行運算,運算的基本規則是依據緊支域內其他點到目標點的權重不同,這裡的某個函式即所謂的緊支函式緊支函式 + 緊支域 +權重函式構成了移動最小二乘演算法的基本數學概念,所謂移動性則體現在緊支域在允許的空間中“滑動”計算,直至覆蓋所有資料。最小二乘一般針對全域性求最優,而移動最小二乘由於其 “移動性”(緊支)不僅能針對全域性優化求解,而且也具有區域性優化性,更進一步的,針對三維點雲,其能夠提取等值面,結合MC(移動立方體)演算法,實現表面三角網格的重建。

pcl中,關於MLS演算法的例子很多很多,自己之前的兩篇水文也算也有個基本的應用介紹,此處不在過多記錄。在這裡,直接使用如下程式碼:

pcl::PointCloud<pcl::PointXYZRGB> mls_points; //存結果
pcl::search::KdTree<pcl::PointXYZRGB>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZRGB>);
pcl::MovingLeastSquares<pcl::PointXYZRGB, pcl::PointXYZRGB> mls;
mls.setComputeNormals(false);
mls.setInputCloud(res);
mls.setPolynomialOrder(2); //MLS擬合的階數,
mls.setSearchMethod(tree);
mls.setSearchRadius(3);

mls.setUpsamplingMethod(pcl::MovingLeastSquares<pcl::PointXYZRGB, pcl::PointXYZRGB>::UpsamplingMethod::VOXEL_GRID_DILATION);
mls.setDilationIterations(0);  //設定VOXEL_GRID_DILATION方法的迭代次數

mls.process(mls_points);

注意,上述計算過程中,使用了pcl::MovingLeastSquares::VOXEL_GRID_DILATION方法,按照pcl官方解釋,該方法不僅能夠修復少量點雲空洞,而且能夠對點雲座標進行區域性優化,輸出為全域性疏密程度一樣的點雲,通過設定不同的迭代次數,該方法不僅能夠降取樣,還能上取樣(主要通過迭代次數控制).

將自己的資料放入上述計算過程,點雲融合區域性效果如下:

多視角三維模型紋理對映  01

肉眼可見點雲更加均勻、平滑,同同時資料量從100w+ 降到 10w+.

上述操作,基本滿足了自己下一步的要求。

進一步的,鑑於表面重建並非重點,針對上述結果,這裡直接使用MeshLab軟體中的泊松重建,結果記錄如下:

多視角三維模型紋理對映  01

多視角紋理對映

在這裡,正式引入自己對所謂“多視角”的理解。

首先,多視角多角度,通常情況下,這兩個概念基本都是在說同一件事情,即從不同方位角拍攝三維模型。但是這其中又隱含兩種不同的 [操作方式],其一如手持式三維掃描器、SLAM中的狀態估計、甚至搭載攝像機/雷達的自動駕駛汽車等,這些場景下基本都是相機在動,目標不動,即相機繞目標運動;其二 如轉檯式三維掃描器等,這些場景基本都是相機不動,目標動,即目標本身具有運動屬性。所以這裡,有必要對多視角多角度做進一步的區分,多視角指的是相機的多視角(對呀,相機才具有真實的物理視角、位姿、拍攝角度 巴拉巴拉...),多角度指的是目標物體的不同角度(對呀,一個物體可以從不同的角度被觀察)。

其次,外參,外參是很重要的一個概念(廢話,還用你說!),可真的引起其他使用者足夠的重視嗎?未必! 我們一般常說的外參,其實是帶有主語的,只是我們太習以為常從而把主語省略了。外參---一般指相機的外參,即描述相機的變化。在多視角三維點雲配準中,每個點雲都有一個變化矩陣,該變化矩陣可以稱其為點雲的外參,即描述點雲的變化。至此,至少出現了兩個外參,且這兩個外參所代表的物理意義完全不一樣,但是又有千絲萬縷的聯絡。我們都知道,巨集觀下運動是相互的,則必然點雲的外參和對應相機的外參互逆。

注: 之所以這裡對上述概念做嚴格區分,主要還是因為自己之前對上述所提到的概念沒有真正深入理解,尤其是對外參的理解,導致後期演算法計算出現錯誤;其二還是後續庫、框架等對上述有嚴格區分。

OpenMVS與紋理對映

終於到了OpenMVS。。。。

已知OpenMVS可以用來稠密重建(點雲)、Mesh重建(點雲-->網格)、Mesh優化(非流行處理、補洞等)、網格紋理對映,正好對應原始碼自帶的幾個APP。

此處,目的只是單純的需要OpenMVS的網格紋理對映功能!瀏覽國內外有關OpenMVS的使用方法,尤其國內,基本都是”一條龍“服務,總結使用流程就是 colmap/OpenMVG/visualSFM...+ OpenMVS,基本都是使用可執行檔案的傻瓜式方式(網上一查一大堆),而且基本都是翻來覆去的相互”引用“(抄襲),顯然不符合自己要求與目的。吐個槽。。。

為了使用Openmvs的紋理對映模組,輸入資料必須是 .mvs格式檔案,額。。。.mvs是個什麼鬼,我沒有啊,怎麼辦?!

好吧,進入OpenMVS原始碼吧,用自己的資料去填充OpenMVS所需的資料介面。(Window10 + VS2017+OpenMVS編譯省略,主要是自己當時編譯的時候沒做記錄,不過CMake工程一般不太複雜)。

場景Scene

檢視OpenMVS自帶的幾個例子,發現其必須要填充Scene類,針對自己所面對的問題,Scene類的主要結構如下:

class MVS_API Scene
{
public:
    PlatformArr platforms; //相機引數和位姿 // camera platforms, each containing the mounted cameras and all known poses
    ImageArr images; //紋理圖,和相機對應 // images, each referencing a platform's camera pose
    PointCloud pointcloud; //點雲 // point-cloud (sparse or dense), each containing the point position and the views seeing it
    Mesh mesh; //網格 // mesh, represented as vertices and triangles, constructed from the input point-cloud

unsigned nCalibratedImages; // number of valid images

unsigned nMaxThreads; // maximum number of threads used to distribute the work load
    
... //省略程式碼
    
bool TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, float fOutlierThreshold=0.f, float fRatioDataSmoothness=0.3f, bool bGlobalSeamLeveling=true, bool bLocalSeamLeveling=true, unsigned nTextureSizeMultiple=0, unsigned nRectPackingHeuristic=3, Pixel8U colEmpty=Pixel8U(255,127,39));
    
... //省略程式碼
    
}

自己所需要的主要函式為bool TextureMesh(),可見其自帶了很多引數,引數意義後期使用時再討論。

Platforms

首先來看platforms,這個東西是Platform的陣列。在OpenMVS中,Platform定義如下:

class MVS_API Platform
{
...

public:
    String name; // platform's name
    CameraArr cameras; // cameras mounted on the platform
    PoseArr poses; 

... 

}

對我們而言,必須需要填充CameraArrPoseArr兩個陣列。

CameraArrCameraIntern型別的陣列。CameraIntern是最基本的相機父類,一提到相機,則必須包含兩個矩陣:內參和外參,CameraIntern也不例外,它需要使用如下三個引數填充,其中K為歸一化的3X3相機內參,可用Eigen中的矩陣填充,所謂歸一化,其實就是該內參矩陣的每個元素除以紋理圖片的最大寬度或高度;R顧名思義,表示相機的旋轉C表示相機的平移,R和C共同構成了相機的外參

    KMatrix K; //相機內參(歸一化後的) the intrinsic camera parameters (3x3)
    RMatrix R; //外參:相機旋轉 rotation (3x3) and
    CMatrix C;

PoseArrPose型別的陣列。Pose是類Platform中定義的一個結構體:

struct Pose {
        RMatrix R; // platform's rotation matrix
        CMatrix C; // platform's translation vector in the global coordinate system
        #ifdef _USE_BOOST
        template <class Archive>
        void serialize(Archive& ar, const unsigned int /*version*/) {
            ar & R;
            ar & C;
        }
        #endif
    };
    typedef CLISTDEF0IDX(Pose,uint32_t) PoseArr;

從上述Pose中可以看出,其也包含了兩個矩陣,但是這裡的Pose中的矩陣所表示的物理意義和CameraIntern中外參的物理意義完全不同,簡單來說,<font color=red>CameraIntern中表示的是相機本身固有或自帶的屬性,而Pose則表示整個Platform平臺(包含相機的)在世界座標系中位姿矩陣,針對每一張紋理圖(下面介紹),這兩個屬性引數共同構成了對應相機的真實外參</font>(後期通過原始碼解釋)【問題1】

Images

imagesImageArr型別的陣列,如同原始碼中解釋,每個Image對應於每個相機位姿。Image的結構如下:

class MVS_API Image
{
public:
    uint32_t platformID;//plateform相對應的ID // ID of the associated platform
    uint32_t cameraID; // camer對應的ID //ID of the associated camera on the associated platform
    uint32_t poseID; // 位姿ID  //ID of the pose of the associated platform
    uint32_t ID; // global ID of the image
    String name; // 該imgage的路徑 // image file name (relative path)
    Camera camera; // view's pose
    uint32_t width, height; // image size
    Image8U3 image; //load的時候已經處理. image color pixels
    ViewScoreArr neighbors; // scored neighbor images
    float scale; // image scale relative to the original size
    float avgDepth;

    ....

}

如上述自己新增的中文註釋,其中<font color =red>platformID、cameraID和poseID三個引數一定要與Platform中的成員屬性嚴格對應</font>,對自身而言這是最重要的三個引數,Platform中的成員變數本身並不攜帶ID屬性,只是根據新增順序預設排序,ID索引從0開始遞增。

Image結構中,還有不得不提的camera成員,如原始碼中所註釋,它表示視角(Camera)的位姿,也就是哪個相機對應於該圖片。乍一看該camera成員也必須要進行填充,可事實並非如此,原因後期解釋【問題2】

該結構中其他成員,如影像寬度高度、影像的name等,在呼叫Image::LoadImage(img_path);函式的的時候會自動填充;neighbors成員表示影像於3D點的鄰接關係,在紋理貼圖中非必要選項,而且不影響紋理貼圖效果。

PointCloud

該成員表示點雲,一般作為OpenMVS的稀疏重建或稠密重建的結果,對於網格不具備約束性,直接捨棄不填充。

Mesh

OpenMVS中的三角網格儲存結構,我只需要知道其有Load函式即可。