skip to content

What does the hierarchy array from cv2.findContours contain, and when do you need RETR_TREE?

level: seniorimportance: should knowfreq 42%

answer

  1. four numbers per contour
  2. they are indices, not counts
  3. -1 means nothing there
  4. siblings, child, parent
  5. outer area includes the holes

basics

~20 s

Hierarchy has shape (1, N, 4); each row is [Next, Previous, First_Child, Parent] holding indices into the contours list, with -1 meaning none. RETR_TREE is needed when nesting matters - counting holes, or telling an outer boundary from the shapes inside it.

solid answer

~50 s

`cv2.findContours` returns a hierarchy array of shape `(1, N, 4)`, one row per contour. The four entries are indices into the `contours` list: `[Next, Previous, First_Child, Parent]`, where `Next` and `Previous` are siblings at the same nesting level, and `-1` means "none." Which fields are populated depends on the retrieval mode. `cv2.RETR_EXTERNAL` returns only outermost contours, so holes never appear at all. `cv2.RETR_LIST` returns everything with no relationships recorded - all four fields are sibling links or -1. `cv2.RETR_CCOMP` gives exactly two levels: outer boundaries and their holes. `cv2.RETR_TREE` reconstructs the full nesting tree, which you need when depth is meaningful - counting holes per part, ignoring the page border around a document, or distinguishing a character's outline from its counters. A practical consequence: `cv2.contourArea` on an outer contour includes its holes, so subtract the children's areas.

code

python · 15 lines
python
import cv2
import numpy as np

img = np.zeros((200, 200), np.uint8)
cv2.circle(img, (100, 100), 60, 255, -1)
cv2.circle(img, (100, 100), 25, 0, -1)      # a hole -> a washer

contours, hierarchy = cv2.findContours(
    img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
print(hierarchy.shape)                      # (1, N, 4)
for i, h in enumerate(hierarchy[0]):
    kind = "outer" if h[3] == -1 else "hole"
    print(i, kind, "area", cv2.contourArea(contours[i]))
# the outer area includes the hole; subtract the child's area for material

go deeper

for a junior

Recognise that findContours returns a hierarchy alongside the contours and that RETR_EXTERNAL is the mode that gives you only the outer shapes. Knowing you can ignore hierarchy for simple counting is acceptable here.

for a middle

Recite the four fields as [Next, Previous, First_Child, Parent], explain that -1 means absent and that they index the contours list, and distinguish the four retrieval modes by what they record.

for a senior

Show the measurement consequence: contourArea on an outer contour includes its holes, so material area needs the children subtracted. Also justify picking the weakest retrieval mode that answers the question, for cost.

for a principal

Own the robustness argument for structural filtering over magic-number filtering - dropping a page border by depth rather than by area threshold - and where classical nesting analysis stops paying off against a learned detector.

## Why a hierarchy exists at all Contours nest. A washer has an outer circle and an inner circle; the letter B has an outline and two counters; a scanned page has a border containing everything else. A flat list of curves cannot tell you which curve encloses which, and "how many holes does this part have" is a real inspection question. The hierarchy array is OpenCV's answer. ## The layout `hierarchy` is a NumPy array of shape `(1, N, 4)` for N contours. The leading axis of size 1 is a binding artefact, which is why every real example indexes `hierarchy[0]` first. Row `i` describes `contours[i]` with four **indices into the contours list**: - index 0 - **Next**: the next contour at the same level, or -1 - index 1 - **Previous**: the previous contour at the same level, or -1 - index 2 - **First_Child**: the first contour nested inside this one, or -1 - index 3 - **Parent**: the contour this one is nested inside, or -1 So `hierarchy[0][i][3] == -1` means contour `i` is top-level, and `hierarchy[0][i][2] != -1` means it encloses something. If no contours were found at all, `hierarchy` is `None`, not an empty array - guard before indexing. ## What each retrieval mode records `cv2.RETR_EXTERNAL` keeps only the outermost contours and discards everything nested inside. Child and parent fields are all -1. This is the correct default for counting solid parts on a belt, and it is also the fastest, since interior boundaries are never traced. `cv2.RETR_LIST` returns every contour, holes included, but records no nesting: only sibling links. Useful when you want all boundaries and genuinely do not care about containment - and dangerous when you *do* care, because hole boundaries silently appear as extra "objects" in your count. `cv2.RETR_CCOMP` organizes results into exactly two levels: the external boundaries of connected components at the top, and the boundaries of their holes at the second. It is the cheap right answer for "parts with holes," but a shape nested inside a hole is lifted back to the top level, so it lies about deeper nesting. `cv2.RETR_TREE` builds the complete nesting tree with correct parent, child and sibling links at every depth. Use it when depth itself carries meaning. ## When depth genuinely matters Three recurring cases. **Hole counting**: a machined part is graded by how many bores it has, which is just the number of children of its outer contour. **Border rejection**: a scanned document often yields a page-boundary contour enclosing everything; with a tree you drop the depth-0 contour and process its children, instead of hard-coding an area threshold that breaks on the next scanner. **Nested content**: a rectangle drawn around a group of symbols, or a fiducial marker whose validity is defined by its nesting pattern - a marker specification can literally be "a black square containing a white square containing a black square," which is a depth query and nothing else. ## Traversal Because the fields are plain indices, iteration is ordinary Python. Top-level contours are those with `hierarchy[0][i][3] == -1`. To walk one contour's children, take `First_Child` and then follow `Next` until it hits -1; each of those may have children of its own. Depth is computed by climbing `Parent` links until you reach -1, and a common idiom is to count that depth per contour and then select by parity - even depth is solid material, odd depth is a hole, alternating all the way down. ## The area trap This is the detail that separates people who have shipped a measurement system from people who have read the tutorial. `cv2.contourArea` computes the area of the **polygon** the contour traces, using Green's theorem. For an outer contour, that polygon includes the region occupied by its holes. A washer's `contourArea` is the area of the full disc, not the annulus. If you are measuring material, you must subtract the areas of the child contours - which is only possible if you retrieved a hierarchy in the first place. The same caution applies to `cv2.moments`: `m00` on an outer contour equals that same hole-inclusive polygon area, and the centroid derived from it is the centroid of the filled shape, not of the material. When you need true foreground pixel counts instead, count labelled pixels rather than integrating a polygon. A related subtlety: contour area from the polygon and a raw pixel count of the same blob differ slightly even with no holes, because the polygon runs through pixel centres along the boundary. For small blobs that discrepancy is a few percent and can matter in pass/fail thresholds. ## Cost and choosing Tracing interior boundaries is not free, and neither is building the tree. On high-resolution frames with many textured blobs, `RETR_EXTERNAL` can be meaningfully faster than `RETR_TREE` simply because it traces fewer curves. The rule is ordinary engineering: request the weakest mode that answers your question. If you only count outer objects, take `RETR_EXTERNAL`; if you need parts and their holes, `RETR_CCOMP`; only reach for `RETR_TREE` when arbitrary depth is part of the specification.

  • With RETR_TREE, how do you count the holes in a single part?
    Find the part's index i, take First_Child = hierarchy[0][i][2], and if it is not -1 walk the Next links from there, counting entries until you reach -1. That sibling chain is exactly the set of contours directly enclosed by the part, which is its hole count at depth one.
  • Why can cv2.contourArea overstate a washer's area, and how do you fix it?
    contourArea integrates the polygon the contour traces, and an outer contour's polygon encloses its holes as well as its material. For a washer that yields the full disc. Retrieve the hierarchy, find the outer contour's children, and subtract their contourArea values to get the annulus.
  • When is RETR_CCOMP not enough and you must use RETR_TREE?
    When nesting goes deeper than two levels and depth carries meaning. RETR_CCOMP flattens everything into outer-boundary and hole levels, so a shape sitting inside a hole is promoted back to the top level rather than being recorded as a grandchild. Fiducial markers and text inside drawn boxes are the usual cases.
  • A scanned page yields one enormous contour enclosing every character. How do you drop it cleanly?
    Retrieve with RETR_TREE, discard the contours whose Parent is -1 when they are the page border, and process their children instead. That is structural and survives a change of scanner or resolution, unlike filtering by an absolute area threshold tuned on one machine.

saying these in an interview costs you the question

  • Reads hierarchy entries as counts rather than indices
  • Thinks RETR_EXTERNAL still reports holes
  • Believes RETR_LIST records parent-child links
  • Treats RETR_CCOMP as arbitrary-depth nesting
  • Reports contourArea of a holed part as its material area

context