0

I have successfully extracted contours from a mask image, using OpenCV:

image = cv2.imread(file)

lower = np.array([240, 240, 240])
upper = np.array([255, 255, 255])
shape_mask = cv2.inRange(image, lower, upper)

contours, hierarchy = cv2.findContours(shape_mask.copy(), cv2.RETR_CCOMP,
                                       cv2.CHAIN_APPROX_SIMPLE)

Now how do I go on to map this contours into a list of polygons (with holes) with shapely to export them as a SHP file?

Partial answer is given here: How to convert NumPy arrays obtained from cv2.findContours to Shapely polygons?

However, this ignores the case of having holes inside the polygons. How would I go on to get all polygons?

Georgy
  • 12,464
  • 7
  • 65
  • 73
Rauni Lillemets
  • 2,299
  • 1
  • 26
  • 39
  • What should happen with multiple nested contours? We can't, for example, represent three nested contours as a Shapely Polygon with a hole. – Georgy Apr 01 '20 at 15:53
  • I thought about this also. The solution I found on GitHub (see below) deals with this by creating a MultiPolygon which contains 1st contour as a Polygon with hole (2nd contour), 3rd contour also as a Polygon with hole, etc. I tested it and it works quite well (opens nicely in QGis). The key for this is the "cv2.RETR_CCOMP" flag in the function cv2.findContours. – Rauni Lillemets Apr 02 '20 at 14:18

1 Answers1

1

Found the answer: https://michhar.github.io/masks_to_polygons_and_back/

Helper to create MultiPolygons from a masked image as numpy array:

def mask_to_polygons(mask, epsilon=10., min_area=10.):
    """Convert a mask ndarray (binarized image) to Multipolygons"""
    # first, find contours with cv2: it's much faster than shapely
    image, contours, hierarchy = cv2.findContours(mask,
                                  cv2.RETR_CCOMP,
                                  cv2.CHAIN_APPROX_NONE)
    if not contours:
        return MultiPolygon()
    # now messy stuff to associate parent and child contours
    cnt_children = defaultdict(list)
    child_contours = set()
    assert hierarchy.shape[0] == 1
    # http://docs.opencv.org/3.1.0/d9/d8b/tutorial_py_contours_hierarchy.html
    for idx, (_, _, _, parent_idx) in enumerate(hierarchy[0]):
        if parent_idx != -1:
            child_contours.add(idx)
            cnt_children[parent_idx].append(contours[idx])
    # create actual polygons filtering by area (removes artifacts)
    all_polygons = []
    for idx, cnt in enumerate(contours):
        if idx not in child_contours and cv2.contourArea(cnt) >= min_area:
            assert cnt.shape[1] == 1
            poly = Polygon(
                shell=cnt[:, 0, :],
                holes=[c[:, 0, :] for c in cnt_children.get(idx, [])
                       if cv2.contourArea(c) >= min_area])
            all_polygons.append(poly)
    all_polygons = MultiPolygon(all_polygons)

    return all_polygons

Credit for creating these helper functions:

the original source of these helpers was a Kaggle post by Konstantin Lopuhin here - you'll need to be logged into Kaggle to see it

Rauni Lillemets
  • 2,299
  • 1
  • 26
  • 39
  • This works great to create shapes that are usable with some things, however, the MultiPolygon is always shapely / GEOS "invalid" for the contours I pass and so this breaks most tools. I've been fiddling with this code to ensure valid polygons are created from the contours but I'm not having much luck.. any ideas? – Marty Jun 08 '23 at 19:05