OpenCV實現基於Zernike矩的亞畫素邊緣檢測
在做物體檢測時,由於成本和應用場合的限制,不能夠一味地增加相機的解析度,或者已經用了解析度很高的相機,但是視野範圍很大,仍然無法實現很高的精度,這時就要考慮亞畫素技術,亞畫素技術就是在兩個畫素點之間進行進一步的細分,從而得到亞畫素級別的邊緣點的座標(也就是float型別的座標),一般來說,現有的技術可以做到2細分、4細分,甚至很牛的能做到更高,通過亞畫素邊緣檢測技術的使用,可以節約成本,提高識別精度。
常見的亞畫素技術包括灰度矩、Hu矩、空間矩、Zernike矩等,通過查閱多篇論文,綜合比較精度、時間和實現難度,選擇Zernike矩作為專案中的亞畫素邊緣檢測演算法。基於Zernike矩的邊緣檢測原理,下一篇文章詳細再總結一下,這裡大體把步驟說一下,並使用OpenCV實現。
大體步驟就是,首先確定使用的模板大小,然後根據Zernike矩的公式求出各個模板係數,例如我選擇的是7*7模板(模板越大精度越高,但是運算時間越長),要計算M00,M11R,M11I,M20,M31R,M31I,M40七個Zernike模板。第二步是對待檢測影象進行預處理,包括濾波二值化等,也可以在進行一次Canny邊緣檢測。第三步是將預處理的影象跟7個Zernike矩模板分別進行卷積,得到7個Zernike矩。第四步是把上一步的矩乘上角度校正係數(這是因為利用Zernike的旋轉不變性,Zernike模型把邊緣都簡化成了x=n的直線,這裡要調整回來)。第五步是計算距離引數l和灰度差引數k,根據k和l判斷該點是否為邊緣點。以下是基於OpenCV的實現。
#include <math.h> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; const double PI = 3.14159265358979323846; const int g_N = 7; Mat M00 = (Mat_<double>(7, 7) << 0, 0.0287, 0.0686, 0.0807, 0.0686, 0.0287, 0, 0.0287, 0.0815, 0.0816, 0.0816, 0.0816, 0.0815, 0.0287, 0.0686, 0.0816, 0.0816, 0.0816, 0.0816, 0.0816, 0.0686, 0.0807, 0.0816, 0.0816, 0.0816, 0.0816, 0.0816, 0.0807, 0.0686, 0.0816, 0.0816, 0.0816, 0.0816, 0.0816, 0.0686, 0.0287, 0.0815, 0.0816, 0.0816, 0.0816, 0.0815, 0.0287, 0, 0.0287, 0.0686, 0.0807, 0.0686, 0.0287, 0); Mat M11R = (Mat_<double>(7, 7) << 0, -0.015, -0.019, 0, 0.019, 0.015, 0, -0.0224, -0.0466, -0.0233, 0, 0.0233, 0.0466, 0.0224, -0.0573, -0.0466, -0.0233, 0, 0.0233, 0.0466, 0.0573, -0.069, -0.0466, -0.0233, 0, 0.0233, 0.0466, 0.069, -0.0573, -0.0466, -0.0233, 0, 0.0233, 0.0466, 0.0573, -0.0224, -0.0466, -0.0233, 0, 0.0233, 0.0466, 0.0224, 0, -0.015, -0.019, 0, 0.019, 0.015, 0); Mat M11I = (Mat_<double>(7, 7) << 0, -0.0224, -0.0573, -0.069, -0.0573, -0.0224, 0, -0.015, -0.0466, -0.0466, -0.0466, -0.0466, -0.0466, -0.015, -0.019, -0.0233, -0.0233, -0.0233, -0.0233, -0.0233, -0.019, 0, 0, 0, 0, 0, 0, 0, 0.019, 0.0233, 0.0233, 0.0233, 0.0233, 0.0233, 0.019, 0.015, 0.0466, 0.0466, 0.0466, 0.0466, 0.0466, 0.015, 0, 0.0224, 0.0573, 0.069, 0.0573, 0.0224, 0); Mat M20 = (Mat_<double>(7, 7) << 0, 0.0225, 0.0394, 0.0396, 0.0394, 0.0225, 0, 0.0225, 0.0271, -0.0128, -0.0261, -0.0128, 0.0271, 0.0225, 0.0394, -0.0128, -0.0528, -0.0661, -0.0528, -0.0128, 0.0394, 0.0396, -0.0261, -0.0661, -0.0794, -0.0661, -0.0261, 0.0396, 0.0394, -0.0128, -0.0528, -0.0661, -0.0528, -0.0128, 0.0394, 0.0225, 0.0271, -0.0128, -0.0261, -0.0128, 0.0271, 0.0225, 0, 0.0225, 0.0394, 0.0396, 0.0394, 0.0225, 0); Mat M31R = (Mat_<double>(7, 7) << 0, -0.0103, -0.0073, 0, 0.0073, 0.0103, 0, -0.0153, -0.0018, 0.0162, 0, -0.0162, 0.0018, 0.0153, -0.0223, 0.0324, 0.0333, 0, -0.0333, -0.0324, 0.0223, -0.0190, 0.0438, 0.0390, 0, -0.0390, -0.0438, 0.0190, -0.0223, 0.0324, 0.0333, 0, -0.0333, -0.0324, 0.0223, -0.0153, -0.0018, 0.0162, 0, -0.0162, 0.0018, 0.0153, 0, -0.0103, -0.0073, 0, 0.0073, 0.0103, 0); Mat M31I = (Mat_<double>(7, 7) << 0, -0.0153, -0.0223, -0.019, -0.0223, -0.0153, 0, -0.0103, -0.0018, 0.0324, 0.0438, 0.0324, -0.0018, -0.0103, -0.0073, 0.0162, 0.0333, 0.039, 0.0333, 0.0162, -0.0073, 0, 0, 0, 0, 0, 0, 0, 0.0073, -0.0162, -0.0333, -0.039, -0.0333, -0.0162, 0.0073, 0.0103, 0.0018, -0.0324, -0.0438, -0.0324, 0.0018, 0.0103, 0, 0.0153, 0.0223, 0.0190, 0.0223, 0.0153, 0); Mat M40 = (Mat_<double>(7, 7) << 0, 0.013, 0.0056, -0.0018, 0.0056, 0.013, 0, 0.0130, -0.0186, -0.0323, -0.0239, -0.0323, -0.0186, 0.0130, 0.0056, -0.0323, 0.0125, 0.0406, 0.0125, -0.0323, 0.0056, -0.0018, -0.0239, 0.0406, 0.0751, 0.0406, -0.0239, -0.0018, 0.0056, -0.0323, 0.0125, 0.0406, 0.0125, -0.0323, 0.0056, 0.0130, -0.0186, -0.0323, -0.0239, -0.0323, -0.0186, 0.0130, 0, 0.013, 0.0056, -0.0018, 0.0056, 0.013, 0); int main() { Mat OriginalImage = imread("original.png", 0); Mat SubImage = OriginalImage; Mat NewSmoothImage; medianBlur(OriginalImage, NewSmoothImage, 13); Mat NewAdaThresImage; adaptiveThreshold(NewSmoothImage, NewAdaThresImage, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 7, 4); vector<Point2d> SubEdgePoints; Mat ZerImageM00; filter2D(NewAdaThresImage, ZerImageM00, CV_64F, M00, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D( cannyImage, zerImageM00, CV_64F, M00, Point(-1,-1), 0, BORDER_DEFAULT); Mat ZerImageM11R; filter2D(NewAdaThresImage, ZerImageM11R, CV_64F, M11R, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D( cannyImage, zerImageM11R, CV_64F, M11R, Point(-1, -1), 0, BORDER_DEFAULT); Mat ZerImageM11I; filter2D(NewAdaThresImage, ZerImageM11I, CV_64F, M11I, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D( cannyImage, zerImageM11I, CV_64F, M11I, Point(-1, -1), 0, BORDER_DEFAULT); Mat ZerImageM20; filter2D(NewAdaThresImage, ZerImageM20, CV_64F, M20, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D( cannyImage, zerImageM20, CV_64F, M20, Point(-1, -1), 0, BORDER_DEFAULT); Mat ZerImageM31R; filter2D(NewAdaThresImage, ZerImageM31R, CV_64F, M31R, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D(cannyImage, zerImageM31R, CV_64F, M31R, Point(-1, -1), 0, BORDER_DEFAULT); Mat ZerImageM31I; filter2D(NewAdaThresImage, ZerImageM31I, CV_64F, M31I, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D(cannyImage, zerImageM31I, CV_64F, M31I, Point(-1, -1), 0, BORDER_DEFAULT); Mat ZerImageM40; filter2D(NewAdaThresImage, ZerImageM40, CV_64F, M40, Point(-1, -1), 0, BORDER_DEFAULT); //////////filter2D(cannyImage, zerImageM40, CV_64F, M40, Point(-1, -1), 0, BORDER_DEFAULT); int row_number = NewAdaThresImage.rows; int col_number = NewAdaThresImage.cols; //使用7個的7*7的Zernike模板(其本質是個矩陣)M00、M11R、M11I、M20、M31R、M31I、M40,分別與影象進行卷積,得到每個畫素點的七個Zernike矩Z00、Z11R、Z11I、Z20、Z31R、Z31I、Z40 //對於每個點,根據它的七個Zernike矩,求得距離引數l和灰度差引數k,當l和k都滿足設定的條件時,則判讀該點為邊緣點,並進一步利用上述7個Zernike矩求出該點的亞畫素級座標 //如果l或k不滿足設定的條件,則該點不是邊緣點,轉到下一個點求解距離引數l和灰度差引數k for (int i = 0; i < row_number; i++) { for (int j = 0; j <col_number; j++) { if (ZerImageM00.at<double>(i, j) == 0) { continue; } //compute theta //vector<vector<double> > theta2(0); double theta_temporary = atan2(ZerImageM31I.at<double>(i, j), ZerImageM31R.at<double>(i, j)); //theta2[i].push_back(thetaTem); //compute Z11'/Z31' double rotated_z11 = 0.0; rotated_z11 = sin(theta_temporary)*(ZerImageM11I.at<double>(i, j)) + cos(theta_temporary)*(ZerImageM11R.at<double>(i, j)); double rotated_z31 = 0.0; rotated_z31 = sin(theta_temporary)*(ZerImageM31I.at<double>(i, j)) + cos(theta_temporary)*(ZerImageM31R.at<double>(i, j)); //compute l double l_method1 = sqrt((5 * ZerImageM40.at<double>(i, j) + 3 * ZerImageM20.at<double>(i, j)) / (8 * ZerImageM20.at<double>(i, j))); double l_method2 = sqrt((5 * rotated_z31 + rotated_z11) / (6 * rotated_z11)); double l = (l_method1 + l_method2) / 2; //compute k/h double k, h; k = 3 * rotated_z11 / 2 / pow((1 - l_method2*l_method2), 1.5); h = (ZerImageM00.at<double>(i, j) - k*PI / 2 + k*asin(l_method2) + k*l_method2*sqrt(1 - l_method2*l_method2)) / PI; //judge the edge double k_value = 20.0; double l_value = sqrt(2) / g_N; double absl = abs(l_method2 - l_method1); if (k >= k_value && absl <= l_value) { Point2d point_temporary; point_temporary.x = j + g_N*l*cos(theta_temporary) / 2; point_temporary.y = i + g_N*l*sin(theta_temporary) / 2; SubEdgePoints.push_back(point_temporary); } else { continue; } } } //顯示所檢測到的亞畫素邊緣 for (size_t i = 0; i < SubEdgePoints.size(); i++) { Point center_forshow(cvRound(SubEdgePoints[i].x), cvRound(SubEdgePoints[i].y)); circle(OriginalImage, center_forshow, 1, Scalar(0, 97, 255), 1, 8, 0); } imshow("亞畫素邊緣", OriginalImage); waitKey(0); return 0; }
---------------------------------------7.28更新分割線-------------------------------------------------
1.因為留言要原始碼的朋友比較多,所以我把上面的程式碼補完整了,新的程式碼能夠實現基本的過程,但是針對不同的應用,肯定不是最優的效果,可能需要根據不同的應用場合進行調整;
2.評論裡提到的雙層輪廓,我能想到的是因為對線進行邊緣檢測操作,因為線的兩側相當於都是前景和背景的邊緣,所以兩側各會有一層邊緣,不管是Canny(事實上不少Zernike論文都建議先用canny再用Zernike)還是二值化,只要送入Zernike的是線而不是區域,就會有雙層輪廓,其他的原因我暫時還沒想到,歡迎大家賜教;
3.雙層輪廓並不一定是壞事,比如做圓檢測,最後是要用這些邊緣點來擬合圓,那麼兩層輪廓擬合出來的圓放大了說應該是處於兩層輪廓中間,這樣的結果其實更符合實際情況,但是如果不是規則輪廓,可能會對邊緣點的表達和擬合有些問題;