1. 程式人生 > >三維計算機視覺(三)--點雲分割

三維計算機視覺(三)--點雲分割

轉自:http://www.cnblogs.com/ironstark/p/5000147.html

點雲分割

  點雲分割可謂點雲處理的精髓,也是三維影象相對二維影象最大優勢的體現。

  點雲分割的目的提取點雲中的不同物體,從而實現分而治之,突出重點,單獨處理的目的。而在現實點雲資料中,往往對場景中的物體有一定先驗知識。比如:桌面牆面多半是大平面,桌上的罐子應該是圓柱體,長方體的盒子可能是牛奶盒......對於複雜場景中的物體,其幾何外形可以歸結於簡單的幾何形狀。這為分割帶來了巨大的便利,因為簡單幾何形狀是可以用方程來描述的,或者說,可以用有限的引數來描述複雜的物體。而方程則代表的物體的拓撲抽象。於是,RanSaC演算法可以很好的將此類物體分割出來。

RanSaC演算法

  RanSaC演算法(隨機取樣一致)原本是用於資料處理的一種經典演算法,其作用是在大量噪聲情況下,提取物體中特定的成分。下圖是對RanSaC演算法效果的說明。圖中有一些點顯然是滿足某條直線的,另外有一團點是純噪聲。目的是在大量噪聲的情況下找到直線方程,此時噪聲資料量是直線的3倍。

  如果用最小二乘法是無法得到這樣的效果的,直線大約會在圖中直線偏上一點。關於隨機取樣一致性演算法的原理,在wiki百科上講的很清楚,甚至給出了虛擬碼和matlab,C程式碼。見網址https://en.wikipedia.org/wiki/RANSAC. 我想換一個不那麼嚴肅或者說不那麼學術的方式來解釋這個演算法。

  實際上這個演算法就是從一堆資料裡挑出自己最心儀的資料。所謂心儀當然是有個標準(目標的形式:滿足直線方程?滿足圓方程?以及能容忍的誤差e)。平面中確定一條直線需要2點,確定一個圓則需要3點。隨機取樣演算法,其實就和小女生找男朋友差不多。

  1. 從人群中隨便找個男生,看看他條件怎麼樣,然後和他談戀愛,(平面中隨機找兩個點,擬合一條直線,並計算在容忍誤差e中有多少點滿足這條直線)
  2. 第二天,再重新找個男生,看看他條件怎麼樣,和男朋友比比,如果更好就換新的(重新隨機選兩點,擬合直線,看看這條直線是不是能容忍更多的點,如果是則記此直線為結果)
  3. 第三天,重複第二天的行為(迴圈迭代)
  4. 終於到了某個年齡,和現在的男朋友結婚(迭代結束,記錄當前結果)

  顯然,如果一個女生按照上面的方法找男朋友,最後一定會嫁一個好的(我們會得到心儀的分割結果)。只要這個模型在直觀上存在,該演算法就一定有機會把它找到。優點是噪聲可以分佈的任意廣,噪聲可以遠大於模型資訊。

  這個演算法有兩個缺點,第一,必須先指定一個合適的容忍誤差e。第二,必須指定迭代次數作為收斂條件。

  綜合以上特性,本演算法非常適合從雜亂點雲中檢測某些具有特殊外形的物體。(作者這個例子,個人感覺有不太合適,有興趣的可以查查具體的演算法原理

PCL中基於RanSaC的點雲分割方法

  PCL支援了大量幾何模型的RanSaC檢測,可以非常方便的對點雲進行分割。其呼叫方法如下:

複製程式碼
  //建立一個模型引數物件,用於記錄結果
  pcl::ModelCoefficients::Ptr coefficients (new pcl::ModelCoefficients);
  //inliers表示誤差能容忍的點 記錄的是點雲的序號
  pcl::PointIndices::Ptr inliers (new pcl::PointIndices);
  // 建立一個分割器
  pcl::SACSegmentation<pcl::PointXYZ> seg;
  // Optional
  seg.setOptimizeCoefficients (true);
  // Mandatory-設定目標幾何形狀
  seg.setModelType (pcl::SACMODEL_PLANE);
  //分割方法:隨機取樣法
  seg.setMethodType (pcl::SAC_RANSAC);
  //設定誤差容忍範圍
  seg.setDistanceThreshold (0.01);
  //輸入點雲
  seg.setInputCloud (cloud);
  //分割點雲
  seg.segment (*inliers, *coefficients);
複製程式碼

  除了平面以外,PCL幾乎支援所有的幾何形狀。作為點雲分割的基礎演算法,RanSaC很強大且必收斂,可以作為機器人抓取,識別等後續任務的前處理。

分割給人最直觀的影響大概就是鄰居和我不一樣。比如某條界線這邊是中華文明,界線那邊是西方文,最簡單的分割方式就是在邊界上找些居民問:"小夥子,你到底能不能上油管啊?”。然後把能上油管的居民座標連成一條線,自然就區分開了兩個地區。也就是說,除了之前提到的基於取樣一致的分割方式以外,應該還存在基於鄰近搜尋的分割方式。通過對比某點和其最近一點的某些特徵,來實現點雲的分割。影象所能提供的分割資訊僅是灰度或RGB向量,而三維點雲卻能夠提供更多的資訊。故點雲在分割上的優勢是影象所無法比擬的(重要的事情要說三遍)。

1.誰是我鄰居--kdTree&OcTree

  由於分割工作需要對點雲的鄰近點進行操作,不斷對比和訪問某個點的鄰居,所以決定點雲的相鄰關係是非常重要的。對於Scan來說,鄰居關係是天然的。但對於很多雜亂點雲,或者濾波,分割後的點雲來說,鄰居關係就已經被破壞了。確定一個點雲之間的相鄰關係可以通過“樹”來完成,目前比較主流的方法包括:kdTree和OcTree,這兩種方法各有特點。

1.1.kdTree---一種遞迴的鄰近搜尋策略

  關於kdTree到底是怎麼工作的https://en.wikipedia.org/wiki/K-d_tree這裡有非常詳細的說明,我不再贅述。但是kdTree實際上包括兩個部分:1.建立kdTree,2.在kdTree中查詢。建立kdTree實際上是一個不斷劃分的過程,首先選擇最sparse的維度,然後找到該維度上的中間點,垂直該維度做第一次劃分。此時k維超平面被一分為二,在兩個子平面中再找最sparse的維度,依次類推知道最後一個點也被劃分。那麼就形了一個不斷二分的樹。如圖所示。

  

  顯然,一般情況下一個點的鄰近點只需要在其父節點和子節點中搜索即可,大大縮小了鄰近點的搜尋規模。並且kdtree可以有效的對插入點進行判斷其最近點在哪個位置。對於低層次視覺來說kdTree演算法是非常重要的。在很多情況下需要給出某個點,再查k臨近點的編號,或者差某半徑範圍內的點。PCL已經實現了kdtree演算法,其呼叫介面如下:

複製程式碼
  #include <pcl/point_cloud.h>
  #include <pcl/kdtree/kdtree_flann.h>



   //建立kdtree 結構
  pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
  //傳入點雲
  kdtree.setInputCloud (cloud);
  //設定輸入點
  pcl::PointXYZ searchPoint;
   //k鄰近搜尋
   int K = 10;
   //設定兩個容器,第一個放點的標號,第二個點到SearchPoint的距離
   std::vector<int> pointIdxNKNSearch(K);
   std::vector<float> pointNKNSquaredDistance(K);
   //進行搜尋,注意,此函式有返回值>0為找到,<0則沒找到
   kdtree.nearestKSearch (searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance)
    


   //    基於距離的搜尋    //
  //兩個未知大小的容器,作用同上
  std::vector<int> pointIdxRadiusSearch;
  std::vector<float> pointRadiusSquaredDistance;
  // 搜尋半徑
  float radius = 3;
  //搜尋,效果同上
  kdtree.radiusSearch (searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance)
複製程式碼

   顯然,我們還需要一個演算法把Idx裡的點雲資料提取出來進行重新著色之類的工作,程式碼可以寫作:

複製程式碼
    pcl::PointCloud<pcl::PointXYZ>::Ptr Npoints(new pcl::PointCloud<pcl::PointXYZ>);
    Npoints->height=1;
    Npoints->width=searchindice.size();
    Npoints->resize (searchindice.size());
    //注意此清空操作非常極其以及特別重要,否則Npoints中會有莫名奇妙的點。
    Npoints->clear();
    int PointNUM = 0;
    for(int i=0;i<searchindice.size();++i)
    {   
        PointNUM = searchindice[i];
        Npoints->push_back(cloud->points[PointNUM]);
    //    cout<<distance[i]<<",  "<<cloud->points[PointNUM].x<<"  "<<cloud->points[PointNUM].y<<"  "<<cloud->points[PointNUM].z<<"  "<<endl;
    }

    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> Npoints_color_handler (Npoints, 0, 255, 0);
    viewer->addPointCloud(Npoints,Npoints_color_handler,"Npoints");
    viewer->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5, "Npoints");
複製程式碼

1.2 OcTree

  OcTree是一種更容易理解也更自然的思想。對於一個空間,如果某個角落裡有個盒子我們卻不知道在哪兒。但是"神"可以告訴我們這個盒子在或者不在某範圍內,顯而易見的方法就是把空間化成8個卦限,然後詢問在哪個卦限內。再將存在的卦限繼續化成8個。意思大概就是太極生兩儀,兩儀生四象,四象生八卦,就這麼一直劃分下去,最後一定會確定一個非常小的空間。對於點雲而言,只要將點雲的立方體凸包用octree生成很多很多小的卦限,那麼在相鄰卦限裡的點則為相鄰點。

  顯然,對於不同點雲應該採取不同的搜尋策略,如果點雲是疏散的,分佈很廣泛,且每什麼規律(如lidar測得的點雲或雙目視覺捕捉的點雲)kdTree能更好的劃分,而octree則很難決定最小立方體應該是多少。太大則一個立方體裡可能有很多點雲,太小則可能立方體之間連不起來。如果點雲分佈非常規整,是某個特定物體的點雲模型,則應該使用ocTree,因為很容易求解凸包並且點與點之間相對距離無需再次比對父節點和子節點,更加明晰。典型的例子是斯坦福的兔子。

2.鄰居,咱倆關係近麼---歐幾里得與區域生長演算法

  基於歐式距離的分割和基於區域生長的分割本質上都是用區分鄰里關係遠近來完成的。由於點雲資料提供了更高維度的資料,故有很多資訊可以提取獲得。歐幾里得演算法使用鄰居之間距離作為判定標準,而區域生長演算法則利用了法線,曲率,顏色等資訊來判斷點雲是否應該聚成一類。

2.1.歐幾里得演算法

  演算法的原理在PCL相關的教程中已經說的比較清楚了,我不再給出虛擬碼。我想用一個故事來講講這個問題。從前有一個腦筋急轉彎,說一個鍋裡有兩粒豆子,如果不用手,要怎麼把它們分開。當時的答案是豆子本來就是分開的,又沒黏在一起,怎麼不叫分開。OK,實際上歐幾里德演算法就是這個意思。兩團點雲就像是兩粒豆子,只要找到某個合適的度量方式,就有辦法把點雲和點雲分開。區分豆子我們用的方法可以歸結於,兩個豆子之間的距離小於分子距離,所以它們並沒有連在一起。如果兩團點雲之間最近兩點的距離小於單個點雲內部點之間的距離,則可以由演算法判斷其分為兩類。假設總點雲集合為A,聚類所得點雲團為Q

  具體的實現方法大致是:

  1. 找到空間中某點p10,有kdTree找到離他最近的n個點,判斷這n個點到p的距離。將距離小於閾值r的點p12,p13,p14....放在類Q裡
  2. 在 Q\p10 裡找到一點p12,重複1
  3. 在 Q\p10,p12 找到找到一點,重複1,找到p22,p23,p24....全部放進Q裡
  4. 當 Q 再也不能有新點加入了,則完成搜尋了

  聽起來好像這個演算法並沒什麼用,因為點雲總是連成片的,很少有什麼東西會浮在空中讓你來分。但是如果和前面介紹的內容聯絡起來就會發現這個演算法威力巨大了。比如

  1. 半徑濾波刪除離群點
  2. 取樣一致找到桌面
  3. 抽掉桌面。。。。。

  顯然,一旦桌面被抽,桌上的物體就自然成了一個個的浮空點雲團。就能夠直接用歐幾里德演算法進行分割了。如圖所示。

  PCL對歐幾里德演算法進行了很好的封裝,其程式碼如下:

複製程式碼
  //被分割出來的點雲團(標號佇列)
  std::vector<pcl::PointIndices> cluster_indices;
  //歐式分割器
  pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
  ec.setClusterTolerance (0.02); // 2cm
  ec.setMinClusterSize (100);
  ec.setMaxClusterSize (25000);
  //搜尋策略樹
  ec.setSearchMethod (tree);
  ec.setInputCloud (cloud_filtered);
  ec.extract (cluster_indices);
複製程式碼

2.2 區域生長演算法

  區域生長演算法直觀感覺上和歐幾里德演算法相差不大,都是從一個點出發,最終佔領整個被分割區域。毛主席說:“星星之火,可以燎原” 就是這個意思。歐幾里德演算法是通過距離遠近,來判斷燒到哪兒。區域生長演算法則不然,燒到哪兒靠燃料(點)的性質是否類似來決定。對於普通點雲,其可由法線、曲率估計演算法獲得其法線和曲率值。通過法線和曲率來判斷某點是否屬於該類。其演算法可以總結為:

  1. 種子周圍的點和種子相比
  2. 法線方向是否足夠相近
  3. 曲率是否足夠小
  4. 如果滿足1,2則該點可用做種子
  5. 如果只滿足1,則歸類而不做種
  6. 從某個種子出發,其“子種子”不再出現則一類聚集完成
  7. 類的規模既不能太大也不能太小

  顯然,上述演算法是針對小曲率變化面設計的。尤其適合對連續階梯平面進行分割:比如SLAM演算法所獲得的建築走廊。

  

  PCL對區域生長演算法有如下封裝:

複製程式碼
  //一個點雲團佇列,用於存放聚類結果
  std::vector <pcl::PointIndices> clusters;
  //區域生長分割器
  pcl::RegionGrowing<pcl::PointXYZ, pcl::Normal> reg;
  
  //輸入分割目標
  reg.setSearchMethod (tree);
  reg.setNumberOfNeighbours (30);
  reg.setInputCloud (cloud);
  //reg.setIndices (indices);
  reg.setInputNormals (normals);
  
  //設定限制條件及先驗知識
  reg.setMinClusterSize (50);
  reg.setMaxClusterSize (1000000);
  reg.setSmoothnessThreshold (3.0 / 180.0 * M_PI);
  reg.setCurvatureThreshold (1.0);

  reg.extract (clusters);
複製程式碼

   除了普通點雲之外,還有一種特殊的點雲,成為RGB點雲。顯而易見,這種點雲除了結構資訊之外,還存在顏色資訊。將物體通過顏色分類,是人類在辨認果實的過程中進化出的能力,顏色資訊可以很好的將複雜場景中的特殊物體分割出來。而顏色點雲也並不那麼遙不可及,Xbox Kinect就可以輕鬆的捕捉顏色點雲。基於顏色的區域生長分割原理上和基於曲率,法線的分割方法是一致的。只不過比較目標換成了顏色,去掉了點雲規模上限的限制。可以認為,同一個顏色且捱得近,是一類的可能性很大,不需要上限來限制。所以這種方式比較適合用於室內場景分割。尤其是複雜室內場景,顏色分割可以輕鬆的將連續的場景點雲變成不同的物體。哪怕是高低不平的地面,沒法用取樣一致分割器抽掉,顏色分割演算法同樣能完成分割任務。

  基於PCL的實現方式如下:

複製程式碼
  //用於存放點雲團的容器
  std::vector <pcl::PointIndices> clusters;
  //顏色分割器
  pcl::RegionGrowingRGB<pcl::PointXYZRGB> reg;
  reg.setInputCloud (cloud);
  //點雲經過了濾波器的預處理,提取了indices
  reg.setIndices (indices);
  reg.setSearchMethod (tree);
  reg.setDistanceThreshold (10);
  //點與點之間顏色容差
  reg.setPointColorThreshold (6);
  //蘋果都是紅的,哪怕離散的蘋果也應該是一類
  reg.setRegionColorThreshold (5);
  reg.setMinClusterSize (600);

  reg.extract (clusters);
複製程式碼 最小割演算法

1.點雲分割的精度

  在之前的兩個章節裡介紹了基於取樣一致的點雲分割和基於臨近搜尋的點雲分割演算法。基於取樣一致的點雲分割演算法顯然是意識流的,它只能割出大概的點雲(可能是杯子的一部分,但杯把兒肯定沒分割出來)。基於歐式演算法的點雲分割面對有牽連的點雲就無力了(比如風箏和人,在不用三維形態學去掉中間的線之前,是無法分割風箏和人的)。基於法線等資訊的區域生長演算法則對平面更有效,沒法靠它來分割桌上的碗和杯子。也就是說,上述演算法更關注能不能分割,除此之外,我們還需要一個方法來解決分割的“好不好”這個問題。也就是說,有沒有哪種方法,可以在一個點不多,一個點不少的情況下,把目標和“其他”分開。

  答案是有,也就是這篇博文要解決的最小割演算法。

2.最小割演算法

  最小割(min-cut)並不是一個什麼很新鮮的東西。它早就用在網路規劃,求解橋問題,影象分割等領域,被移植到點雲分割上也不足為奇。最小割演算法是圖論中的一個概念,其作用是以某種方式,將兩個點分開,當然這兩個點中間可能是通過無數的點再相連的。如圖所示。

  如果要分開最左邊的點和最右邊的點,紅綠兩種割法都是可行的,但是紅線跨過了三條線,綠線只跨過了兩條。單從跨線數量上來論可以得出綠線這種切割方法更優的結論。但假設線上有不同的權值,那麼最優切割則和權值有關了。它到底是怎麼找到那條綠線的暫且不論。總而言之,就是有那麼一個演算法,當你給出了點之間的 “圖” (廣義的),以及連線的權值時,最小割演算法就能按照你的要求把圖分開。

3.點雲 “圖”

  顯而易見,切割有兩個非常重要的因素,第一個是獲得點與點之間的拓撲關係,也就是生成一張“圖”。第二個是給圖中的連線賦予合適的權值。只要這兩個要素合適,最小割演算法就會辦好剩下的事情。點雲是一種非常適合分割的物件(我第三次強調這個事情了),點雲有天然分開的點。有了點之後,只要把點雲中所有的點連起來就可以了。連線演算法如下:

  1. 找到每個點最近的n個點
  2. 將這n個點和父點連線
  3. 找到距離最小的兩個塊(A塊中某點與B塊中某點距離最小),並連線
  4. 重複3,直至只剩一個塊

  現在已經有了“圖”,只要給圖附上合適的權值,就完成了所有任務。物體分割給人一個直觀印象就是屬於該物體的點,應該相互之間不會太遠。也就是說,可以用點與點之間的歐式距離來構造權值。所有線的權值可對映為線長的函式。

  貌似我們現在已經搞定一切了,其實不然。分割總是有一個目標的,而這種精準打擊的演算法,顯然你要告訴我打擊物件是誰,打擊範圍多大——目標需要人為指定(center),尺寸需要提前給出(radius)。

  OK,我們現在有了打擊物件了(指定了目標物體上的一個點),接下來要做的,就是讓除此物件之外的物體被保護起來,不受到打擊。保護的方法就是認為加重目標範圍之外的權值(罰函式)

 

  上述過程其實看起來還不夠智慧,如果有辦法讓我只需要點一下滑鼠,選中要分割的物體,接下來電腦替我操心其他事情,那就太好了。這其實是可以實現的,稱為AutoMatic Regime.但PCL並沒有封裝這個演算法,忽略不表。

4.PCL對最小割演算法的實現

複製程式碼
  //生成分割器
  pcl::MinCutSegmentation<pcl::PointXYZ> seg;
  //分割輸入分割目標
  seg.setInputCloud (cloud);
  //指定打擊目標(目標點)
  pcl::PointCloud<pcl::PointXYZ>::Ptr foreground_points(new    pcl::PointCloud<pcl::PointXYZ> ());
  pcl::PointXYZ point;
  point.x = 68.97;
  point.y = -18.55;
  point.z = 0.57;
  foreground_points->points.push_back(point);
  seg.setForegroundPoints (foreground_points);
  //指定權函式sigma
  seg.setSigma (0.25);
  //物體大概範圍
  seg.setRadius (3.0433856);
  //用多少生成圖
  seg.setNumberOfNeighbours (14);
  //和目標點相連點的權值(至少有14個)
  seg.setSourceWeight (0.8);
  //分割結果
  std::vector <pcl::PointIndices> clusters;
  seg.extract (clusters);
複製程式碼

  顯然,最小割演算法更注重分割的精確性而不是分割自動進行。最小割演算法用於半自動分割識別有著巨大的優勢,適合用於計算機視覺,城市場景點雲分析一類。但對機器人來說,或許和特徵點檢測演算法聯合起來能獲得較好的效果。

  

  圖中顯示,最小割演算法成功找到了靠的很近的汽車。顯然歐式演算法r取太大則無法區分左右汽車,r取太小則無法區分車頭和車身(玻璃不反光,是沒有點雲的)。

1.超體聚類——一種來自影象的分割方法

  超體(supervoxel)是一種集合,集合的元素是“體”。與體素濾波器中的體類似,其本質是一個個的小方塊。與之前提到的所有分割手段不同,超體聚類的目的並不是分割出某種特定物體,其對點雲實施過分割(over segmentation),將場景點雲化成很多小塊,並研究每個小塊之間的關係。這種將更小單元合併的分割思路已經出現了有些年份了,在影象分割中,畫素聚類形成超畫素,以超畫素關係來理解影象已經廣為研究。本質上這種方法是對區域性的一種總結,紋理,材質,顏色類似的部分會被自動的分割成一塊,有利於後續識別工作。比如對人的識別,如果能將頭髮,面部,四肢,軀幹分開,則能更好的對各種姿態,性別的人進行識別。

  點雲和影象不一樣,其不存在畫素鄰接關係。所以,超體聚類之前,必須以八叉樹對點雲進行劃分,獲得不同點團之間的鄰接關係。與影象相似點雲的鄰接關係也有很多,如面鄰接,線鄰接,點鄰接。其具體解釋如下圖:

  基於超體聚類的點雲分割,使用點鄰接(藍色)作為相鄰判據。

2.超體聚類的實現步驟

 舉個簡單的例子來體會下超體聚類,其過程和結晶類似。但不是水結晶成冰,而是鹽溶液過飽和狀態下的多晶核結晶。所有的晶核(seed)同時開始生長,最終填滿整個空間,使物質具有晶體結構。 超體聚類實際上是一種特殊的區域生長演算法,和無限制的生長不同,超體聚類首先需要規律的佈置區域生長“晶核”。晶核在空間中實際上是均勻分佈的,並指定晶核距離(Rseed)。再指定粒子距離(Rvoxel)。再指定最小晶粒(MOV),過小的晶粒需要融入最近的大晶粒。關係如圖所示:

  有了晶粒和結晶範圍之後,我們只需要控制結晶過程,就能將整個空間劃分開了。結晶過程的本質就是不斷吸納類似的粒子(八分空間)。類似是一個比較模糊的概念,關於類似的定義有以下公式:

 

  公式中的Dc,表示顏色上的差異,Dn表示法線上的差異,Ds代表點距離上的差異。w_*表示一系列權重。用於控制結晶形狀。在晶核周圍尋找一圈,D最小的體素被認為是下一個“被髮展的黨員”。需要注意的是,結晶過程並不是長完一個晶核再長下一個,二是所有的晶核同時開始生長(雖然計算機計算時必然有先後,但從層次上來說是同時的)。其生長順序如下圖所示:

  接下來所有晶核繼續公平競爭,發展第二個“黨員”,以此迴圈,最終所有晶體應該幾乎同時完成生長。整個點雲也被晶格所分割開來。並且保證了一個晶包裡的粒子都是類似的。

3.PCL對超體聚類的實現

複製程式碼
  //設定結晶引數
  float voxel_resolution = 0.008f;
  float seed_resolution = 0.1f;
  float color_importance = 0.2f;
  float spatial_importance = 0.4f;
  float normal_importance = 1.0f;
  
  //生成結晶器
  pcl::SupervoxelClustering<PointT> super (voxel_resolution, seed_resolution);
  //和點雲形式有關
  if (disable_transform)
    super.setUseSingleCameraTransform (false);
  //輸入點雲及結晶引數
  super.setInputCloud (cloud);
  super.setColorImportance (color_importance);
  super.setSpatialImportance (spatial_importance);
  super.setNormalImportance (normal_importance);
  //輸出結晶分割結果:結果是一個對映表
 std::map <uint32_t, pcl::Supervoxel<PointT>::Ptr > supervoxel_clusters;
  super.extract (supervoxel_clusters);
  //獲得晶體中心
  PointCloudT::Ptr voxel_centroid_cloud = super.getVoxelCentroidCloud ();
  //獲得晶體
  PointLCloudT::Ptr labeled_voxel_cloud = super.getLabeledVoxelCloud ();
複製程式碼

   執行上訴過程後,會將晶體對映成一系列數。數代表的是指向各個晶體的指標。可以通過getter函式,把晶體有關的資訊拖出來。拖出來的是點雲。

複製程式碼
  //將相連的晶體中心連起來並顯示 
  std::multimap<uint32_t, uint32_t> supervoxel_adjacency;
  super.getSupervoxelAdjacency (supervoxel_adjacency);
  std::multimap<uint32_t,uint32_t>::iterator label_itr = supervoxel_adjacency.begin ();
for ( ; label_itr != supervoxel_adjacency.end (); )
  {
    //First get the label
    uint32_t supervoxel_label = label_itr->first;
    //Now get the supervoxel corresponding to the label
    pcl::Supervoxel<PointT>::Ptr supervoxel = supervoxel_clusters.at (supervoxel_label);

    //Now we need to iterate through the adjacent supervoxels and make a point cloud of them
    PointCloudT adjacent_supervoxel_centers;
    std::multimap<uint32_t,uint32_t>::iterator adjacent_itr = supervoxel_adjacency.equal_range (supervoxel_label).first;
    for ( ; adjacent_itr!=supervoxel_adjacency.equal_range (supervoxel_label).second; ++adjacent_itr)
    {
      pcl::Supervoxel<PointT>::Ptr neighbor_supervoxel = supervoxel_clusters.at (adjacent_itr->second);
      adjacent_supervoxel_centers.push_back (neighbor_supervoxel->centroid_);
    }
    //Now we make a name for this polygon
    std::stringstream ss;
    ss << "supervoxel_" << supervoxel_label;
    //This function is shown below, but is beyond the scope of this tutorial - basically it just generates a "star" polygon mesh from the points given
    addSupervoxelConnectionsToViewer (supervoxel->centroid_, adjacent_supervoxel_centers, ss.str (), viewer);
    //Move iterator forward to next label
    label_itr = supervoxel_adjacency.upper_bound (supervoxel_label);
  }
複製程式碼

       至此,生成了不同的晶體之間的鄰接關係。結果如下所示(不同晶核距離0.1m,0.05m)

  此方法主要為識別做前期準備,但我認為,這種東西用在三維視覺+有限元倒是極好的。可以在不使用應變片的前提下對物體各個部分應變進行直接測量。在已知力的情況下可以建立物體剛度和應變的關係,貌似鋼包迴轉臺的手裡分析可以這樣解決。蛋疼的是實際工業機械哪有那麼多花花綠綠的給你分割,很難形成有效的對應點匹配。

1.航空測量與點雲的形態學

  航空測量是對地形地貌進行測量的一種高效手段。生成地形三維形貌一直是地球學,測量學的研究重點。但對於城市,森林,等獨特地形來說,航空測量會受到影響。因為土地表面的樹,地面上的房子都認為的改變了地貌,可以認為是地貌上的噪聲點。設計一種有效的手段去除地面噪聲對地形測量的影響顯得非常重要。這種工作可以認為是一種特殊的點雲分割,一般情況下點雲分割的目標是去除地面,而這種方法需要在不使用地面平整假設的前提下獲得地面。

  形態學是影象處理中非常重要的概念,對二值影象而言,可由簡單的膨脹運算和腐蝕運算組成一個完整的影象處理族。但是想要將這個演算法移植到三維點雲上是比較難的,首先一般的點雲沒有明顯的對映值,也沒有清晰的定義域,很難設計形態學處理的基理。但是LIDAR點雲例外。由於LIDAR點雲由飛機獲得,飛機距離地面相對較遠,且測量方向和地面垂直。這就形成了比較完整的xy->z對映(z方向的範圍遠遠小於xy方向),z方向代表地面物體的高度,x,y方向為平行與地面且相互垂直的兩個軸。有了明確的定義域以及單值對映關係就有了設計形態學演算法的基本要素。實際上除了形態學演算法之外,許多影象處理演算法都可以用來分割LIDAR點雲了,本質上這就是一幅大影象。

2.三維形態學運算元

  對於影象而言,形態學運算一般是針對二值影象而言的。當然也有針對灰度的形態學運算,其原理應該和針對點雲的形態學運算類似(我猜的)。形態學運算元的設計實際上非常簡單,只要能設計出基礎的膨脹和腐蝕運算元就可以組合得到一系列的處理。

  其中,d表示膨脹運算元,e表示腐蝕運算元。運算元的原理有些像中值濾波,通過選取一個窗w中最高點或最低點來完成影象的膨脹和腐蝕,其效果如下圖所示:

  

  在航拍圖的橫截面上可以很清楚的看出膨脹與腐蝕的效果。對於房子和樹可以用不同尺度窗(從小到大)先腐蝕至地面。但是這會導致一個巨大的問題。。。如果地面上有個土包(比如秦始皇陵),那麼這個土包也會在一次次的腐蝕中被消耗。那豈不是秦始皇陵就發現不了?所以還有一個補償演算法用於解決這個問題,稱為線性補償演算法。

  建築物和土包有一個巨大的區別,建築物往往相對比較陡峭,而土包卻是變化比較平緩的。這個可以作為一個判據,用於判斷物體是否需要被腐蝕,也作為窗收斂的判據。

  式中k稱為斜率,代表下一個窗的大小是上一個窗的2^k倍

  s是一個因子

  dh是切深判據,每一次腐蝕大於切深判據才認為是有效的,小於切深判據則是土包。

  上述公式是怎麼發現的就需要問論文作者了,所有材料都被收錄於文章:

  A Progressive Morphological Filter for Removing Nonground Measurements From Airborne LIDAR Data

3.PCL對本演算法的實現

複製程式碼
  //生成形態濾波器
  pcl::ProgressiveMorphologicalFilter<pcl::PointXYZ> pmf;
  pmf.setInputCloud (cloud);
  //設定窗的大小以及切深,斜率資訊
  pmf.setMaxWindowSize (20);
  pmf.setSlope (1.0f);
  pmf.setInitialDistance (0.5f);
  pmf.setMaxDistance (3.0f);
  //提取地面
  pmf.extract (ground->indices);

  // 從標號到點雲
  pcl::ExtractIndices<pcl::PointXYZ> extract;
  extract.setInputCloud (cloud);
  extract.setIndices (ground);
  extract.filter (*cloud_filtered);
複製程式碼 View Code

  演算法效果如圖:

  

1.影象分割的兩條思路

  場景分割是機器視覺中的重要任務,尤其對家庭機器人而言,優秀的場景分割演算法是實現複雜功能的基礎。但是大家搞了幾十年也還沒搞定——不是我說的,是接下來要介紹的這篇論文說的。影象分割的搞法大概有兩種:劍宗——自低向上:先將影象聚類成小的畫素團再慢慢合併,氣宗——自頂向下:用多尺度模板分割影象,再進一步將影象優化分割成不同物體。當然,還有將二者合而為一的方法:training with data set. 這第三種方法也不好,太依賴於已知的物體而失去了靈活性。家庭機器人面對家裡越來越多的東西需要一種非訓練且效果很好的分割法。  Object Partitioning using Local Convexity 一文的作者從古籍中(也不老,1960s左右吧),找到了一種基於凹凸性的分割方法。實際上基於凹凸的影象理解在之前是被研究過的,但是隨著神經網路的出現,漸漸這種從明確物理意義入手的影象"理解"方法就被淹沒了。對於二維影象而言,其凹凸性較難描述,但對於三維影象而言,凹凸幾乎是與生俱來的性質。

2.LCCP方法 

  LCCP是Locally Convex Connected Patches的縮寫,翻譯成中文叫做 ”區域性凸連線打包一波帶走“~~~演算法大致可以分成兩個部分:1.基於超體聚類的過分割。2.在超體聚類的基礎上再聚類。超體聚類作為一種過分割方法,在理想情況下是不會引入錯誤資訊的,也就是說適合在此基礎上再進行處理。LCCP方法並不依賴於點雲顏色,所以只使用空間資訊和法線資訊,wc=0。ws=1,wn=4。

2.1演算法理論

  點雲完成超體聚類之後,對於過分割的點雲需要計算不同的塊之間凹凸關係。凹凸關係通過 CC(Extended Convexity Criterion) SC (Sanity criterion)判據來進行判斷。其中 CC 利用相鄰兩片中心連線向量與法向量夾角來判斷兩片是凹是凸。顯然,如果圖中a1>a2則為凹,反之則為凸。

  考慮到測量噪聲等因素,需要在實際使用過程中引入門限值(a1需要比a2大出一定量)來濾出較小的凹凸誤判。此外,為去除一些小噪聲引起的誤判,還需要引入“第三方驗證”,如果某塊和相鄰兩塊都相交,則其凹凸關係必相同。CC 判據最終如CCe:

  

  如果相鄰兩面中,有一個面是單獨的,cc判據是無法將其分開的。舉個簡單的例子,兩本厚度不同的書並排放置,視覺演算法應該將兩本書分割開。如果是臺階,則視覺演算法應該將臺階作為一個整體。本質上就是因為厚度不同的書存在surface-singularities。為此需要引入SC判據,來對此進行區分。

  

  如圖所示,相鄰兩面是否真正聯通,是否存在單獨面,與θ角有關,θ角越大,則兩面真的形成凸關係的可能性就越大。據此,可以設計SC判據:

  其中S(向量)為兩平面法向量的叉積。

  最終,兩相鄰面之間凸邊判據為:

  

  在標記完各個小區域的凹凸關係後,則採用區域增長演算法將小區域聚類成較大的物體。此區域增長演算法受到小區域凹凸性限制,既:

  只允許區域跨越凸邊增長。

至此,分割完成,在濾去多餘噪聲後既獲得點雲分割結果。此外:考慮到RGB-D影象隨深度增加而離散,難以確定八叉樹尺寸,故在z方向使用對數變換以提高精度。分割結果如圖:

  

                        

  從圖中可知,糾纏在一起,顏色形狀相近的物體完全被分割開了,如果是影象分割要達到這個效果,那就。。。。。呵呵呵。。。。

2.2 PCL的實現

  官網並未給出具體實現並測試,我不對以下程式碼有效性負責。

  1.超體聚類

複製程式碼
  //設定結晶引數
  float voxel_resolution = 0.008f;
  float seed_resolution = 0.1f;
  float color_importance = 0.2f;
  float spatial_importance = 0.4f;
  float normal_importance = 1.0f;
  
  //生成結晶器
  pcl::SupervoxelClustering<PointT> super (voxel_resolution, seed_resolution);
  //和點雲形式有關
  if (disable_transform)
    super.setUseSingleCameraTransform (false);
  //輸入點雲及結晶引數
  super.setInputCloud (cloud);
  super.setColorImportance (color_importance);
  super.setSpatialImportance (spatial_importance);
  super.setNormalImportance (normal_importance);
  //