Circle Detection for Fourier-Mellin Shear Estimation

I have the following cross-correlation image:

This image is originally 0-1 floats. I need to detect the circle in the image, but I am having trouble doing so with cv::HoughCircles. Clearly, I’d prefer not to be forced to run Canny edges (preferring instead to binarize the image myself), but the functions give me no choice. Any ideas for parameters or alternative methods? The SNR of this image is representative of the typical problem I am trying to solve.

See this example of “self-binarizing” via simple thresholding that fails cv::HoughCircles:
SelfBinarized

See also this post.

1 Like

see opencv_contrib/edge_drawing.py at 4.x · opencv/opencv_contrib · GitHub

Try to calculate radial distribution

EdgeDrawing seems to work well, but quickly hangs on new input with the default parameters. This attached image is an example where the call to detectEllipses never returns (or takes too long for me to wait around for it to finish). Using 4.5.5. Any ideas?

circles_input

My circle of interest is not guaranteed to be centered at the image center, maybe that was something you were assuming. Though the global image maximum should lay on the ring. Could you expand on your suggestion? Thanks!

could you add the code you tested. i have no problem with your image.

#include <iostream>                                                                                                      
#include <opencv2/core.hpp>                                                     
#include <opencv2/highgui.hpp>                                                  
#include <opencv2/imgproc.hpp>                                                  
#include <opencv2/ximgproc/edge_drawing.hpp>                                    
                                                                                
static void about() {                                                           
  std::cout << "\ntest_edge_drawing\n";                                         
}                                                                               
                                                                                
int main(int argc, const char** argv) {                                         
  const cv::String keys =                                                       
      "{ h help |      | print this message }"                                  
      "{ @image |      | /path/to/image     }";                                 
                                                                                
  cv::CommandLineParser parser(argc, argv, keys);                               
                                                                                
  if (parser.has("help")) {                                                     
    about();                                                                    
    parser.printMessage();                                                      
    std::cout << std::endl;                                                     
    return 0;                                                                   
  }                                                                             
                                                                                
  if (!parser.check()) {                                                        
    parser.printErrors();                                                       
    exit(1);                                                                    
  }                                                                             
                                                                                
  const auto image_path = parser.get<std::string>("@image");                    
  cv::Mat image = cv::imread(image_path, cv::IMREAD_GRAYSCALE);                 
                                                                                
  if (image.empty()) {                                                          
    std::cerr << "Failed to load image " << image_path;                         
    exit(1);                                                                    
  }                                                                             
                                                                                
  bool active = true;                                                           
  int32_t count = 0;
  while (active) {                                                              
    std::cout << "Run #" << count++ << std::endl;                               
                                                                                
    //                                                                          
    // Not efficient, but more closely mimics my initial case:                  
    //                                                                          
    auto ed = cv::ximgproc::createEdgeDrawing();                                
                                                                                
    ed->detectEdges(image);                                                     
    cv::Mat edges;                                                              
    ed->getEdgeImage(edges);                                                    
    cv::imshow("Edges", edges);                                                 
                                                                                
    std::vector<cv::Vec6d> ellipses;                                            
    ed->detectEllipses(ellipses);                                               
                                                                                
    cv::Mat viz = image.clone();                                                
    for(const auto& e : ellipses) {                                             
      cv::ellipse(viz, {e[0], e[1]}, {e[2] + e[3], e[2] + e[4]}, e[5], 0,       
      360, {128}, 2, cv::LINE_AA);                                              
    }                                                                           
    cv::imshow("Circles", viz);                                                 
                                                                                
    //                                                                          
    // ESC to terminate:                                                        
    //                                                                          
    if (cv::waitKey(1) == 27) {                                                 
      active = false;                                                           
    }                                                                           
  }                                                                             
                                                                                
  return 0;                                                                     
}

Strangely, I don’t see the hanging with this minimal example…

My failing code snippet is the following:

    cv::normalize(cross_correlation_centered_, cross_correlation_centered_, 0,  
                  1.0, cv::NORM_MINMAX);                                        
                                                                                
    // Circles experiment:                                                      
    Timer timer;                                                                
    timer.start();                                                              
    vision::soft_threshold(cross_correlation_centered_,                         
                           cross_correlation_centered_, 0.6, 4.0);              
    cv::Mat gray;                                                               
    cv::Rect roi(cv::Point(peak.x - 50, peak.y - 50),                           
                 cv::Point(peak.x + 50, peak.y + 50));                          
    vision::convert_and_scale(cross_correlation_centered_(roi), gray, CV_8U);   
    cv::GaussianBlur(gray, gray, {3, 3}, 0.0, 0.0);                             
                                                                                
    static int index = 0;                                                       
    cv::imwrite("circles_input" + std::to_string(index++) + ".png", gray);      
                                                                                
    auto ed = cv::ximgproc::createEdgeDrawing();                                
    ed->detectEdges(gray);                                                      
    cv::Mat edges;                                                              
    ed->getEdgeImage(edges);                                                    
    cv::imshow("Edges", edges);                                                 
    std::vector<cv::Vec6d> ellipses;                                            
    ed->detectEllipses(ellipses);                                               
                                                                                
    LOG(DEBUG) << "Found " << ellipses.size() << " ellipses";                   
    for(const auto& e : ellipses) {                                            
      cv::ellipse(gray, {e[0], e[1]}, {e[2] + e[3], e[2] + e[4]}, e[5], 0,      
      360, {128}, 2, cv::LINE_AA);                                              
    }                                                                           
                                                                                
    LOG(DEBUG) << "Circles: " << timer.stop().elapsed_ms() << "ms";             
    cv::imshow("Circles", gray);

Removing unnecessary calls to cv::imshow, getEdgeImage, and cv::ellipse, etc. makes no difference.

see the last update improvements on EdgeDrawing · opencv/opencv_contrib@081bebd · GitHub

maybe input image is not CV_8UC1

The image is 8UC1, the call to convert and scale does this and it doesn’t hang or crash on the first image it sees, but the second. Here is the gdb output:

#0  0x00007f1dbbc7e7c6 in cv::ximgproc::EdgeDrawingImpl::SplitSegment2Lines(double*, double*, int, int) ()
    at /opt/opencv4.5.5/lib/libopencv_ximgproc.so.405
#1  0x00007f1dbbc7fd71 in cv::ximgproc::EdgeDrawingImpl::detectEllipses(cv::_OutputArray const&) ()
    at /opt/opencv4.5.5/lib/libopencv_ximgproc.so.405

I changed my test program to run through a directory of saved images similar to that shared above and it reliably core dumps, though some images are processed just fine, either with no returned ellipses or a correct one. Here is an example offending image:
circles_input7

seems there is a bug.

as a workaround you can call detectLines() first

vector<Vec4f> lines;
ed->detectLines(lines);
std::vector<cv::Vec6d> ellipses;                                            
ed->detectEllipses(ellipses);

Thanks! Adding this seems to fix it.

also i advice playing with params

for example

ed->params.GradientThresholdValue = 100;
ed->params.NFAValidation= false; // can return more lines and ellipses ( without internal filtering)

Do you not want to run canny for performance reasons, or for some other reason?

Do you know where the circle is going to be in the image, or could you do a first pass to determine where it is? If so you could run canny only on the image ROI that is likely to contain the circle which could really save on processing time.

I have had luck searching for an appropriate canny parameter by starting at the center of a bounded range, and alternately searching higher / lower. When I get no edges back I stop searching higher, and when I get “too many” edges back, I stop searching lower. This might be too expensive for you to do, but I found it works well when you have to be robust to changes in edge intensity etc.

I implemented the circle detection described here:

My notes say “II.8 - II.15” - I don’t recall why I chose this method from the paper, and I don’t recall why I chose this over hough circles, but it provides good results.

i created a PR to fix the bug

1 Like