9

I am working on a program to detect the tips of a probing device and analyze the color change during probing. The input/output mechanisms are more or less in place. What I need now is the actual meat of the thing: detecting the tips.

In the images below, the tips are at the center of the crosses. I thought of applying BFS to the images after some threshold'ing but was then stuck and didn't know how to proceed. I then turned to OpenCV after reading that it offers feature detection in images. However, I am overwhelmed by the vast amount of concepts and techniques utilized here and again, clueless about how to proceed.

Am I looking at it the right way? Can you give me some pointers?

Colored Image Image extracted from short video

Binary image with threshold set at 95 Binary version with threshold set at 95

MrOrdinaire
  • 165
  • 1
  • 6
  • What sort of accuracy are you looking for? Are you OK with commercial software? Will the image have some random rotation or can you align the camera (roughly) with the target? – Guy Sirton Mar 24 '12 at 18:44
  • @GuySirton: can you tell me some options you have on your mind? the tips can be aligned to within 5 degrees. Since this is a high precision instrument, it is desirable for the result to be as highly precise as possible. – MrOrdinaire Mar 24 '12 at 19:02
  • Can you post actual raw images, too? – Niki Mar 24 '12 at 22:36
  • @nikie: What do you mean by raw image? I extracted frames from the video using FFmpeg and saved it as ppm files, then used GIMP2 to convert it to png like this one. – MrOrdinaire Mar 25 '12 at 02:25

3 Answers3

10

Template Matching Approach

Here is a simple matchTemplate solution, that is similar to the approach that Guy Sirton mentions.

Template matching will work as long as you don't have much scaling or rotation occurring with your target.

Here is the template that I used: enter image description here

Here is the code I used to detect several of the unobstructed crosses:

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char* argv[])
{
    string inputName = "crosses.jpg";
    string outputName = "crosses_detect.png";
    Mat img   = imread( inputName, 1);
    Mat templ = imread( "crosses-template.jpg", 1);

    int resultCols =  img.cols - templ.cols + 1;
    int resultRows = img.rows - templ.rows + 1;
    Mat result( resultCols, resultRows, CV_32FC1 );

    matchTemplate(img, templ, result, CV_TM_CCOEFF);
    normalize(result, result, 0, 255.0, NORM_MINMAX, CV_8UC1, Mat());

    Mat resultMask;
    threshold(result, resultMask, 180.0, 255.0, THRESH_BINARY);

    Mat temp = resultMask.clone();
    vector< vector<Point> > contours;
    findContours(temp, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(templ.cols / 2, templ.rows / 2));

    vector< vector<Point> >::iterator i;
    for(i = contours.begin(); i != contours.end(); i++)
    {
        Moments m = moments(*i, false);
        Point2f centroid(m.m10 / m.m00, m.m01 / m.m00);
        circle(img, centroid, 3, Scalar(0, 255, 0), 3);
    }

    imshow("img", img);
    imshow("results", result);
    imshow("resultMask", resultMask);

    imwrite(outputName, img);

    waitKey(0);

    return 0;
}

This results in this detection image:
enter image description here

This code basically sets a threshold to separate the cross peaks from the rest of the image, and then detects all of those contours. Finally, it computes the centroid of each contour to detect the center of the cross.

Shape Detection Alternative

Here is an alternative approach using triangle detection. It doesn't seems as accurate as the matchTemplate approach, but might be an alternative you could play with.

Using findContours we detect all the triangles in the image, which results in the following:

enter image description here

Then I noticed all the triangle vertices cluster near the cross center, so then these clusters are used to centroid the cross center point shown below:

enter image description here

Finally, here is the code that I used to do this:

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <list>

using namespace cv;
using namespace std;

vector<Point> getAllTriangleVertices(Mat& img, const vector< vector<Point> >& contours);
double euclideanDist(Point a, Point b);

vector< vector<Point> > groupPointsWithinRadius(vector<Point>& points, double radius);
void printPointVector(const vector<Point>& points);
Point computeClusterAverage(const vector<Point>& cluster);

int main(int argc, char* argv[])
{
    Mat img   = imread("crosses.jpg", 1);
    double resizeFactor = 0.5;
    resize(img, img, Size(0, 0), resizeFactor, resizeFactor);

    Mat momentImg = img.clone();

    Mat gray;
    cvtColor(img, gray, CV_BGR2GRAY);

    adaptiveThreshold(gray, gray, 255.0, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 19, 15);
    imshow("threshold", gray);
    waitKey();

    vector< vector<Point> > contours;
    findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);

    vector<Point> allTriangleVertices = getAllTriangleVertices(img, contours);

    imshow("img", img);
    imwrite("shape_detect.jpg", img);
    waitKey();

    printPointVector(allTriangleVertices);
    vector< vector<Point> > clusters = groupPointsWithinRadius(allTriangleVertices, 10.0*resizeFactor);
    cout << "Number of clusters: " << clusters.size() << endl;

    vector< vector<Point> >::iterator cluster;
    for(cluster = clusters.begin(); cluster != clusters.end(); ++cluster)
    {
        printPointVector(*cluster);

        Point clusterAvg = computeClusterAverage(*cluster);
        circle(momentImg, clusterAvg, 3, Scalar(0, 255, 0), CV_FILLED);
    }

    imshow("momentImg", momentImg);
    imwrite("centroids.jpg", momentImg);
    waitKey();

    return 0;
}

vector<Point> getAllTriangleVertices(Mat& img, const vector< vector<Point> >& contours)
{
    vector<Point> approxTriangle;
    vector<Point> allTriangleVertices;
    for(size_t i = 0; i < contours.size(); i++)
    {
        approxPolyDP(contours[i], approxTriangle, arcLength(Mat(contours[i]), true)*0.05, true);
        if(approxTriangle.size() == 3)
        {
            copy(approxTriangle.begin(), approxTriangle.end(), back_inserter(allTriangleVertices));
            drawContours(img, contours, i, Scalar(0, 255, 0), CV_FILLED);

            vector<Point>::iterator vertex;
            for(vertex = approxTriangle.begin(); vertex != approxTriangle.end(); ++vertex)
            {
                circle(img, *vertex, 3, Scalar(0, 0, 255), 1);
            }
        }
    }

    return allTriangleVertices;
}

double euclideanDist(Point a, Point b)
{
    Point c = a - b;
    return cv::sqrt(c.x*c.x + c.y*c.y);
}

vector< vector<Point> > groupPointsWithinRadius(vector<Point>& points, double radius)
{
    vector< vector<Point> > clusters;
    vector<Point>::iterator i;
    for(i = points.begin(); i != points.end();)
    {
        vector<Point> subCluster;
        subCluster.push_back(*i);

        vector<Point>::iterator j;
        for(j = points.begin(); j != points.end(); )
        {
            if(j != i &&  euclideanDist(*i, *j) < radius)
            {
                subCluster.push_back(*j);
                j = points.erase(j);
            }
            else
            {
                ++j;
            }
        }

        if(subCluster.size() > 1)
        {
            clusters.push_back(subCluster);
        }

        i = points.erase(i);
    }

    return clusters;
}

Point computeClusterAverage(const vector<Point>& cluster)
{
    Point2d sum;
    vector<Point>::const_iterator point;
    for(point = cluster.begin(); point != cluster.end(); ++point)
    {
        sum.x += point->x;
        sum.y += point->y;
    }

    sum.x /= (double)cluster.size();
    sum.y /= (double)cluster.size();

    return Point(cvRound(sum.x), cvRound(sum.y));
}

void printPointVector(const vector<Point>& points)
{
    vector<Point>::const_iterator point;
    for(point = points.begin(); point != points.end(); ++point)
    {
        cout << "(" << point->x << ", " << point->y << ")";
        if(point + 1 != points.end())
        {
            cout << ", ";
        }
    }
    cout << endl;
}

I fixed a few bugs in my previous implementation, and cleaned the code up a bit. I also tested it with various resize factors, and it seemed to perform quite well. However, after I reached a quarter scale it started to have trouble properly detecting triangles, so this might not work well for extremely small crosses. Also, it appears there is a bug in the moments function as for some valid clusters it was returning (-NaN, -NaN) locations. So, I believe the accuracy is a good bit improved. It may need a few more tweaks, but overall I think it should be a good starting point for you.

I think my triangle detection would work better if the black border around the triangles was a bit thicker/sharper, and if there were less shadows on the triangles themselves.

Hope that helps!

mevatron
  • 13,911
  • 4
  • 55
  • 72
  • can that template be used on images with smaller crosses? Thanks. – MrOrdinaire Mar 25 '12 at 02:29
  • from what I read till now, this template won't work on smaller crosses of the same shape, right? – MrOrdinaire Mar 25 '12 at 12:52
  • @MrOrdinaire That is correct. Template matching does not work very well when your target template can be rotated or scaled. You might try the autocorrelation method mentioned above. Also, feature detection methods (SURF, SIFT, BRIEF, etc...) may be more robust. – mevatron Mar 25 '12 at 20:21
  • Any idea how to convert your program to use multiple threads? I can compile your program normally on VS2010 (after inserting #include in the first lines) and it runs fine. However, as soon as I turn on multi-threaded, it starts complaining. – MrOrdinaire Apr 01 '12 at 08:23
  • @MrOrdinaire What does it complain about? – mevatron Apr 01 '12 at 14:51
  • It cannot free the memory of the vector object after any call to the cv library with it as an argument. I have to create memory for the vector object on the heap and let it leak. – MrOrdinaire Apr 08 '12 at 03:02
  • @MrOrdinaire I'm not exactly sure to what `vector` you are referring. I used lots of vectors in the sample above. All of the vectors should be declared on the stack in my implementation, so I'm not sure why you're using the heap. Ask a new question with more details, and I'll see if I can help. – mevatron Apr 10 '12 at 20:30
4

How about simply determining the autocorrelation as you have a nice periodic pattern in your images.

Let say you have your target image:

enter image description here

And a template image to synchronize to

enter image description here

You can determine the Autocorrelation of both:

enter image description here enter image description here

In both you can detect the ACF peaks, as in this example enter image description here

These peaks you can match against each other using the Hungarian algorithm. Here a match is indicated by a white line.

enter image description here

This gives you a set of matching 2D coordinates, which hopefully satisfy the relationship:

x = Ax'

Where A is tranformation matrix with scaling and rotation. Solving it thus gives you the rotation and the scaling between your template and your target image. Translation can then be ascertained via normal cross-correlation with the partly rectified/corrected image and the template image.

Maurits
  • 2,082
  • 3
  • 28
  • 32
  • can you tell me what I can do then with these data? Sorry I am completely new to computer vision. – MrOrdinaire Mar 25 '12 at 02:34
  • How can I compute the autocorrelation in OpenCV? Thank you. I have found a way to find the angle using 1 DFT and some exhaustive search. Is your way of doing this less computationally complex than mine? – MrOrdinaire Mar 26 '12 at 17:12
2

I have used a commercial tool called HexSight ( http://www.lmi3d.com/product/hexsight ) in the past in a very similar application. We were pretty happy with its performance and accuracy.

If you're looking for something very rough/basic you can try caluclating the cross correlation between a reference image and the image you're looking at. An alternative (which is what HexSight uses) is to start with some edge detection algorithm before attempting to find matches to the reference image. In either case, you can follow up with some refinement algorithm once you have a rough candidate. Given that you are trying to find some particular target type you may be able to apply some heuristics or take advantage of the specific target with a custom algorithm.

Here's an idea for a custom solution, assuming the crosses are evenly spaced, on a grid, and the image is not too distorted:

  • Sum all the rows and columns of the image. This will generate peaks between the rows and columns of the crosses (that are white).
  • Determine the image rotation by searching for an angle the maximizes the width and amplitude of those peaks. You can do a binary search between +/- your maximum expected angle.
  • Once you have detemined the angle you can now use the centerlines of the peaks to determine the probe locations. You will actually have another narrow/smaller peak right around the center of the targets.

Under the assumptions this should be fairly robust and accurate because it uses information from the entire image. It will not be too sensitive to any local issues, it will even work if a target (or a few) is completely missing or obscured.

Having good optics and illumination are pre-requisities to getting high quality results out of any system. The image you've attches doesn't look that great.

Guy Sirton
  • 8,331
  • 2
  • 26
  • 36
  • 1
    Thank you for your timely reply. Since I am completely new to computer vision, I cannot yet understand what you said. Can you explain to me what you mean by summing all the rows and columns? About the quality of the image, the next step for my program is to grab frames directly from the camera, instead of reading from a video file. Hope the quality then will be better. – MrOrdinaire Mar 25 '12 at 02:39
  • 1
    @MrOrdinaire: Imagine the image as an array/grid of grayscale values. Take each column of the array and sum all the grayscale values. Do that for every column and you end up with one number per column. A vector. Do the same for every row and you end up with another vector. Put those in Excel and graph them and you'll see what I mean. Try rotating the image, do the same, graph them again. – Guy Sirton Mar 25 '12 at 03:50
  • I have managed to get the sums as row and column vectors using opencv. How can I tell if a rotation has higher and wider peaks than others? – MrOrdinaire Mar 25 '12 at 13:39
  • @MrOrdinaire: You can determine the width by thresholding the vector. Iterate over the vector, look for start point > threshold and the width will be to the first point < threshold. The height would be the maximum value in this interval. – Guy Sirton Mar 25 '12 at 18:26
  • I have found a way to find the angle using 1 DFT and some exhaustive search. What would be the result of multiplying the vector of sums of rows and the vector of sums of cols? Would it be of any help? I feel it might give me the location of the tips but cannot confirm that as I don't know how to plot the data in 3D. – MrOrdinaire Mar 26 '12 at 17:10