格特征提取利器vtkFeatureEdges全解析)
做網(wǎng)格數(shù)據(jù)處理的時(shí)候“特征提取”這四個(gè)字恐怕是我被問(wèn)到最多的高頻詞之一。一搜特征提取方法網(wǎng)上鋪天蓋地都是圖像領(lǐng)域的SIFT、ORB或者點(diǎn)云領(lǐng)域的法向估計(jì)、FPFH描述子可當(dāng)你手里拿的是一個(gè)由頂點(diǎn)、邊、三角面片構(gòu)成的多邊形網(wǎng)格時(shí)最趁手的工具其實(shí)是VTK自帶的vtkFeatureEdges。這個(gè)類長(zhǎng)期躺在vtkFiltersCore包里不起眼出場(chǎng)率卻極高模型分割前的棱線定位、配準(zhǔn)前的輪廓線抽取、網(wǎng)格簡(jiǎn)化時(shí)想要保留硬邊背后幾乎都有它的影子。今天這篇VTK手冊(cè)就想把這個(gè)多邊形網(wǎng)格特征提取利器從頭拆透——不只是貼一段能跑的代碼而是把每個(gè)開(kāi)關(guān)的含義、FeatureAngle的取值邏輯、提取后線條為什么老斷成碎片、怎么配合鼠標(biāo)拾取和Qt界面做交互一條一條講清楚。適合三類讀者正在用VTK做網(wǎng)格顯示與處理的開(kāi)發(fā)人員、需要從CT/MRI等值面里抽輪廓線的影像工程師、以及把網(wǎng)格簡(jiǎn)化與分割當(dāng)日常工作的圖形學(xué)同學(xué)。順便說(shuō)個(gè)常見(jiàn)誤區(qū)。很多人搜特征提取方法搜到的是圖像特征或者點(diǎn)云特征描述子然后試圖直接套到三角網(wǎng)格上。圖像是規(guī)則網(wǎng)格三角網(wǎng)格是非規(guī)則拓?fù)鋬烧邔?duì)“鄰域”的定義完全不一樣。vtkFeatureEdges恰恰是基于網(wǎng)格本身的拓?fù)溧徲颉簿褪沁叺墓蚕砻骊P(guān)系——來(lái)定義特征。理解這一點(diǎn)你才不會(huì)拿圖像特征提取的思路去調(diào)網(wǎng)格提取的參數(shù)。1. 先搞清楚特征邊在網(wǎng)格里到底是怎么定義的1.1 一張三角網(wǎng)格里邊其實(shí)只有四種狀態(tài)一個(gè)三角網(wǎng)格表面每條邊無(wú)非處于四種狀態(tài)之一。第一種是邊界邊整條邊只屬于一個(gè)面片邊緣直接暴露在外模型的開(kāi)口、裁切邊緣都會(huì)出現(xiàn)它。第二種是特征邊一條邊同時(shí)屬于兩個(gè)面片但這兩個(gè)面片在這個(gè)位置上“折”得比較厲害二面角大于給定閾值機(jī)械零件的棱線、牙齒模型的牙尖棱脊都屬于這一類。第三種是非流形邊一條邊被三個(gè)甚至更多的面片同時(shí)占有這是網(wǎng)格數(shù)據(jù)里典型的“不該出現(xiàn)但偏要出現(xiàn)”的結(jié)構(gòu)通常意味著該區(qū)域拓?fù)溆衅茡p。第四種是流形邊即普通的、只被兩個(gè)面片共享且?jiàn)A角平緩的邊模型表面絕大多數(shù)邊都是這種。vtkFeatureEdges干的事情就是把這四種邊全部掃描出來(lái)再按你的需要挑著輸出。想提取閉合輪廓用邊界邊想要模型上的折痕棱邊用特征邊想做網(wǎng)格完整性診斷打開(kāi)非流形邊。我在做網(wǎng)格工具鏈時(shí)經(jīng)常把“邊類型統(tǒng)計(jì)”當(dāng)成網(wǎng)格體檢的第一步因?yàn)椴煌愋蛿?shù)量的比例往往能直接暴露這個(gè)模型是CAD導(dǎo)出的、CT重建的還是激光掃描拼接的。1.2 在VTK管線里它處在什么位置vtkFeatureEdges繼承自vtkPolyDataAlgorithm輸入是vtkPolyData輸出還是vtkPolyData。輸入可以是STL、OBJ、VTP直接讀進(jìn)來(lái)的網(wǎng)格也可以是vtkMarchingCubes從DICOM體數(shù)據(jù)里跑出來(lái)的等值面。輸出里不再包含任何面片只有頂點(diǎn)和線單元每條線單元對(duì)應(yīng)一條原始邊。它是完全非破壞性的既不改網(wǎng)格幾何也不動(dòng)拓?fù)渲话选澳男┻吺翘卣鬟叀钡慕Y(jié)論復(fù)制到一張新的PolyData里非常適合插在管線中間做后續(xù)再處理。我一般把它放在“讀取-清理-預(yù)處理”之后、下游算法之前。為什么不跳過(guò)預(yù)處理因?yàn)橹貜?fù)點(diǎn)會(huì)直接毀掉邊共享關(guān)系的判斷一個(gè)看起來(lái)封閉的模型可能因?yàn)轫旤c(diǎn)坐標(biāo)不完全一致而處處開(kāi)縫特征邊提取結(jié)果立刻變成一堆亂線。更隱蔽的是法向不一致會(huì)讓二面角計(jì)算整體偏移該提的特征邊提不出來(lái)、不該提的冒出來(lái)。這兩個(gè)坑我在第4章會(huì)逐一展開(kāi)。2. 核心參數(shù)逐個(gè)拆解這些開(kāi)關(guān)一個(gè)都別亂開(kāi)2.1 四個(gè)開(kāi)關(guān)的默認(rèn)行為先記在心里vtkFeatureEdges提供的方法名非常直白看一眼就知道是干嘛的方法默認(rèn)狀態(tài)作用典型場(chǎng)景BoundaryEdgesOn/OffOn輸出只被一個(gè)面片使用的邊界邊網(wǎng)格開(kāi)口輪廓、區(qū)域切割線FeatureEdgesOn/OffOn輸出二面角大于FeatureAngle的折痕邊棱線、特征線提取NonManifoldEdgesOn/OffOff輸出被三個(gè)及以上面片共用的非流形邊網(wǎng)格拓?fù)湓\斷與修復(fù)ManifoldEdgesOn/OffOff輸出普通平緩邊畫完整線框、輸出網(wǎng)格全部邊SetFeatureAngle(angle)30.0特征角閾值單位度核心調(diào)參對(duì)象ColoringOn/OffOff讓不同類型邊帶不同顏色標(biāo)量特征類型診斷我第一次用它時(shí)把所有開(kāi)關(guān)一股腦打開(kāi)輸出瞬間變成幾百萬(wàn)條線段整個(gè)模型幾乎每條邊都被“提”了出來(lái)完全不是特征線的樣子。原因就是ManifoldEdgesOn把常規(guī)的普通邊全部放行了。經(jīng)驗(yàn)之談默認(rèn)設(shè)置通常已經(jīng)夠用只有想診斷網(wǎng)格流形性時(shí)才臨時(shí)打開(kāi)非流形邊只有要導(dǎo)出完整線框時(shí)才打開(kāi)流形邊。日常項(xiàng)目里“邊界邊特征邊”這對(duì)組合占到了九成場(chǎng)景。2.2 FeatureAngle才是靈魂折痕到底多“折”才算特征FeatureAngle默認(rèn)是30度。判定邏輯可以從二面角理解一條邊屬于兩個(gè)相鄰三角形兩個(gè)三角形各自有法向量把法向夾角與FeatureAngle比較大于閾值就標(biāo)記為特征邊。直觀想象就是拿一張紙對(duì)折折痕處兩半平面夾角越小折痕越鋒利。對(duì)立方體而言任意相鄰面的法向夾角是90度遠(yuǎn)大于30度所以12條棱全部會(huì)被提取對(duì)球面或平面相鄰面法向夾角趨近于0度自然提不出什么東西。選閾值沒(méi)有通吃值跟模型來(lái)源強(qiáng)相關(guān)。我平時(shí)積累的參數(shù)經(jīng)驗(yàn)工程CAD類網(wǎng)格幾何精度高特征角設(shè)15到30度都很合適地形表面或掃描重建的粗糙網(wǎng)格表面本來(lái)就有密集小起伏設(shè)5到10度能保留主要山脊線設(shè)30度以上基本什么都提不出來(lái)醫(yī)學(xué)影像重建的等值面普遍帶噪聲常用40到60度去抓真正的銳利邊界。調(diào)試時(shí)別憑感覺(jué)寫個(gè)小腳本把提取到的線條總數(shù)、平均長(zhǎng)度打出來(lái)一邊改閾值一邊對(duì)比兩三輪就能鎖定這個(gè)模型適合的區(qū)間。需要提醒一點(diǎn)二面角計(jì)算依賴兩個(gè)面的法向量方向是統(tǒng)一的。如果輸入網(wǎng)格的法向沒(méi)有整理過(guò)部分面片朝外、部分朝內(nèi)算出來(lái)的夾角會(huì)偏差巨大。遇到提取結(jié)果詭異時(shí)先跑一遍vtkPolyDataNormals把法向統(tǒng)一再回頭調(diào)FeatureAngle這個(gè)順序不能反。2.3 輸出精度與分類著色兩個(gè)容易被忽視的小開(kāi)關(guān)SetOutputPointsPrecision控制輸出點(diǎn)坐標(biāo)的數(shù)據(jù)類型默認(rèn)跟隨輸入。如果后面接vtkTubeFilter做可視化、或者要做精確坐標(biāo)計(jì)算建議顯式指定高精度。VTK里常用的取值是DEFAULT_PRECISION、SINGLE_PRECISION和DOUBLE_PRECISION我實(shí)際用double的場(chǎng)景更多因?yàn)榫W(wǎng)格處理鏈路上精度丟失往往在不易察覺(jué)處發(fā)生等到后續(xù)模塊報(bào)錯(cuò)再回溯就比較痛苦。ColoringOn則是調(diào)試神器。打開(kāi)后輸出的點(diǎn)屬性或單元屬性里會(huì)帶一個(gè)類型標(biāo)量邊界邊、特征邊、非流形邊會(huì)得到不同取值。配合vtkLookupTable和Mapper按Cell數(shù)據(jù)著色白花花一片的特征線立刻變成“紅是開(kāi)口、藍(lán)是棱線、綠是壞邊”的體檢報(bào)告。這個(gè)能力放在大型網(wǎng)格上特別值錢你不可能用肉眼去逐條檢查成千上萬(wàn)條邊。3. 實(shí)操全過(guò)程把特征線從網(wǎng)格里干凈地拿出來(lái)3.1 最小可運(yùn)行代碼Python和C雙版本先放Python版這也是我平時(shí)做原型最快的方式import vtk sphere vtk.vtkSphereSource() sphere.SetPhiResolution(40) sphere.SetThetaResolution(40) sphere.Update() feature vtk.vtkFeatureEdges() feature.SetInputData(sphere.GetOutput()) feature.BoundaryEdgesOn() feature.FeatureEdgesOn() feature.NonManifoldEdgesOff() feature.ManifoldEdgesOff() feature.SetFeatureAngle(30.0) feature.ColoringOn() feature.Update() edge_data feature.GetOutput() print(特征線總數(shù):, edge_data.GetNumberOfLines())C版本邏輯完全一樣只是調(diào)用方式不同#include vtkSmartPointer.h #include vtkSphereSource.h #include vtkFeatureEdges.h #include vtkPolyData.h auto sphere vtkSmartPointervtkSphereSource::New(); sphere-SetPhiResolution(40); sphere-SetThetaResolution(40); sphere-Update(); auto feature vtkSmartPointervtkFeatureEdges::New(); feature-SetInputData(sphere-GetOutput()); feature-BoundaryEdgesOn(); feature-FeatureEdgesOn(); feature-NonManifoldEdgesOff(); feature-ManifoldEdgesOff(); feature-SetFeatureAngle(30.0); feature-ColoringOn(); feature-Update(); vtkPolyData* edgeData feature-GetOutput(); std::cerr 特征線總數(shù): edgeData-GetNumberOfLines() std::endl;兩個(gè)版本有一個(gè)共同易錯(cuò)點(diǎn)用SetInputConnection時(shí)下游如果沒(méi)顯式Update某些情況下拿到的輸出是空的。穩(wěn)妥做法是取值前顯式調(diào)用一次Update不要賭渲染管線一定會(huì)幫你觸發(fā)。另外GetOutput返回的指針由VTK管線管理不需要你自己delete。如果你的輸入直接來(lái)自VTK數(shù)據(jù)源用SetInputConnection(source-GetOutputPort())更符合管線習(xí)慣我上面寫SetInputData更多是處理已經(jīng)從文件加載進(jìn)內(nèi)存的vtkPolyData。注意vtkFeatureEdges的輸入必須是包含面單元的vtkPolyData。如果你手上只有散點(diǎn)云先走一遍表面重建流程別指望它直接處理點(diǎn)云。3.2 特征線渲染別用細(xì)線直接糊臉套一層Tube光拿到線數(shù)據(jù)還不夠渲染時(shí)通常要當(dāng)作高亮層疊在原網(wǎng)格上面。直接用vtkPolyDataMapper渲染lines特征線只有一兩個(gè)像素粗根本看不清。我通常會(huì)給線套一層vtkTubeFilter讓每條線變成有半徑的管道視覺(jué)效果立竿見(jiàn)影tubes vtk.vtkTubeFilter() tubes.SetInputConnection(feature.GetOutputPort()) tubes.SetRadius(0.02) tubes.SetNumberOfSides(8) tubes.CappingOn() mapper_line vtk.vtkPolyDataMapper() mapper_line.SetInputConnection(tubes.GetOutputPort()) actor_line vtk.vtkActor() actor_line.SetMapper(mapper_line) actor_line.GetProperty().SetColor(1.0, 0.0, 0.0) mapper_mesh vtk.vtkPolyDataMapper() mapper_mesh.SetInputData(sphere.GetOutput()) actor_mesh vtk.vtkActor() actor_mesh.SetMapper(mapper_mesh) actor_mesh.GetProperty().SetOpacity(0.3)這里Tube半徑要結(jié)合模型尺度。模型包圍盒對(duì)角線長(zhǎng)10個(gè)單位時(shí)半徑0.02幾乎看不見(jiàn)取包圍盒對(duì)角線長(zhǎng)度的1/200作為初始值再按需微調(diào)。還有個(gè)細(xì)節(jié)原網(wǎng)格設(shè)為半透明之后特征線疊在上面才不會(huì)被完全遮住。如果后續(xù)還要做交互拾取建議把特征線的顯示層級(jí)調(diào)高一點(diǎn)避免拾取時(shí)命中了帶著三角形面的原網(wǎng)格而不是細(xì)線。3.3 交互式提取用鼠標(biāo)坐標(biāo)定位最近的特征線純靜態(tài)提取始終差點(diǎn)意思。網(wǎng)格很復(fù)雜、特征線很多時(shí)你經(jīng)常會(huì)想要點(diǎn)一下某條特征線看它對(duì)應(yīng)模型的哪個(gè)結(jié)構(gòu)。這個(gè)需求落到VTK里就兩件事獲取鼠標(biāo)坐標(biāo)再做拾取。用vtkCellPicker最合適因?yàn)樗芊祷啬泓c(diǎn)中的單元編號(hào)而特征線輸出的每個(gè)線單元正好就是一個(gè)cell拾取到之后再取它的世界坐標(biāo)就很方便。下面是我項(xiàng)目里的一個(gè)精簡(jiǎn)模板假設(shè)你已經(jīng)創(chuàng)建了渲染窗口和交互器變量名是iren核心是往交互器上掛觀察者在左鍵點(diǎn)擊事件里做pickpicker vtk.vtkCellPicker() picker.SetTolerance(0.005) def on_left_button(obj, event): x, y obj.GetEventPosition() renderer obj.GetCurrentRenderer() picker.Pick(x, y, 0, renderer) if picker.GetCellId() 0: world_pos picker.GetPickPosition() print(f點(diǎn)擊特征線屏幕坐標(biāo)({x},{y}), 世界坐標(biāo)({world_pos[0]:.3f}, {world_pos[1]:.3f}, {world_pos[2]:.3f})) else: print(沒(méi)有點(diǎn)到特征線) style vtk.vtkInteractorStyleTrackballCamera() iren.SetInteractorStyle(style) style.AddObserver(LeftButtonPressEvent, on_left_button)這里有個(gè)坐標(biāo)變換的細(xì)節(jié)經(jīng)常讓新手困惑窗口坐標(biāo)系通常是左上角為原點(diǎn)VTK內(nèi)部渲染坐標(biāo)系卻是左下角為原點(diǎn)所以直接用屏幕坐標(biāo)去對(duì)應(yīng)世界坐標(biāo)時(shí)Y軸方向偏差在所難免。最穩(wěn)的辦法不是手動(dòng)算坐標(biāo)軸而是像我上面這樣把位置交給vtkCellPicker去處理讓它返回正確的世界坐標(biāo)拾取位置。拿到鼠標(biāo)坐標(biāo)之后能做更多事我用vtkKdTree先建特征線頂點(diǎn)索引拾取后立刻找離點(diǎn)擊位置最近的特征點(diǎn)再用vtkGlyph3D在對(duì)應(yīng)位置放一個(gè)高亮小球。整個(gè)流程純疊加層不修改原始網(wǎng)格就算幾十萬(wàn)條特征線也能保持實(shí)時(shí)響應(yīng)。另外picker對(duì)象建議在初始化時(shí)創(chuàng)建并復(fù)用不要在回調(diào)里反復(fù)new。頻繁創(chuàng)建picker對(duì)象雖然不會(huì)崩但會(huì)有無(wú)謂的分配開(kāi)銷交互密集時(shí)性能差異能感覺(jué)出來(lái)。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄這些坑我全踩過(guò)4.1 特征數(shù)量爆炸幾十萬(wàn)條線糊滿整個(gè)屏幕這個(gè)現(xiàn)象多半由兩個(gè)因素觸發(fā)。第一FeatureAngle設(shè)得太小表面噪聲全被當(dāng)成特征第二誤開(kāi)了ManifoldEdgesOn普通邊全部涌出。排查第一步打印特征線數(shù)量、總長(zhǎng)度和長(zhǎng)度分布。噪聲引起的特征線普遍很短調(diào)大閾值后數(shù)量會(huì)出現(xiàn)明顯斷崖如果數(shù)量紋絲不動(dòng)回頭檢查是不是開(kāi)著流形邊。第二步如果閾值調(diào)到很高數(shù)量還是不降那基本能確認(rèn)輸入網(wǎng)格太臟先用vtkSmoothPolyDataFilter做平滑、或者重新remesh再跑特征提取不要拿原始掃描網(wǎng)格硬扛。注意關(guān)于FeatureAngle的調(diào)參我的習(xí)慣是“少量多次”。每次只改2到5度記錄輸出線數(shù)量和平均線長(zhǎng)連續(xù)對(duì)比三條記錄后再確定最終值。一次猛調(diào)15度往往會(huì)把真正需要的棱線連帶刪光。4.2 特征線斷成碎片連不成一條完整折痕這是所有特征提取繞不開(kāi)的痛。vtkFeatureEdges按邊輸出每條輸出對(duì)應(yīng)一個(gè)2點(diǎn)的vtkLine cell所以就算兩條線幾何上首尾相連在拓?fù)渖弦彩莾蓚€(gè)獨(dú)立cell。想要拿到連續(xù)曲線至少兩步先用vtkCleanPolyData把重復(fù)坐標(biāo)點(diǎn)合并再用vtkStripper把相鄰線段串成polyline。如果串出來(lái)的polyline還是斷的大概率是FeatureAngle剛好卡在邊界上個(gè)別關(guān)鍵邊被漏標(biāo)。我自己用的補(bǔ)線工具大致分四步第一步vtkCleanPolyData合并重復(fù)點(diǎn)第二步遍歷所有l(wèi)ine cell建立每個(gè)點(diǎn)的鄰接線列表第三步找出所有只有一條鄰接線的自由端點(diǎn)按距離閾值聚成組同一組內(nèi)兩個(gè)自由端用直線段或者沿網(wǎng)格最短路補(bǔ)上第四步聚完后再整體用vtkStripper串一次。這個(gè)方法比單純調(diào)閾值可控得多尤其適合機(jī)械零件上的圓角過(guò)渡線因?yàn)槟切┑胤降慕嵌缺緛?lái)就是漸變的閾值怎么設(shè)都容易斷。4.3 邊界邊數(shù)量對(duì)不上要么為0要么多到離譜邊界邊數(shù)量異常先別懷疑算法八成是輸入網(wǎng)格的真實(shí)狀態(tài)。第一種情況網(wǎng)格看起來(lái)開(kāi)放但邊界邊為0這通常意味著數(shù)據(jù)是“假封閉”各個(gè)片體被焊死了你的預(yù)期就要調(diào)整。第二種情況邊界邊多得離譜說(shuō)明模型存在大量細(xì)小裂縫常見(jiàn)于從標(biāo)簽圖像轉(zhuǎn)等值面的過(guò)程或者重復(fù)點(diǎn)導(dǎo)致面片之間拓?fù)浠ゲ幌噙B。用vtkCleanPolyData清理一遍重復(fù)點(diǎn)再統(tǒng)一計(jì)算一次法向大概率能解決九成的邊界誤判。處理完還不對(duì)就用vtkPolyDataNormals檢查法向方向是否一致方向不一致的網(wǎng)格特征邊和邊界邊都會(huì)同時(shí)錯(cuò)亂。4.4 大數(shù)據(jù)集上的性能等待別讓特征提取卡住交互幾十萬(wàn)面片對(duì)vtkFeatureEdges來(lái)說(shuō)單次耗時(shí)其實(shí)不算夸張真正煩人的是GUI里拖動(dòng)滑塊反復(fù)調(diào)FeatureAngle每次都整管更新界面直接卡死。我的三級(jí)降載方案可以分享第一級(jí)提取前先用vtkDecimatePro把面數(shù)降到能接受的水平第二級(jí)緩存特征提取結(jié)果只有FeatureAngle變化超過(guò)設(shè)定步長(zhǎng)才重算第三級(jí)在Qt界面里把重算丟給后臺(tái)線程避免阻塞渲染。三級(jí)疊加之后再大的數(shù)據(jù)交互起來(lái)也算順滑。還有一種更隱蔽的性能問(wèn)題輸入網(wǎng)格里如果有大量重復(fù)點(diǎn)vtkFeatureEdges會(huì)花很多時(shí)間在一致性檢查上。所以在上游加一次vtkCleanPolyData不光是正確性問(wèn)題也是性能問(wèn)題。5. 進(jìn)階玩法特征邊的價(jià)值在提取之后才真正放大5.1 用ColoringOn給網(wǎng)格做“拓?fù)潴w檢”打開(kāi)ColoringOn特征邊就會(huì)帶上分類標(biāo)量此時(shí)給Mapper配一個(gè)vtkLookupTable打開(kāi)CellData著色模式表面立刻變成“紅是開(kāi)口、藍(lán)是棱線、綠是壞邊”的體檢圖。這個(gè)技巧對(duì)來(lái)源不明的網(wǎng)格尤其實(shí)用。有一次我處理一個(gè)牙科掃描網(wǎng)格打開(kāi)分類著色才發(fā)現(xiàn)模型某個(gè)區(qū)域藏了大量非流形邊后果是該區(qū)域的任何分割、簡(jiǎn)化操作都不正常。那段區(qū)域后來(lái)做了一遍remesh后續(xù)分割算法的準(zhǔn)確性立刻提升。在大型模型上靠肉眼逐條檢查邊的狀態(tài)是不可能的分類著色就是最高效的篩選方式。5.2 特征邊 網(wǎng)格簡(jiǎn)化把硬邊保護(hù)起來(lái)做網(wǎng)格簡(jiǎn)化的人都知道面片從100萬(wàn)壓到10萬(wàn)機(jī)械零件上的棱線和圓角往往先被磨平。vtkFeatureEdges在這里可以扮演“保護(hù)名單”的角色先提取特征邊把特征邊涉及的頂點(diǎn)索引收集起來(lái)再做帶約束的簡(jiǎn)化。vtkDecimatePro本身有PreserveEdges之類的選項(xiàng)但實(shí)際效果和數(shù)據(jù)結(jié)構(gòu)相關(guān)性很強(qiáng)并不總是可靠。我現(xiàn)在更常自己維護(hù)一個(gè)頂點(diǎn)集合把它作為簡(jiǎn)化時(shí)“不允許刪除”的名單傳給簡(jiǎn)化器實(shí)測(cè)下來(lái)棱線可以保留八成左右。收集特征頂點(diǎn)集合的思路很簡(jiǎn)單遍歷輸出的lines用vtkCellArray取出每個(gè)line的兩個(gè)點(diǎn)Id寫進(jìn)一個(gè)set容器。偽代碼大概是這樣的std::setvtkIdType protectedIds; vtkCellArray* lines edgeData-GetLines(); vtkIdType npts; const vtkIdType* pts; for (lines-InitTraversal(); lines-GetNextCell(npts, pts); ) { for (vtkIdType i 0; i npts; i) protectedIds.insert(pts[i]); }這個(gè)集合可以作為約束簡(jiǎn)化的輸入。把特征邊當(dāng)成簡(jiǎn)化保形的起點(diǎn)而不是終點(diǎn)才是它真正有價(jià)值的使用方式。5.3 把特征提取模塊嵌進(jìn)Qt界面實(shí)時(shí)調(diào)參桌面工具里特征提取經(jīng)常需要一個(gè)帶滑塊的調(diào)參面板。VTK 9時(shí)代推薦用QVTKOpenGLNativeWidget配合vtkGenericOpenGLRenderWindow而不是老舊的QVTKWidget。核心代碼長(zhǎng)這樣from PyQt5.QtWidgets import QSlider, QVBoxLayout, QWidget from vtk.qt.QVTKOpenGLNativeWidget import QVTKOpenGLNativeWidget import vtk class FeatureWidget(QWidget): def __init__(self, mesh): super().__init__() self.vtk_widget QVTKOpenGLNativeWidget() self.render_window vtk.vtkGenericOpenGLRenderWindow() self.vtk_widget.setRenderWindow(self.render_window) self.feature vtk.vtkFeatureEdges() self.feature.SetInputData(mesh) self.feature.FeatureEdgesOn() self.feature.BoundaryEdgesOn() self.feature.SetFeatureAngle(30.0) self.feature.Update() self.slider QSlider() self.slider.setRange(1, 90) self.slider.setValue(30) self.slider.valueChanged.connect(self.update_angle) layout QVBoxLayout() layout.addWidget(self.vtk_widget) layout.addWidget(self.slider) self.setLayout(layout) self.vtk_widget.GetRenderWindow().Render() def update_angle(self, value): self.feature.SetFeatureAngle(float(value)) self.feature.Update() self.vtk_widget.GetRenderWindow().Render()這里有幾個(gè)細(xì)節(jié)值得注意。第一QVTKOpenGLNativeWidget必須綁定一個(gè)vtkGenericOpenGLRenderWindow實(shí)例不能直接塞一個(gè)默認(rèn)RenderWindow進(jìn)去否則OpenGL上下文會(huì)出問(wèn)題。第二滑塊拖動(dòng)過(guò)程要加防抖不必每個(gè)像素都觸發(fā)重算簡(jiǎn)單的做法是只響應(yīng)release事件或者用定時(shí)器合并連續(xù)變化。第三如果后臺(tái)線程里觸發(fā)了vtkFeatureEdges重算記得把結(jié)果回傳到Qt主線程后再調(diào)用Render不要跨線程直接操作渲染窗口。5.4 特征邊作為分割、測(cè)量與配準(zhǔn)的起點(diǎn)特征邊提取完下游能接的東西非常多。用vtkConnectivityFilter對(duì)提取出的特征線做連通成分分析可以把模型表面按“被特征邊圍成的區(qū)域”切成獨(dú)立區(qū)塊作為交互式分割的初始掩膜。做測(cè)量工具時(shí)拾取一條特征邊后取該邊兩個(gè)鄰接面片的法向量計(jì)算夾角把這個(gè)角度回填到界面里就是一個(gè)現(xiàn)成的折角測(cè)量模塊。把特征邊點(diǎn)集轉(zhuǎn)成頂點(diǎn)數(shù)據(jù)后再走點(diǎn)云處理管線也常用于跨模態(tài)幾何特征對(duì)齊的預(yù)實(shí)驗(yàn)。我實(shí)際項(xiàng)目里還遇到過(guò)一個(gè)有意思的用法把特征邊按長(zhǎng)度排序過(guò)濾掉太短的碎線后剩下的長(zhǎng)特征線直接作為模型“骨架”用來(lái)做對(duì)象姿態(tài)估計(jì)的粗對(duì)齊。整個(gè)過(guò)程不復(fù)雜但效果比單純用原始網(wǎng)格做全局配準(zhǔn)要快得多因?yàn)樘卣骶€的點(diǎn)數(shù)比面片數(shù)量少兩三個(gè)數(shù)量級(jí)。說(shuō)到底vtkFeatureEdges產(chǎn)出的是底層拓?fù)湫畔⒄嬲膬r(jià)值要靠后續(xù)算法來(lái)放大它負(fù)責(zé)把“哪里是結(jié)構(gòu)線”說(shuō)清楚剩下的交給業(yè)務(wù)邏輯。最后說(shuō)點(diǎn)個(gè)人的使用體會(huì)。做了幾年網(wǎng)格處理我最大的感觸是vtkFeatureEdges這個(gè)類本身很小、參數(shù)也少真正拉開(kāi)差距的往往是預(yù)處理和后續(xù)處理。很多人把特征提取看成一步濾鏡操作拿著原始網(wǎng)格直接懟進(jìn)去結(jié)果不符預(yù)期就到處找替代方案。其實(shí)網(wǎng)格數(shù)據(jù)類型千差萬(wàn)別臟活永遠(yuǎn)在前后兩端前端做好clean、smooth、法向統(tǒng)一后端做線條連通、過(guò)濾、分類中間那一小步交給vtkFeatureEdges是最省心的。如果你正在為棱線提不出來(lái)、輪廓斷成碎片頭疼我建議先檢查網(wǎng)格質(zhì)量再耐心調(diào)FeatureAngle最后才考慮換算法。順便分享一個(gè)小習(xí)慣每次修改參數(shù)后把輸出記錄成一行日志哪怕簡(jiǎn)單兩句話積累幾十組數(shù)據(jù)之后你對(duì)“什么樣的網(wǎng)格配什么樣的閾值”就會(huì)形成直覺(jué)。這個(gè)小類可能永遠(yuǎn)不會(huì)登上算法頭條但它是整個(gè)網(wǎng)格處理鏈路上最可靠的基石之一。