findContours around pixels

I’m trying to use OpenCV for the first time to try and find the contours of several regions in a labelmap. Every region has its own numeric integer value. However it seems the contours are drawn from the pixel-centers instead of around the pixels. Is there a way to get it to draw contours around the pixels?
Here the same problem is encountered three years ago.
Here is some demo-code and an image to show what I mean:

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

lblmap = np.array([[0, 1, 1, 0, 0],
                   [0, 1, 1, 2, 0],
                   [0, 0, 1, 0, 2],
                   [3, 3, 3, 2, 2],
                   [0, 3, 0, 2, 0]])
ax.imshow(lblmap)
mask = np.zeros(lblmap.shape, dtype=np.uint8)
nROI = np.max(lblmap)
areas = []
for roi_id in range(1, 1 + nROI):
    cv.compare(lblmap, int(roi_id), cv.CMP_EQ, mask)
    contour, _ = cv.findContours(mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    x = np.hstack((contour[0][:, 0, 0], contour[0][:, 0, 0][0]))
    y = np.hstack((contour[0][:, 0, 1], contour[0][:, 0, 1][0]))
    areas.append({'shape': (x, y), 'color': (1, 0, 0)})

fig, ax = plt.subplots(figsize=(45, 30))
for area in areas:
    ax.plot(area['shape'][0], area['shape'][1], color=area['color'])

1 Like

please explain, what that means, its unclear


Hi Berak,
Thanks for the fast response. The picture above shows the behavior of OpenCV in red and what I would like in green. As you can see the green line goes over the edge of the pixel, where the edge of the region is. The red line is inside the region and goes from pixel-center to pixel-center.

i’m not even sure, if pixels have a “center” here, but for sure:
there is no “outline” of a pixel

what is the problem behind your question ?

I see what you mean. no, I believe there is no way to make OpenCV give you contours that sit on the edges of pixels.

you could approximate the solution by nearest-neighbor-resizing… I think you can do that for a low factor (2 or so) and then “snap” the solution and remove a few (literal) corner cases.

or reimplement the algorithm.

I think OpenCV would benefit from a findContours that returns contours sitting on pixel edges (optionally).

Yes, I understand a pixel is “not a little square” right? My problem is I have a labelmap with about 10k of these regions with area’s ranging between 50-100 pixels. I want to plot their outline, but if I use cv.findContours the outlines leave a gap of about a pixel between each region. I would like them to touch, since in the original labelmap they also touch.

Thank you! Good to know that OpenCV doesn’t have that option, then I can stop searching.
A findContours that returns countours sitting on pixel edges would also work for me indeed. Like the red line in the picture below.
image
Together with a colleague we are working on an algorithm that will create the red line from the black line by expanding each vertex to 4 corners, and connecting a subset based on the direction of the edges.

We changed the example a bit to have different contours in the image. This algorithm will expand the black lines found by openCV to the red lines that pass the pixel edges:

"""
Demonstration of enhance_contour. Algorithm to expand the contours found by cv2.findContours
"""
import numpy as np
from matplotlib import pyplot as plt
import cv2 as cv


def points(v, dv, dw, P):
    """
    Expand the vertex v to points at distance P
    :param P: expanding points as np.array (4,2)
    :param v: single vertex as np.array (2,)
    :param dv: origin as np.array (2,)
    :param dw: destiation as np.array (2,)
    :return:
    """
    j = int(np.arctan2(*dv) // (np.pi / 2))
    k = -int(-np.arctan2(*dw) // (np.pi / 2))
    return [v + P[(i + j) % 4] for i in range((k - j) % 4 + 1)]


def enhance_contour(U, P):
    """
    Expand a contour found by cv2.findContours with distances P.
    :param P: expanding points as np.array (4,2)
    :param U: single, not-closed contour with vertices as as np.array: (n,2)
    :return:
    """
    if type(U[0]) is np.intc:  # single coordinate
        return U + P
    dU = np.diff(np.vstack((U[-1], U)).T).T
    dV = np.diff(np.vstack((U, U[0])).T).T
    return np.array([p for u, du, dv in zip(U, dU, dV) for p in points(u, du, dv, P)])


# generate image
im = np.array(((0, 1, 1, 0, 4),
               (0, 1, 1, 2, 0),
               (2, 2, 1, 0, 2),
               (3, 3, 3, 2, 2),
               (0, 3, 0, 2, 0)), 'uint8')

P = np.array(((-1, -1), (-1, 1), (1, 1), (1, -1))) / 2  # contour of the pixels
P = np.array(((-1, 0), (0, 1), (1, 0), (0, -1))) / 2  # contour through pixel edes
U = [np.squeeze(c) for i in range(1, 1+np.max(im)) for c in cv.findContours((im == i).astype('uint8'), cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[0]]
U2 = [enhance_contour(u, P) for u in U]

# close contours and display
UC = [u if type(u[0]) is np.intc else np.vstack((u, u[0])) for u in U]
UC2 = [np.vstack((u, u[0])) for u in U2]
plt.imshow(im)
for u in UC:
    plt.plot(*u.T, '.-k')
for u in UC2:
    plt.plot(*u.T, '.-r')
plt.show()

Figure_1

1 Like

Changing
P = np.array(((-1, 0), (0, 1), (1, 0), (0, -1))) / 2
into
P = np.array(((-1, -1), (-1, 1), (1, 1), (1, -1))) / 2
results in a contour around the pixels:
Figure_1

1 Like