1

I am trying to compute the area and the center coordinates of objects in a binary mask using opencv. However, I noticed that in some cases I get the wrong result. For example, if I get the contours like this:

import numpy as np
import cv2

binary_mask = np.array([
    [0, 0, 1, 1, 0, 0],
    [0, 0, 1, 1, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [1, 1, 1, 1, 1, 1]])

contours, _ = cv2.findContours(
    binary_mask.astype(np.uint8),
    cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE)

>>> contours
(array([[[0, 3]],

        [[5, 3]]], dtype=int32),
 array([[[2, 0]],

        [[2, 1]],

        [[3, 1]],

        [[3, 0]]], dtype=int32))

Then I get ZeroDivisionError for the center calculation:

def get_centroid_from_contour(contour):
    M = cv2.moments(contour)
    cX = int(M["m10"] / M["m00"])
    cY = int(M["m01"] / M["m00"])
    return (cX, cY)

>>> get_centroid_from_contour(contour)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in get_centroid_from_contour_
ZeroDivisionError: float division by zero

This I think is related to the fact that somehow opencv thinks the 2x2 squared object has zero area:

>>> cv2.contourArea(contours[0])
0.0

It seems something related to "open objects". Indeed the first contour only contains two points that do not close the polygon, but I have no idea how to fix this. I also tried closing the contour as suggested here but it doesn't work.

Christoph Rackwitz
  • 11,317
  • 4
  • 27
  • 36
Luca Clissa
  • 810
  • 2
  • 7
  • 27
  • 1
    Does this answer your question? [OpenCV cv2.moments returns all moments to zero](https://stackoverflow.com/questions/62392240/opencv-cv2-moments-returns-all-moments-to-zero) – Cris Luengo Jun 06 '23 at 17:05
  • Not really, at least not at the moment I posted my question. The proposed solutions [1] didn't work in my case. After I posted I got an answer that was then added to the post you mentioned [2]. Anyway according to @ChristophRackwitz answer's seem the root cause is the same, so I understand this may be considered as a duplicate to an extent. I was aware of the original question and I also cited it. In my view my question is different as I am interested in areas/centers and not moments. [1] https://stackoverflow.com/a/72249832/7678074 [2] https://stackoverflow.com/a/76416625/7678074 – Luca Clissa Jun 07 '23 at 08:18

1 Answers1

2

Looks like a bug in the implementation of moments(). It appears to not consider how contours are defined in OpenCV.

The issue isn't with your 2x2 connected component, but with the 6x1 one. That one is returned first. Its contour consists of two points.

For the contour of a 1-pixel line, moments() should have still given a non-zero m00 because that is how contours work in OpenCV. Since you say you got a ZeroDivisionError, it must have been zero. That is incorrect for OpenCV's notion of a contour, but correct for every other notion of a polygon/contour.

In OpenCV, a contour describes the polygon that needs to be drawn to reproduce the picture. The line goes ON the outer edge pixels that are still inside the connected component. That is why the contour of your line CC becomes a 2-corner polygon of, strictly speaking, zero area.

OpenCV could have defined contours to be zero-width lines that circumscribe the outer pixels of a connected component. That would have required:

  1. the drawing calls in OpenCV to not be as broken and neglected as they are (I'm being blunt but it's true)
  2. the conventions for a contour to allow non-integer coordinates

Feel free to submit an issue on OpenCV's github page. The moments() implementation needs a fix.

Addendum: the fix should probably be another flag that distinguishes "contours" from proper polygons, so the mathematically correct behavior isn't lost, but instead made optional. Everyone calls that function on contours, so that should be the default.

Christoph Rackwitz
  • 11,317
  • 4
  • 27
  • 36
  • Considering this answer is nearly identical to [this other one](https://stackoverflow.com/a/76416625/7328782) you just posted, I think this question should be closed as duplicate of the other. The issues presented are the same, duplicating answers is, IMO, a bad practice. – Cris Luengo Jun 06 '23 at 17:06
  • 1
    fair point. I'll make sure the answer on the older question isn't missing anything important from this one, and vote to close – Christoph Rackwitz Jun 06 '23 at 17:42
  • Thanks for the comments! @CrisLuengo the difference I see is that I'm interested in the computation of the area and the center of the objects, not just the moments. However the root causes seem related apparently, so it is fine for me to close this is you think it is appropriate. Anyway, would you suggest to use scikit-image instead for this kind of image processing? – Luca Clissa Jun 07 '23 at 08:09
  • 2
    @LucaClissa the area is the 0th order moment and the center are the 1st order normalized moments. —— If you ask me what library I recommend, I’ll always say [DIPlib](https://diplib.org/), but I *might* be biased… scikit-image is OK too, usually much slower than DIPlib. People know that [I am not of a fan of OpenCV](https://www.crisluengo.net/archives/1140/). – Cris Luengo Jun 07 '23 at 12:33
  • @CrisLuengo I am sorry I didn't know about the meaning of moments, well now I fully get your comment about the overlapping with the other question. Anyway thanks for sharing you insights about the libraries. I tried with scikit-image and it works in this case (however it gets regions and contours in reverse order wrt opencv)! I'll give a try to DIPlib too :) – Luca Clissa Jun 07 '23 at 13:33
  • LOVELY writeup @CrisLuengo and bookmarked. the "maturity" of OpenCV doesn't mean it's getting better, just more *ripe* in some places . my experience with merely getting some opinions out of the core devs makes me less than optimistic about being allowed to fix existing stuff. replacement might be feasible. OpenCV could really use a "machine vision" module that just ignores imgproc and takes a lot of hints from commercial MV packages (and diplib probably, but I haven't touched that so far). – Christoph Rackwitz Jun 07 '23 at 16:58
  • @ChristophRackwitz Thanks. *Ripe* is a nice word to describe it. – Cris Luengo Jun 07 '23 at 17:29