2013-12-20 127 views
25

我試圖檢測圖像中的完整圓和半圓。 enter image description here在opencv中檢測半圓

我下面下面提到步驟: 過程圖像(包括Canny邊緣檢測) 查找輪廓和繪製它們的空的圖像上,這樣我可以消除不必要的分量。 (處理後的圖像正是我想要的。) 使用HoughCircles檢測圓圈。這就是我得到的。

enter image description here

我試圖在HoughCircles改變參數但因爲它變化的基礎上照明和圓形的圖像的位置的結果並不一致。 我接受或拒絕一個基於它的大小的圓圈。所以結果是不可接受的。此外,我還有一長串「可接受」的圈子,所以我需要在HoughCircle params中留出一些補貼。 至於整圈,很容易 - 我可以簡單地找到輪廓的「圓度」。問題是半圈!

請找霍夫之前編輯的圖像變換enter image description here

+0

是它的權利,你可以邊哪個/檢測(後精明和Hough)圖像中屬於圈半圈?而你的問題是,霍夫結果的圈子位置不夠好? 如何用所有圓邊(或至少3個正確的圓邊像素)擬合參數化圓? (三點定義一個圓圈!)...爲了使它更健壯,你可以使用RANSAC算法(inlier/outlier counting =>巨大的缺失部分=半圓)。 沒有嘗試過,但可能有效?!? – Micka

+0

謝謝米卡!你所說的參數化圓是霍夫變換算法本身,不是嗎?如果你看到霍夫的結果,在檢測到的圓上有3個以上的位置!我對RANSAC毫無頭緒,我會檢查出來 – harsh

+0

在計算/繪製圓圈之前,您能否提供邊緣圖像? 您是否嘗試用較少的線條厚度繪製輪廓? – Micka

回答

32

直接在圖片上使用houghCircle,不要先提取邊緣。 然後測試每個檢測到的圓,多百分比如何在圖像中確實存在:

int main() 
{ 
    cv::Mat color = cv::imread("../houghCircles.png"); 
    cv::namedWindow("input"); cv::imshow("input", color); 

    cv::Mat canny; 

    cv::Mat gray; 
    /// Convert it to gray 
    cv::cvtColor(color, gray, CV_BGR2GRAY); 

    // compute canny (don't blur with that image quality!!) 
    cv::Canny(gray, canny, 200,20); 
    cv::namedWindow("canny2"); cv::imshow("canny2", canny>0); 

    std::vector<cv::Vec3f> circles; 

    /// Apply the Hough Transform to find the circles 
    cv::HoughCircles(gray, circles, CV_HOUGH_GRADIENT, 1, 60, 200, 20, 0, 0); 

    /// Draw the circles detected 
    for(size_t i = 0; i < circles.size(); i++) 
    { 
     Point center(cvRound(circles[i][0]), cvRound(circles[i][1])); 
     int radius = cvRound(circles[i][2]); 
     cv::circle(color, center, 3, Scalar(0,255,255), -1); 
     cv::circle(color, center, radius, Scalar(0,0,255), 1); 
    } 

    //compute distance transform: 
    cv::Mat dt; 
    cv::distanceTransform(255-(canny>0), dt, CV_DIST_L2 ,3); 
    cv::namedWindow("distance transform"); cv::imshow("distance transform", dt/255.0f); 

    // test for semi-circles: 
    float minInlierDist = 2.0f; 
    for(size_t i = 0; i < circles.size(); i++) 
    { 
     // test inlier percentage: 
     // sample the circle and check for distance to the next edge 
     unsigned int counter = 0; 
     unsigned int inlier = 0; 

     cv::Point2f center((circles[i][0]), (circles[i][1])); 
     float radius = (circles[i][2]); 

     // maximal distance of inlier might depend on the size of the circle 
     float maxInlierDist = radius/25.0f; 
     if(maxInlierDist<minInlierDist) maxInlierDist = minInlierDist; 

     //TODO: maybe paramter incrementation might depend on circle size! 
     for(float t =0; t<2*3.14159265359f; t+= 0.1f) 
     { 
      counter++; 
      float cX = radius*cos(t) + circles[i][0]; 
      float cY = radius*sin(t) + circles[i][1]; 

      if(dt.at<float>(cY,cX) < maxInlierDist) 
      { 
       inlier++; 
       cv::circle(color, cv::Point2i(cX,cY),3, cv::Scalar(0,255,0)); 
      } 
      else 
       cv::circle(color, cv::Point2i(cX,cY),3, cv::Scalar(255,0,0)); 
     } 
     std::cout << 100.0f*(float)inlier/(float)counter << " % of a circle with radius " << radius << " detected" << std::endl; 
    } 

    cv::namedWindow("output"); cv::imshow("output", color); 
    cv::imwrite("houghLinesComputed.png", color); 

    cv::waitKey(-1); 
    return 0; 
} 

對於此輸入:

enter image description here

它給出了這樣的輸出:

enter image description here

紅圈是霍夫結果。

圓圈上的綠色採樣點是正常值。

藍點是異常值。

控制檯輸出:

100 % of a circle with radius 27.5045 detected 
100 % of a circle with radius 25.3476 detected 
58.7302 % of a circle with radius 194.639 detected 
50.7937 % of a circle with radius 23.1625 detected 
79.3651 % of a circle with radius 7.64853 detected 

如果你想測試RANSAC而不是霍夫,看看this

+0

非常棒!它工作:)但爲什麼它不能在邊緣上工作?我的目標是通過減小輪廓尺寸的下限來減少錯誤的圓圈識別......這樣我就可以獲得乾淨的處理圖像。 – harsh

+2

我不確定。我想這是因爲openCv的houghCircle在內部使用了canny。如果你在邊緣工作,canny不起作用。如果你在粗邊上工作,canny會給出兩個互相靠近的圓圈,從而產生干擾。但這只是一個猜測。 – Micka

+0

@harsh:使用RANSAC添加了另一個答案(還有很多工作要做)直接在邊緣圖像上工作 – Micka

0

由霍夫算法檢測到的半圓是最有可能是正確的。這裏的問題可能是,除非嚴格控制場景的幾何形狀,即相機相對於目標的精確位置,以便圖像軸與目標平面垂直,否則將得到省略號而不是投影在圖像上的圓圈平面。更不要說由光學系統引起的變形,這進一步退化了幾何圖形。如果你在這裏依賴精度,我會推薦camera calibration

+0

物體保持在平行於攝像機安裝的玻璃表面上。我同意圖像中存在一些校準問題......但更多的是與被檢測到的圓心[90.3,87.5]而不是[90,87]相關...... – harsh

7

下面是另一種方式來做到這一點,一個簡單的RANSAC版本(要做很多優化以提高速度),它適用於邊緣圖像。直到它被取消

  1. 選擇隨機

    的方法循環這些步驟3的邊緣像素

  2. 從他們
  3. 估計圓(3點就足以確定一個圓)
  4. 驗證或證僞它真的圓:計算給定邊所代表的圓的百分比
  5. 如果驗證了圓,從輸入中去除圓/ egdes

    int main() 
    { 
    //RANSAC 
    
    //load edge image 
    cv::Mat color = cv::imread("../circleDetectionEdges.png"); 
    
    // convert to grayscale 
    cv::Mat gray; 
    cv::cvtColor(color, gray, CV_RGB2GRAY); 
    
    // get binary image 
    cv::Mat mask = gray > 0; 
    //erode the edges to obtain sharp/thin edges (undo the blur?) 
    cv::erode(mask, mask, cv::Mat()); 
    
    std::vector<cv::Point2f> edgePositions; 
    edgePositions = getPointPositions(mask); 
    
    // create distance transform to efficiently evaluate distance to nearest edge 
    cv::Mat dt; 
    cv::distanceTransform(255-mask, dt,CV_DIST_L1, 3); 
    
    //TODO: maybe seed random variable for real random numbers. 
    
    unsigned int nIterations = 0; 
    
    char quitKey = 'q'; 
    std::cout << "press " << quitKey << " to stop" << std::endl; 
    while(cv::waitKey(-1) != quitKey) 
    { 
        //RANSAC: randomly choose 3 point and create a circle: 
        //TODO: choose randomly but more intelligent, 
        //so that it is more likely to choose three points of a circle. 
        //For example if there are many small circles, it is unlikely to randomly choose 3 points of the same circle. 
        unsigned int idx1 = rand()%edgePositions.size(); 
        unsigned int idx2 = rand()%edgePositions.size(); 
        unsigned int idx3 = rand()%edgePositions.size(); 
    
        // we need 3 different samples: 
        if(idx1 == idx2) continue; 
        if(idx1 == idx3) continue; 
        if(idx3 == idx2) continue; 
    
        // create circle from 3 points: 
        cv::Point2f center; float radius; 
        getCircle(edgePositions[idx1],edgePositions[idx2],edgePositions[idx3],center,radius); 
    
        float minCirclePercentage = 0.4f; 
    
        // inlier set unused at the moment but could be used to approximate a (more robust) circle from alle inlier 
        std::vector<cv::Point2f> inlierSet; 
    
        //verify or falsify the circle by inlier counting: 
        float cPerc = verifyCircle(dt,center,radius, inlierSet); 
    
        if(cPerc >= minCirclePercentage) 
        { 
         std::cout << "accepted circle with " << cPerc*100.0f << " % inlier" << std::endl; 
         // first step would be to approximate the circle iteratively from ALL INLIER to obtain a better circle center 
         // but that's a TODO 
    
         std::cout << "circle: " << "center: " << center << " radius: " << radius << std::endl; 
         cv::circle(color, center,radius, cv::Scalar(255,255,0),1); 
    
         // accept circle => remove it from the edge list 
         cv::circle(mask,center,radius,cv::Scalar(0),10); 
    
         //update edge positions and distance transform 
         edgePositions = getPointPositions(mask); 
         cv::distanceTransform(255-mask, dt,CV_DIST_L1, 3); 
        } 
    
        cv::Mat tmp; 
        mask.copyTo(tmp); 
    
        // prevent cases where no fircle could be extracted (because three points collinear or sth.) 
        // filter NaN values 
        if((center.x == center.x)&&(center.y == center.y)&&(radius == radius)) 
        { 
         cv::circle(tmp,center,radius,cv::Scalar(255)); 
        } 
        else 
        { 
         std::cout << "circle illegal" << std::endl; 
        } 
    
        ++nIterations; 
        cv::namedWindow("RANSAC"); cv::imshow("RANSAC", tmp); 
    } 
    
    std::cout << nIterations << " iterations performed" << std::endl; 
    
    
    cv::namedWindow("edges"); cv::imshow("edges", mask); 
    cv::namedWindow("color"); cv::imshow("color", color); 
    
    cv::imwrite("detectedCircles.png", color); 
    cv::waitKey(-1); 
    return 0; 
    } 
    
    
    float verifyCircle(cv::Mat dt, cv::Point2f center, float radius, std::vector<cv::Point2f> & inlierSet) 
    { 
    unsigned int counter = 0; 
    unsigned int inlier = 0; 
    float minInlierDist = 2.0f; 
    float maxInlierDistMax = 100.0f; 
    float maxInlierDist = radius/25.0f; 
    if(maxInlierDist<minInlierDist) maxInlierDist = minInlierDist; 
    if(maxInlierDist>maxInlierDistMax) maxInlierDist = maxInlierDistMax; 
    
    // choose samples along the circle and count inlier percentage 
    for(float t =0; t<2*3.14159265359f; t+= 0.05f) 
    { 
        counter++; 
        float cX = radius*cos(t) + center.x; 
        float cY = radius*sin(t) + center.y; 
    
        if(cX < dt.cols) 
        if(cX >= 0) 
        if(cY < dt.rows) 
        if(cY >= 0) 
        if(dt.at<float>(cY,cX) < maxInlierDist) 
        { 
         inlier++; 
         inlierSet.push_back(cv::Point2f(cX,cY)); 
        } 
    } 
    
    return (float)inlier/float(counter); 
    } 
    
    
    inline void getCircle(cv::Point2f& p1,cv::Point2f& p2,cv::Point2f& p3, cv::Point2f& center, float& radius) 
    { 
        float x1 = p1.x; 
        float x2 = p2.x; 
        float x3 = p3.x; 
    
        float y1 = p1.y; 
        float y2 = p2.y; 
        float y3 = p3.y; 
    
        // PLEASE CHECK FOR TYPOS IN THE FORMULA :) 
        center.x = (x1*x1+y1*y1)*(y2-y3) + (x2*x2+y2*y2)*(y3-y1) + (x3*x3+y3*y3)*(y1-y2); 
        center.x /= (2*(x1*(y2-y3) - y1*(x2-x3) + x2*y3 - x3*y2)); 
    
        center.y = (x1*x1 + y1*y1)*(x3-x2) + (x2*x2+y2*y2)*(x1-x3) + (x3*x3 + y3*y3)*(x2-x1); 
        center.y /= (2*(x1*(y2-y3) - y1*(x2-x3) + x2*y3 - x3*y2)); 
    
        radius = sqrt((center.x-x1)*(center.x-x1) + (center.y-y1)*(center.y-y1)); 
    } 
    
    
    
    std::vector<cv::Point2f> getPointPositions(cv::Mat binaryImage) 
    { 
    std::vector<cv::Point2f> pointPositions; 
    
    for(unsigned int y=0; y<binaryImage.rows; ++y) 
    { 
        //unsigned char* rowPtr = binaryImage.ptr<unsigned char>(y); 
        for(unsigned int x=0; x<binaryImage.cols; ++x) 
        { 
         //if(rowPtr[x] > 0) pointPositions.push_back(cv::Point2i(x,y)); 
         if(binaryImage.at<unsigned char>(y,x) > 0) pointPositions.push_back(cv::Point2f(x,y)); 
        } 
    } 
    
    return pointPositions; 
    } 
    

輸入:

enter image description here

輸出:

enter image description here

控制檯輸出:

press q to stop 
    accepted circle with 50 % inlier 
    circle: center: [358.511, 211.163] radius: 193.849 
    accepted circle with 85.7143 % inlier 
    circle: center: [45.2273, 171.591] radius: 24.6215 
    accepted circle with 100 % inlier 
    circle: center: [257.066, 197.066] radius: 27.819 
    circle illegal 
    30 iterations performed` 

優化應包括:

  1. 使用的所有內圍以適應更好的圓圈

  2. 不要每個檢測圈後計算距離變換(這是相當昂貴)。直接從點/邊集計算inlier,並從該列表中刪除inlier邊。

  3. 如果圖像中有許多小圓圈(和/或大量的噪音),它不可能隨機選擇3個邊緣像素或圓圈。 =>先嚐試輪廓檢測並檢測每個輪廓的圓圈。之後嘗試檢測圖像中剩下的所有「其他」圓圈。

  4. 很多其他的東西

+1

謝謝Micka ......我也會試試這個......但是你之前的解決方案工作得很好..我會嘗試修改HoughCircles來消除Canny邊緣檢測。 – harsh

0

您更好地與不同內核嘗試高斯模糊。那將有助於你

GaussianBlur(src_gray, src_gray, Size(11, 11), 5,5); 

所以更改size(i,i),j,j)

+0

我會考慮使用多個高斯內核,如果你想要走這條路線,也許會製作一個縮放空間。內核的單一改變是不夠的。 – rayryeng

0

我知道這有點遲,但我用了不同的方法h要容易得多。 從cv2.HoughCircles(...)可以得到圓的中心和直徑(x,y,r)。所以我簡單地通過圓的所有中心點,並檢查它們是否比圖像的邊緣距離更遠。

這裏是我的代碼:

 height, width = img.shape[:2] 

     #test top edge 
     up = (circles[0, :, 0] - circles[0, :, 2]) >= 0 

     #test left edge 
     left = (circles[0, :, 1] - circles[0, :, 2]) >= 0 

     #test right edge 
     right = (circles[0, :, 0] + circles[0, :, 2]) <= width 

     #test bottom edge 
     down = (circles[0, :, 1] + circles[0, :, 2]) <= height 

     circles = circles[:, (up & down & right & left), :]