ahansen
ahansen

Reputation: 234

Filling in a single colour background in OpenCV

I'm writing a program in OpenCV which takes a picture of parasite eggs and attempts to identify at least a large portion of them. My problem is that the input image I have the best results with has a large background. I've tried both filling in the background, and cropping it out, but when I do I get a worse selection of the eggs.

My currently thought out solution is to use the image with the background and then fill it in. It felt like it would be easy because I just want to fill anything outside of that circle with black, but I'm not sure how to actually perform the action. If anyone could point be toward a method to use, or any suggestions that would be great.

Here is a link to what the image looks like:

enter image description here

Thanks!

Upvotes: 1

Views: 2881

Answers (2)

karlphillip
karlphillip

Reputation: 93410

It seems that you need the exterior of the image to be filled with black because it makes it easier to identify the eggs since they will be isolated in white.

But what if the parasite eggs magically appeared as blue? I'll explain this in a second, but this approach would free you from the burden of clicking on the image every time a new sample needs to be analyzed.

I wrote the answer in C++, but you if follow what the code does I'm sure you can quickly translate it to Python.

#include <iostream>
#include <vector>

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


int main(int argc, char* argv[])
{
    // Load input image (3-channel)
    cv::Mat input = cv::imread(argv[1]);
    if (input.empty())
    {
        std::cout << "!!! failed imread()" << std::endl;
        return -1;
    }   

    // Convert the input to grayscale (1-channel)
    cv::Mat grayscale = input.clone();
    cv::cvtColor(input, grayscale, cv::COLOR_BGR2GRAY);

What grayscale looks like at this point:

    // Locate the black circular shape in the grayscale image
    std::vector<std::vector<cv::Point> > contours;
    cv::findContours(grayscale, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);

    // Fill the interior of the largest circular shape found with BLUE
    cv::Mat circular_shape = input.clone();
    for (size_t i = 0; i < contours.size(); i++)
    {
        std::vector<cv::Point> cnt = contours[i];
        double area = cv::contourArea(cv::Mat(cnt));        

        if (area > 500000 && area < 1000000) // magic numbers to detect the right circular shape
        {
            std::cout << "* Area: " << area << std::endl;
            cv::drawContours(circular_shape, contours, i, cv::Scalar(255, 0, 0), 
                             cv::FILLED, 8, std::vector<cv::Vec4i>(), 0, cv::Point() );
        }           
    }   

What circular_shape looks like at this point:

    // Create the output image with the same attributes of the original, i.e. dimensions & 3-channel, so we have a colored result at the end
    cv::Mat output = cv::Mat::zeros(input.size(), input.type());

    // copyTo() uses circular_shape as a mask and copies that exact portion of the input to the output
    input.copyTo(output, circular_shape);

    cv::namedWindow("Eggs", cv::WINDOW_NORMAL | cv::WINDOW_KEEPRATIO);  
    cv::imshow("Eggs", output);
    cv::resizeWindow("Eggs", 800, 600);
    cv::waitKey(0);

    return 0;
}

The output displayed on the window is:

The advantage of this solution is that the user doesn't need to interact with the application to facilitate the detection of the eggs, since they are already painted in blue.

After this, other operations can be done on the output image such as cv::inRange() to isolate colored objects from the rest of the image.

So, for completion's sake I'll add a few more lines of text/code to demonstrate what you could do from this point forward to completely isolate the eggs from the rest of the image:

// Isolate blue pixels on the output image
cv::Mat blue_pixels_only;
cv::inRange(output, cv::Scalar(255, 0, 0), cv::Scalar(255, 0, 0), blue_pixels_only);

What blue_pixels_only looks like at this stage:

// Get rid of pixels on the edges of the shape 
int erosion_type = cv::MORPH_RECT; // MORPH_RECT, MORPH_CROSS, MORPH_ELLIPSE
int erosion_size = 3;
cv::Mat element = cv::getStructuringElement(erosion_type, 
                                            cv::Size(2 * erosion_size + 1, 2 * erosion_size + 1), 
                                            cv::Point(erosion_size, erosion_size));
cv::erode(blue_pixels_only, blue_pixels_only, element);
cv::dilate(blue_pixels_only, blue_pixels_only, element);

cv::imshow("Eggs", blue_pixels_only);
cv::imwrite("blue_pixels_only.png", blue_pixels_only);

What blue_pixels_only looks like at this stage:

Upvotes: 1

ahansen
ahansen

Reputation: 234

Worked out a fix for my problem, I created a mouse event callback which fills in whatever I click with black. Below is the code I used in the callback:

def paint(event, x, y, flags, param):
    global opening                                                                                                                         

    if event == cv2.EVENT_LBUTTONDOWN:
        h, w = opening.shape[:2]
        mask = np.zeros((h+2, w+2), np.uint8)
        cv2.floodFill(opening, mask, (x,y), (0, 0, 0)) 
        cv2.imshow("open", opening)

Upvotes: 1

Related Questions