5.4.3 邊緣檢測-canny運算元
Canny運算元是John Canny於20世紀80年代提出的一種多級邊緣檢測演算法。John Canny研究了最優邊緣的特性,即檢測到的邊緣要儘可能跟實際的邊緣接近並儘可能的多,同時,要儘量降低噪聲對邊緣檢測的干擾。其計算步驟如下
1)對源影象進行高斯平滑以消除影象中噪聲
2)採用差分法近似計算影象每一個畫素的梯度,並計算梯度的模值和方向
3)對梯度進行"非極大抑制":影象邊緣點梯度值通常在梯度方向是極大值,因此檢測邊緣需要將非極大值賦值0來抑制非邊緣點。檢測方法就是在一個區域性視窗內,如果中心畫素點的梯度不比梯度方向上相鄰兩個畫素值大,那麼該中心畫素點梯度值賦0。
4)雙閾值法檢測邊緣和連線邊緣。取兩個梯度閾值high和low,將梯度影象中小於high的畫素賦0得到邊緣影象I1,該影象能夠接近影象邊緣但是可能會存在間斷點;將梯度影象中小於low的畫素賦0得到邊緣影象I2,該圖中受噪聲影響比較大,但是邊緣資訊更多。在連線邊緣時,以I1為基礎,對非零點進行邊緣跟蹤,如果追蹤過程中出現中斷,則從I2對應畫素點及其鄰域來尋找可以連線的邊緣,直至結束。
以上是Canny運算元的計算步驟。
在VTK中沒有實現一個專門的類來做Canny邊緣檢測。
但是我們可以根據以上步驟來實現:
#include <vtkSmartPointer.h> #include <vtkJPEGReader.h> #include <vtkImageData.h> #include <vtkImageShiftScale.h> #include <vtkImageActor.h> #include <vtkRenderer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkInteractorStyleImage.h> #include <vtkImageCast.h> //影象資料型別轉換 #include <vtkImageGaussianSmooth.h> #include <vtkImageGradient.h> #include <vtkImageMagnitude.h> #include <vtkImageNonMaximumSuppression.h> //將影象中的非區域性峰值設定為0 #include <vtkImageConstantPad.h>//增加影象的大小 #include <vtkImageToStructuredPoints.h>//將vtkImageData格式轉換為規則點集 #include <vtkLinkEdgels.h> //根據點的相鄰關係連線成連續的折線Polyline #include <vtkThreshold.h> #include <vtkGeometryFilter.h> //資料轉換為幾何資料,輸出型別為vtkPolyData #include <vtkSubPixelPositionEdgels.h> #include <vtkCamera.h> #include <vtkProperty.h> #include <vtkStripper.h> //將輸入的多邊形生成三角形帶或者折線段 #include <vtkPolyDataMapper.h> int main(int argc, char* argv[]) { vtkSmartPointer<vtkJPEGReader> reader = vtkSmartPointer<vtkJPEGReader>::New(); reader->SetFileName("data\\lena-gray.jpg"); reader->Update(); vtkSmartPointer<vtkImageCast> ic = vtkSmartPointer<vtkImageCast>::New(); ic->SetOutputScalarTypeToFloat(); ic->SetInputConnection(reader->GetOutputPort()); vtkSmartPointer<vtkImageGaussianSmooth> gs = vtkSmartPointer<vtkImageGaussianSmooth>::New(); gs->SetInputConnection(ic->GetOutputPort()); gs->SetDimensionality(2); //設定要計算梯度的維度 gs->SetRadiusFactors(1, 1, 0); vtkSmartPointer<vtkImageGradient> imgGradient = vtkSmartPointer<vtkImageGradient>::New(); imgGradient->SetInputConnection(gs->GetOutputPort()); imgGradient->SetDimensionality(2); vtkSmartPointer<vtkImageMagnitude> imgMagnitude = vtkSmartPointer<vtkImageMagnitude>::New(); imgMagnitude->SetInputConnection(imgGradient->GetOutputPort()); vtkSmartPointer<vtkImageNonMaximumSuppression> nonMax = vtkSmartPointer<vtkImageNonMaximumSuppression>::New(); nonMax->SetMagnitudeInputData(imgMagnitude->GetOutput()); nonMax->SetVectorInputData(imgGradient->GetOutput()); nonMax->SetDimensionality(2); vtkSmartPointer<vtkImageConstantPad> pad = vtkSmartPointer<vtkImageConstantPad>::New(); pad->SetInputConnection(imgGradient->GetOutputPort()); pad->SetOutputNumberOfScalarComponents(3);//用於設定輸出影象的畫素資料組分個數 pad->SetConstant(0); //設定輸出影象中擴大的區域畫素值 vtkSmartPointer<vtkImageToStructuredPoints> i2sp1 = vtkSmartPointer<vtkImageToStructuredPoints>::New();//將vtkImageData格式轉換為規則點集 i2sp1->SetInputConnection(nonMax->GetOutputPort()); i2sp1->SetVectorInputData(pad->GetOutput()); vtkSmartPointer<vtkLinkEdgels> imgLink = vtkSmartPointer<vtkLinkEdgels>::New(); //根據點的相鄰關係連線成連續的折線Polyline imgLink->SetInputData(i2sp1->GetOutput()); imgLink->SetGradientThreshold(2); vtkSmartPointer<vtkThreshold> thresholdEdgels = vtkSmartPointer<vtkThreshold>::New(); thresholdEdgels->SetInputConnection(imgLink->GetOutputPort()); thresholdEdgels->ThresholdByUpper(10); //用於獲取輸入任意型別資料的滿足閾值條件的單元資料 thresholdEdgels->AllScalarsOff(); vtkSmartPointer<vtkGeometryFilter> gf = vtkSmartPointer<vtkGeometryFilter>::New(); //將資料轉換為幾何資料,輸出型別為vtkPolyData gf->SetInputConnection(thresholdEdgels->GetOutputPort()); vtkSmartPointer<vtkImageToStructuredPoints> i2sp = vtkSmartPointer<vtkImageToStructuredPoints>::New(); i2sp->SetInputConnection(imgMagnitude->GetOutputPort()); i2sp->SetVectorInputData(pad->GetOutput()); //接收一系列連續曲線及其對應的梯度系資訊作為輸入,利用梯度資訊來調整曲線位置 vtkSmartPointer<vtkSubPixelPositionEdgels> spe = vtkSmartPointer<vtkSubPixelPositionEdgels>::New(); spe->SetInputConnection(gf->GetOutputPort()); spe->SetGradMapsData(i2sp->GetStructuredPointsOutput()); vtkSmartPointer<vtkStripper> strip = vtkSmartPointer<vtkStripper>::New(); strip->SetInputConnection(spe->GetOutputPort()); vtkSmartPointer<vtkPolyDataMapper> dsm = vtkSmartPointer<vtkPolyDataMapper>::New(); dsm->SetInputConnection(strip->GetOutputPort()); dsm->ScalarVisibilityOff(); vtkSmartPointer<vtkActor> planeActor = vtkSmartPointer<vtkActor>::New(); planeActor->SetMapper(dsm); planeActor->GetProperty()->SetAmbient(1.0); planeActor->GetProperty()->SetDiffuse(0.0); planeActor->GetProperty()->SetColor(1.0, 0.0, 0.0); vtkSmartPointer<vtkImageActor> originalActor = vtkSmartPointer<vtkImageActor>::New(); originalActor->SetInputData(reader->GetOutput()); double originalViewport[4] = { 0.0, 0.0, 0.5, 1.0 }; double gradviewport[4] = { 0.5, 0.0, 1.0, 1.0 }; vtkSmartPointer<vtkRenderer> originalRenderer = vtkSmartPointer<vtkRenderer>::New(); originalRenderer->SetViewport(originalViewport); originalRenderer->AddActor(originalActor); originalRenderer->ResetCamera(); originalRenderer->SetBackground(1.0, 1.0, 1.0); vtkSmartPointer<vtkRenderer> gradRenderer = vtkSmartPointer<vtkRenderer>::New(); gradRenderer->SetViewport(gradviewport); gradRenderer->AddActor(planeActor); gradRenderer->ResetCamera(); gradRenderer->SetBackground(1.0, 1.0, 1.0); vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New(); renderWindow->SetSize(900, 300); renderWindow->AddRenderer(originalRenderer); renderWindow->AddRenderer(gradRenderer); renderWindow->Render(); renderWindow->SetWindowName("CannyExample"); vtkSmartPointer<vtkRenderWindowInteractor> renderWindowInteractor = vtkSmartPointer<vtkRenderWindowInteractor>::New(); vtkSmartPointer<vtkInteractorStyleImage> style = vtkSmartPointer<vtkInteractorStyleImage>::New(); renderWindowInteractor->SetInteractorStyle(style); renderWindowInteractor->SetRenderWindow(renderWindow); renderWindowInteractor->Initialize(); renderWindowInteractor->Start(); return EXIT_SUCCESS; }
該程式執行出來的結果有問題,還有待解決!
該程式比較複雜,處理邊緣時將其作為幾何資料來進行處理。因此涉及了部分幾何資料操作的filter,這裡如果不明白可以先放一下,再幾何資料處理部分會做詳細介紹。
程式首先讀入影象,計算影象的梯度和模值。
接下來按照Canny運算元的步驟進行處理。
我們詳細介紹用到的相應的filter:
vtkImageNonMaximumSuppression將影象中的非區域性峰值設定為0,輸入和輸出型別都是vtkImageData:其中輸入有兩個,模值影象(magnitude)和向量影象,一個典型的應用就是輸入梯度模值影象和梯度向量影象對梯度做非極大值抑制。
vtkImageConstantPad增加影象的大小,其輸入和輸出都為vtkImageData。其中函式SetOutputNumberOfScalarComponents(3)用於設定輸出影象的畫素資料組分個數,函式SetConstant(0)用於設定輸出影象中擴大的區域畫素值。而SetOutputWholeExtent()則用於設定輸出影象的範圍。這裡的作用是將梯度影象畫素的組分修改為3,方便下面vtkImageToStructuredPoints使用。
vtkImageToStructuredPoints將vtkImageData格式轉換為規則點集。該類的輸入型別是vtkImageData,另外還有一個可選的RGB三組分向量影象輸入;輸出型別是vtkStructuredPoints,當輸入向量影象時,向量影象畫素資料會轉為輸出影象的對應點的屬性。
vtkLinkEdgels類根據點的相鄰關係連線成連續的折線Polyline。其內部閾值變數GradientThreshold,可以用來排除輸入點中梯度值小於該閾值的點。當使用vtkLinkEdgels進行Canny運算元的雙閾值邊緣檢測時,GradientThreshold可以用作較小的閾值。設定該閾值的函式是SetGradientThreshold(2)。
vtkThreshold用於獲取輸入任意型別資料的滿足閾值條件的單元資料。該類的輸入為VTK的任意資料型別,輸出資料型別是不規則網格。閾值設定有:大於閾值,小於閾值和介於兩個閾值之間。內部提供了兩種屬性模式AttributeMode設定,即閾值比較時是採用的點屬性還是單元屬性,預設下是點屬性。而當屬性為多元資料時,還需要設定閾值比較時使用哪個組分的資料。其中提供了三種模式選擇,所有組分都滿足閾值條件,任意一個滿足閾值條件和使用者指定的組分滿足閾值條件。當使用點屬性資料時,如果設定了AllScalars,那麼單元滿足閾值條件的前提會是其所有點的屬性都滿足閾值條件。這裡將閾值設定為10,即Canny中雙閾值的較大閾值。
vtkGeometryFilter將資料轉換為幾何資料,輸出型別為vtkPolyData。該類從vtkThreshold的輸出中提取影象邊緣的幾何資料。
vtkSubPixelPositionEdgels接收一系列連續曲線及其對應的梯度系資訊作為輸入,利用梯度資訊來調整曲線位置。這裡對前面提取的影象邊緣再根據其梯度進行調整。
vtkStripper用來將輸入的多邊形、三角形或者線段生成三角形帶或者折線段。輸入的多邊形資料必須是三角形,否則不會進行帶化處理。因此處理多邊形資料時,可以先用vtkTriangleFilter進行三角化後再使用本類。如果輸入中存在孤立點的話,也不會進行任何處理。預設情況下,該filter處理後會丟棄掉屬性資料。
參考資料:
1.《The Visualization Toolkit – AnObject-Oriented Approach To 3D Graphics (4th Edition)》
2. 張曉東, 羅火靈. VTK圖形影象開發進階[M]. 機械工業出版社, 2015.
所用軟體:vtk7.0+visual studio 2013
注:此文知識學習筆記,僅記錄完整程式和實現結果,具體原理參見:
https://blog.csdn.net/www_doling_net/article/details/8541534
https://blog.csdn.net/shenziheng1/article/category/6114053/4