3
votes

I have a black&white image (single channel, 0's and 255's only) and I'd like to remove small blobs in it that are below a certain area threshold using openCV 3.4.2 in Java.

Now I already found the following thread: Removing blobs from a binary image, which is pretty much the same - but I need some help translating the answer into Java code. Also, since I'd like to remove black spots, I invert the image before and after processing. EDIT: thanks to some kind suggestions I managed to get a working code and modified it so now it's an MCVE.

My attempt so far:

import java.util.ArrayList;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

public class contourCheck {

public static void main(String[] args) {

    // initialises openCV   
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);   

    // reads original black&white image
    Mat binary_image =  Imgcodecs.imread("C:/Users/MyName/Desktop/TestImages/testpic.png", Imgcodecs.CV_LOAD_IMAGE_GRAYSCALE);

    // creates temporary Mat
    Mat temp_image = binary_image;

    // inverts image
    Core.bitwise_not(temp_image,temp_image);

    // finds all contours in the image
    ArrayList<MatOfPoint> contours = new ArrayList<MatOfPoint>();
    Imgproc.findContours(temp_image, contours, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_NONE);

    // deletes contours above minArea to keep only the contours that are supposed to be removed from the image
    int minArea = 10;
    for (int i = 0; i < contours.size(); i++) {
        double area = Imgproc.contourArea(contours.get(i));
        if (area > minArea) {
            contours.remove(i);
        }   
    } 
    System.out.println("number of contours remaining: " + contours.size());  
    for (int j = 0; j < contours.size(); j++) {

        if (j > 0) { // apparently temp_image gets also inverted, therefore it gets inverted here once again
            Core.bitwise_not(temp_image,temp_image);
        }
        // fills in small (<= minArea) contours with 0's  
        Imgproc.drawContours(temp_image, contours,j, new Scalar(0),Core.FILLED);

        // inverts image once again to get the original state
        Core.bitwise_not(temp_image,binary_image);

        // writes image with filtered contours
        Imgcodecs.imwrite("C:/Users/MyName/Desktop/TestImages/test/testpic_filtered" + j + ".png", binary_image);
    }
}
}

Now here's an example picture where I'd like to remove all black spots that are below minArea(= 10):

example binary image ("testpic.png")

What surprises me now is that some of the very big components are removed (e.g. the huge circle with some little circles inside or the huge rectangle) while smaller ones are kept. Is there an error in my code or am I misunderstanding some concepts here? Also, why does bitwise_not also invert temp_image, the source Mat (Core.bitwise_not(temp_image,binary_image))?

Note: the code creates an image for every contour that is removed, meaning that 74 images are created.

Example output (last image after removing all the contours):

image with contours removed

1
Please, turn that into a self-contained MCVE that just compiles, and add some sample input/output images. | drawContours just draws into the Mat you've provided it -- it won't change the channel count. - Dan Mašek
As a way to get more clue about what's happening, move the drawContours into the for loop, and have it draw one contour at a time into a blank image, and visualize it using HighGUI (or just save the images to disk instead). - Dan Mašek
Thank you for your feedback, I'll do those changes when I'm back home later today and then present an update - user4429422
Hi, I turned the code into an MCVE and added sample input/output. - user4429422
Ah, contours.remove(i); inside the loop that iterates over contours. You increment the index, even when you remove, so that you end up skipping some entries. Instead of removing undesireable ones from the original contours array, add the desired ones into a new ArrayList and then draw that. - Dan Mašek

1 Answers

0
votes

The problem lies in the following loop:

for (int i = 0; i < contours.size(); i++) {
    double area = Imgproc.contourArea(contours.get(i));
    if (area > minArea) {
        contours.remove(i);
    }   
}

Notice, that you iterate over contours, and at the same time you sometimes remove elements. Remember that remove will shift all the subsequent elements forward by one position. Now, you increment the index on every iteration. This will cause you to skip a contour, for every one you've removed. Since your current goal is to retain only the small enough contours, the effect is that some of the bigger ones slip through.

My suggestion to solve this problem would be to take a slightly different approach -- rather than removing the undesireable contours, I'd create a new instance of ArrayList<MatOfPoint>, and populate it with the contours I'm interested in and use that in further processing.