Search code examples
javaopencvcentroid

Center of mass computation yields wrong results in OpenCV


I will start by stating that I'm slowly going insane. I am trying to extract contours from an image and compute their centers of mass using Java and OpenCV.

For all the inner contours, the results are correct, however for the outer (largest) contour, the centroid is way, way off. The input image, the code and the output result are all below. OpenCV version is 3.1.

Others have had this problem and the suggestions were to:

  1. Check if the contour is closed. It is, I checked.
  2. Use Canny to detect edges before extracting contours. I don't understand why that's necessary, but I tried it and the result is that it messes up the tree hierarchy since it generates two contours for each edge, which is not something I want.

The input image is very large (27MB) and the weird part is that when I resized it to 1000x800, the center of mass suddenly got computed correctly, however, I need to be able to process the image at the original resolution.

/*
     * To change this license header, choose License Headers in Project Properties.
     * To change this template file, choose Tools | Templates
     * and open the template in the editor.
 */
package com.philrovision.dxfvision.matching;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.imgproc.Moments;
import org.testng.annotations.Test;

/**
 *
 * @author rhobincu
 */
public class MomentsNGTest {

    @Test
    public void testOpenCvMoments() {
        Mat image = Imgcodecs.imread("moments_fail.png");
        Mat channel = new Mat();
        Core.extractChannel(image, channel, 1);
        Mat mask = new Mat();
        Imgproc.threshold(channel, mask, 191, 255, Imgproc.THRESH_BINARY);

        Mat filteredMask = new Mat();
        Imgproc.medianBlur(mask, filteredMask, 5);

        List<MatOfPoint> allContours = new ArrayList<>();
        Mat hierarchy = new Mat();

        Imgproc.findContours(filteredMask, allContours, hierarchy, Imgproc.RETR_TREE,
                Imgproc.CHAIN_APPROX_SIMPLE, new Point(0, 0));

        MatOfPoint largestContour = allContours.stream().max((c1, c2) -> {
            double area1 = Imgproc.contourArea(c1);
            double area2 = Imgproc.contourArea(c2);
            if (area1 < area2) {
                return -1;
            } else if (area1 > area2) {
                return 1;
            }
            return 0;
        }).get();

        Mat debugCanvas = new Mat(image.size(), CvType.CV_8UC3);
        Imgproc.drawContours(debugCanvas, Arrays.asList(largestContour), -1, new Scalar(255, 255, 255), 3);
        Imgproc.drawMarker(debugCanvas, getCenterOfMass(largestContour),
                new Scalar(255, 255, 255));
        Rect boundingBox = Imgproc.boundingRect(largestContour);
        Imgproc.rectangle(debugCanvas, boundingBox.br(), boundingBox.tl(), new Scalar(0, 255, 0), 3);
        System.out.printf("Bounding box area is: %f and contour area is: %f", boundingBox.area(), Imgproc.contourArea(
                largestContour));
        Imgcodecs.imwrite("output.png", debugCanvas);

    }

    private static Point getCenterOfMass(MatOfPoint contour) {
        Moments moments = Imgproc.moments(contour);
        return new Point(moments.m10 / moments.m00, moments.m01 / moments.m00);
    }
}

Input: (full image here) enter image description here Output: enter image description here

STDOUT:

Bounding box area is: 6460729,000000 and contour area is: 5963212,000000

The centroid is drawn close to the upper left corner, outside the contour.


Solution

  • As mentioned in the comment discussion, it looks like this issue you're having was reported specifically in the Java implementation on OpenCV's GitHub. It was eventually solved with this simple pull request. There were some unnecessary int castings.

    Possible solutions then:

    1. Upgrading OpenCV should fix you up.

    2. You can edit your library files with the fix (it's simply removing an (int) cast on a few lines).

    3. Define your own function to calculate the centroids.


    If you're bored and want to figure out 3, it's actually not a difficult calculation:

    Centroids of a contour are usually calculated from image moments. As shown on that page, a moment M_ij can be defined on images as:

    M_ij = sum_x sum_y (x^i * y^j * I(x, y))
    

    and the centroid of a binary shape is

    (x_c, y_c) = (M_10/M_00, M_01/M_00)
    

    Note that M_00 = sum_x sum_y (I(x, y)) which, in a binary 0 and 1 image, is just the number of white pixels. If your contourArea is working as you stated in the comments, you can simply use that as M_00. Then note also that M_10 is just the sum of the x values corresponding to white pixels and M_01 with the y values. These can be easily calculated, and you can define your own centroid function with the contours.