Skip to content

feat(imgproc): add find_contours with hierarchy modes#797

Open
mathurojus wants to merge 2 commits intokornia:mainfrom
mathurojus:feat/find-contours
Open

feat(imgproc): add find_contours with hierarchy modes#797
mathurojus wants to merge 2 commits intokornia:mainfrom
mathurojus:feat/find-contours

Conversation

@mathurojus
Copy link

Implemented find_contours for kornia-imgproc with OpenCV-like behavior for binary images.

What’s included

  • New contour API in imgproc:
    • find_contours(...)
    • compatibility alias find_countours(...)
  • Retrieval modes:
    • External
    • List
    • CComp
    • Tree
  • Approximation modes:
    • None
    • Simple
  • Hierarchy output in OpenCV format: [next, prev, child, parent]

Tests added

  • single rectangle contour
  • contour with hole / hierarchy
  • deeper nesting for tree mode
  • comparison of None vs Simple approximation

Reference issue event:
#169 (comment)

Closes #169

@qodo-code-review
Copy link
Contributor

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Add find_contours with OpenCV-compatible hierarchy modes

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implement find_contours function with OpenCV-compatible retrieval modes
  - External, List, CComp, Tree modes for different hierarchy extraction strategies
  - Contour approximation modes (None, Simple) for point compression
• Add comprehensive contour tracing and component labeling algorithms
  - 8-connected component labeling with flood-fill
  - Boundary tracing using directional search
  - Hierarchy construction supporting nested contours
• Include backward-compatible alias find_countours for misspelled API
• Add four test cases covering rectangles, holes, nesting, and approximation modes
Diagram
flowchart LR
  BinaryImage["Binary Image<br/>0=background, non-zero=foreground"]
  LabelFG["Label Foreground<br/>Components"]
  TraceBoundary["Trace Component<br/>Boundaries"]
  Approximate["Approximate Contour<br/>Points"]
  LabelBG["Label Background<br/>Components"]
  BuildHierarchy["Build Hierarchy<br/>Based on Mode"]
  Result["FindContoursResult<br/>contours + hierarchy"]
  
  BinaryImage --> LabelFG
  BinaryImage --> LabelBG
  LabelFG --> TraceBoundary
  LabelBG --> TraceBoundary
  TraceBoundary --> Approximate
  Approximate --> BuildHierarchy
  BuildHierarchy --> Result
Loading

Grey Divider

File Changes

1. crates/kornia-imgproc/src/contours.rs ✨ Enhancement +703/-0

Complete contour extraction with hierarchy support

• New module implementing find_contours with 4 retrieval modes (External, List, CComp, Tree)
• Contour approximation modes (None, Simple) for point compression
• Component labeling using 8-connected flood-fill algorithm
• Boundary tracing with directional search following Moore's algorithm
• Hierarchy construction supporting parent-child relationships for nested contours
• Four comprehensive test cases validating rectangles, holes, deep nesting, and approximation

crates/kornia-imgproc/src/contours.rs


2. crates/kornia-imgproc/src/lib.rs ✨ Enhancement +3/-0

Export contours module

• Export new contours module in public API
• Add module documentation comment

crates/kornia-imgproc/src/lib.rs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 8, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (4) 📎 Requirement gaps (0)

Grey Divider


Action required

1. README missing contours module 📘 Rule violation ✧ Quality
Description
A new public contours module is exported, but the crate README’s module list is not updated to
mention it. This makes the new API harder to discover and violates the requirement to update
user-facing docs for public API changes.
Code

crates/kornia-imgproc/src/lib.rs[R15-16]

+/// contour extraction module.
+pub mod contours;
Evidence
PR Compliance ID 12 requires updating relevant README/module documentation when adding public APIs.
The PR exports pub mod contours; but the kornia-imgproc README module list does not include
contours.

AGENTS.md
crates/kornia-imgproc/src/lib.rs[15-16]
crates/kornia-imgproc/README.md[83-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new public module `kornia_imgproc::contours` was added, but the crate README does not list or describe it, reducing discoverability.

## Issue Context
The public API surface was extended by exporting `pub mod contours;`. The README has a `Modules` section enumerating available modules, but it currently omits `contours`.

## Fix Focus Areas
- crates/kornia-imgproc/src/lib.rs[15-16]
- crates/kornia-imgproc/README.md[83-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Magic number 16 in tracing 📘 Rule violation ⛯ Reliability
Description
The boundary tracing safety limit uses a hard-coded multiplier 16, which is an algorithm parameter
and is not self-documenting. This makes the algorithm harder to tune and reason about.
Code

crates/kornia-imgproc/src/contours.rs[355]

+    let safety_limit = width * height * 16;
Evidence
PR Compliance ID 13 requires replacing hard-coded numeric algorithm parameters with named
constants/config. The new implementation uses width * height * 16 as a safety limit without a
named constant explaining the choice.

crates/kornia-imgproc/src/contours.rs[354-356]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The contour tracing loop uses a hard-coded safety multiplier `16` (`width * height * 16`) to prevent infinite loops. This is an algorithm parameter and should be a named constant to clarify intent and allow future tuning.

## Issue Context
The safety limit affects algorithm termination behavior and should be self-documenting.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[354-365]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Panic on empty labels 🐞 Bug ⛯ Reliability
Description
fg_count/bg_count are computed as max_label + 1 without handling the all--1 case, which
overflows in debug/test builds and panics for images with no foreground (or no background). This
breaks expected behavior where such inputs should return zero contours.
Code

crates/kornia-imgproc/src/contours.rs[R114-120]

+    let fg_mask: Vec<bool> = src.as_slice().iter().map(|&v| v != 0).collect();
+    let fg_labels = label_components(&fg_mask, width, height);
+    let fg_count = fg_labels
+        .iter()
+        .copied()
+        .max()
+        .map_or(0usize, |m| (m as usize) + 1);
Evidence
label_components initializes all labels to -1; when there are no components, max() returns
-1, and (m as usize) + 1 overflows in debug builds, causing a panic instead of producing 0
components.

crates/kornia-imgproc/src/contours.rs[114-120]
crates/kornia-imgproc/src/contours.rs[151-156]
crates/kornia-imgproc/src/contours.rs[272-274]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`find_contours` computes `fg_count`/`bg_count` as `(max_label as usize) + 1`. When the image has no components, labels remain `-1` and `max()` returns `-1`; casting to `usize` and adding `1` overflows in debug builds, causing a panic.

## Issue Context
`label_components` initializes all labels to `-1` and only assigns non-negative labels when it finds a `true` pixel.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[114-120]
- crates/kornia-imgproc/src/contours.rs[151-156]
- crates/kornia-imgproc/src/contours.rs[272-304]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Tree parent selection wrong 🐞 Bug ✓ Correctness
Description
In RetrievalMode::Tree, a foreground contour’s parent is chosen as the first adjacent non-border
background component found by a raster scan, which is order-dependent and can pick an internal hole
instead of the outside background. For border-touching foreground components with holes (border
fully covered by foreground), this can even create a parent cycle (foreground ↔ hole).
Code

crates/kornia-imgproc/src/contours.rs[R212-233]

+        RetrievalMode::Tree => {
+            let mut parent = vec![None; entries.len()];
+            for (idx, e) in entries.iter().enumerate() {
+                match e.kind {
+                    ContourKind::Hole => {
+                        parent[idx] = e.parent_hint;
+                    }
+                    ContourKind::Foreground => {
+                        if let Some(bg_comp_id) = find_adjacent_background_component(
+                            &fg_labels,
+                            &bg_labels,
+                            width,
+                            height,
+                            e.component_id,
+                        ) {
+                            let bg_id = bg_comp_id as usize;
+                            if bg_id < bg_touches_border.len()
+                                && !bg_touches_border[bg_id]
+                                && bg_id < bg_comp_to_entry.len()
+                            {
+                                parent[idx] = bg_comp_to_entry[bg_id];
+                            }
Evidence
Holes are assigned parent_hint via the first adjacent foreground label found. In Tree mode,
foreground contours are assigned a parent via the first adjacent background label found, provided
that background component doesn’t touch the border. If the first adjacent background label is an
internal hole (non-border), the foreground becomes a child of its own hole while that hole remains a
child of the foreground, producing an invalid hierarchy (and potential cycles).

crates/kornia-imgproc/src/contours.rs[173-188]
crates/kornia-imgproc/src/contours.rs[212-234]
crates/kornia-imgproc/src/contours.rs[483-509]
crates/kornia-imgproc/src/contours.rs[534-541]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RetrievalMode::Tree` assigns a foreground contour’s parent to the first adjacent non-border background component returned by a raster scan. This is order-dependent and can select an internal hole background component, producing incorrect hierarchy and (for border-touching foregrounds with holes) can create a foreground↔hole parent cycle.

## Issue Context
- Holes set `parent_hint` to an adjacent foreground component.
- Foregrounds set `parent` to an adjacent non-border background component, but the adjacency search returns the first found label, not necessarily the outside component.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[212-238]
- crates/kornia-imgproc/src/contours.rs[173-188]
- crates/kornia-imgproc/src/contours.rs[483-509]
- crates/kornia-imgproc/src/contours.rs[528-571]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. find_boundary_start not rustfmt 📘 Rule violation ⛯ Reliability
Description
The function signature for find_boundary_start is on a single long line and appears not to be
rustfmt-formatted. This risks failing the project’s formatting/clippy gates with warnings denied.
Code

crates/kornia-imgproc/src/contours.rs[306]

+fn find_boundary_start(labels: &[i32], width: usize, height: usize, comp_id: i32) -> Option<usize> {
Evidence
PR Compliance ID 2 requires rustfmt formatting and clippy cleanliness. The added signature
formatting strongly suggests the file was not run through rustfmt and may trip formatting/Clippy
checks in CI.

AGENTS.md
crates/kornia-imgproc/src/contours.rs[306-306]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new code appears not to be rustfmt-formatted, which can cause CI failures when formatting/clippy warnings are denied.

## Issue Context
At least one newly-added function signature is on a single long line that rustfmt would typically reflow.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[306-306]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. find_countours lacks doctest example 📘 Rule violation ✧ Quality
Description
The public find_countours API has no # Example doctest, reducing usability and risking
documentation drift. Even though it’s an alias, it is a user-facing entry point.
Code

crates/kornia-imgproc/src/contours.rs[R249-270]

+/// Backward-compatible alias for a misspelled API name.
+///
+/// # Arguments
+///
+/// * `src` - Binary single-channel image (`0` background, non-zero foreground).
+/// * `mode` - Contour retrieval mode.
+/// * `method` - Contour approximation method.
+///
+/// # Returns
+///
+/// A [`FindContoursResult`] containing contour points and hierarchy.
+///
+/// # Errors
+///
+/// Propagates errors from [`find_contours`].
+pub fn find_countours<A: ImageAllocator>(
+    src: &Image<u8, 1, A>,
+    mode: RetrievalMode,
+    method: ContourApproximationMode,
+) -> Result<FindContoursResult, ImageError> {
+    find_contours(src, mode, method)
+}
Evidence
PR Compliance ID 7 requires non-trivial public APIs to include a compilable doctest example.
find_countours is public and documented but does not include an example section.

AGENTS.md
crates/kornia-imgproc/src/contours.rs[249-270]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The public alias `find_countours` lacks a `# Example` doctest section, which reduces discoverability and can let docs drift.

## Issue Context
`find_countours` is a backward-compatible alias and still a user-facing API.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[249-270]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Repeated full-image scans 🐞 Bug ➹ Performance
Description
Boundary start and adjacency are found by scanning the entire image once per component, causing
worst-case O(pixels × components) runtime and large slowdowns on big images with many regions. This
is especially costly in modes that also label and process background components.
Code

crates/kornia-imgproc/src/contours.rs[R125-180]

+    for comp_id in 0..fg_count {
+        let start = find_boundary_start(&fg_labels, width, height, comp_id as i32);
+        if let Some(start_idx) = start {
+            let contour =
+                trace_component_boundary(&fg_labels, width, height, comp_id as i32, start_idx);
+            let contour = approximate(contour, method);
+            let entry_idx = entries.len();
+            entries.push(ContourEntry {
+                points: contour,
+                kind: ContourKind::Foreground,
+                component_id: comp_id as i32,
+                parent_hint: None,
+            });
+            fg_comp_to_entry[comp_id] = Some(entry_idx);
+        }
+    }
+
+    let mut bg_labels = Vec::<i32>::new();
+    let mut bg_touches_border = Vec::<bool>::new();
+    let mut bg_comp_to_entry: Vec<Option<usize>> = Vec::new();
+
+    if matches!(
+        mode,
+        RetrievalMode::List | RetrievalMode::CComp | RetrievalMode::Tree
+    ) {
+        let bg_mask: Vec<bool> = fg_mask.iter().map(|&is_fg| !is_fg).collect();
+        bg_labels = label_components(&bg_mask, width, height);
+        let bg_count = bg_labels
+            .iter()
+            .copied()
+            .max()
+            .map_or(0usize, |m| (m as usize) + 1);
+        bg_touches_border = (0..bg_count)
+            .map(|id| component_touches_border(&bg_labels, width, height, id as i32))
+            .collect();
+        bg_comp_to_entry = vec![None; bg_count];
+
+        for comp_id in 0..bg_count {
+            if bg_touches_border[comp_id] {
+                continue;
+            }
+
+            let start = find_boundary_start(&bg_labels, width, height, comp_id as i32);
+            if let Some(start_idx) = start {
+                let contour =
+                    trace_component_boundary(&bg_labels, width, height, comp_id as i32, start_idx);
+                let contour = approximate(contour, method);
+
+                let parent_hint = find_adjacent_foreground_component(
+                    &bg_labels,
+                    &fg_labels,
+                    width,
+                    height,
+                    comp_id as i32,
+                )
+                .and_then(|fg_id| fg_comp_to_entry.get(fg_id as usize).and_then(|x| *x));
Evidence
find_boundary_start scans the whole image for a given component id and is called in loops over all
foreground and background components. Adjacency helpers similarly scan the full image per component.

crates/kornia-imgproc/src/contours.rs[125-130]
crates/kornia-imgproc/src/contours.rs[162-170]
crates/kornia-imgproc/src/contours.rs[306-316]
crates/kornia-imgproc/src/contours.rs[454-480]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several helpers (`find_boundary_start`, `find_adjacent_foreground_component`, `find_adjacent_background_component`) do full-image scans and are called once per component, creating worst-case O(pixels × components) behavior.

## Issue Context
This can become very slow on large images with many connected components, especially when also processing background components for hierarchy modes.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[125-140]
- crates/kornia-imgproc/src/contours.rs[162-190]
- crates/kornia-imgproc/src/contours.rs[306-316]
- crates/kornia-imgproc/src/contours.rs[454-510]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +15 to +16
/// contour extraction module.
pub mod contours;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Readme missing contours module 📘 Rule violation ✧ Quality

A new public contours module is exported, but the crate README’s module list is not updated to
mention it. This makes the new API harder to discover and violates the requirement to update
user-facing docs for public API changes.
Agent Prompt
## Issue description
A new public module `kornia_imgproc::contours` was added, but the crate README does not list or describe it, reducing discoverability.

## Issue Context
The public API surface was extended by exporting `pub mod contours;`. The README has a `Modules` section enumerating available modules, but it currently omits `contours`.

## Fix Focus Areas
- crates/kornia-imgproc/src/lib.rs[15-16]
- crates/kornia-imgproc/README.md[83-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

let mut prev = (start_x - 1, start_y);
let start_prev = prev;
let mut safety = 0usize;
let safety_limit = width * height * 16;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Magic number 16 in tracing 📘 Rule violation ⛯ Reliability

The boundary tracing safety limit uses a hard-coded multiplier 16, which is an algorithm parameter
and is not self-documenting. This makes the algorithm harder to tune and reason about.
Agent Prompt
## Issue description
The contour tracing loop uses a hard-coded safety multiplier `16` (`width * height * 16`) to prevent infinite loops. This is an algorithm parameter and should be a named constant to clarify intent and allow future tuning.

## Issue Context
The safety limit affects algorithm termination behavior and should be self-documenting.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[354-365]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +114 to +120
let fg_mask: Vec<bool> = src.as_slice().iter().map(|&v| v != 0).collect();
let fg_labels = label_components(&fg_mask, width, height);
let fg_count = fg_labels
.iter()
.copied()
.max()
.map_or(0usize, |m| (m as usize) + 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Panic on empty labels 🐞 Bug ⛯ Reliability

fg_count/bg_count are computed as max_label + 1 without handling the all--1 case, which
overflows in debug/test builds and panics for images with no foreground (or no background). This
breaks expected behavior where such inputs should return zero contours.
Agent Prompt
## Issue description
`find_contours` computes `fg_count`/`bg_count` as `(max_label as usize) + 1`. When the image has no components, labels remain `-1` and `max()` returns `-1`; casting to `usize` and adding `1` overflows in debug builds, causing a panic.

## Issue Context
`label_components` initializes all labels to `-1` and only assigns non-negative labels when it finds a `true` pixel.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[114-120]
- crates/kornia-imgproc/src/contours.rs[151-156]
- crates/kornia-imgproc/src/contours.rs[272-304]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +212 to +233
RetrievalMode::Tree => {
let mut parent = vec![None; entries.len()];
for (idx, e) in entries.iter().enumerate() {
match e.kind {
ContourKind::Hole => {
parent[idx] = e.parent_hint;
}
ContourKind::Foreground => {
if let Some(bg_comp_id) = find_adjacent_background_component(
&fg_labels,
&bg_labels,
width,
height,
e.component_id,
) {
let bg_id = bg_comp_id as usize;
if bg_id < bg_touches_border.len()
&& !bg_touches_border[bg_id]
&& bg_id < bg_comp_to_entry.len()
{
parent[idx] = bg_comp_to_entry[bg_id];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Tree parent selection wrong 🐞 Bug ✓ Correctness

In RetrievalMode::Tree, a foreground contour’s parent is chosen as the first adjacent non-border
background component found by a raster scan, which is order-dependent and can pick an internal hole
instead of the outside background. For border-touching foreground components with holes (border
fully covered by foreground), this can even create a parent cycle (foreground ↔ hole).
Agent Prompt
## Issue description
`RetrievalMode::Tree` assigns a foreground contour’s parent to the first adjacent non-border background component returned by a raster scan. This is order-dependent and can select an internal hole background component, producing incorrect hierarchy and (for border-touching foregrounds with holes) can create a foreground↔hole parent cycle.

## Issue Context
- Holes set `parent_hint` to an adjacent foreground component.
- Foregrounds set `parent` to an adjacent non-border background component, but the adjacency search returns the first found label, not necessarily the outside component.

## Fix Focus Areas
- crates/kornia-imgproc/src/contours.rs[212-238]
- crates/kornia-imgproc/src/contours.rs[173-188]
- crates/kornia-imgproc/src/contours.rs[483-509]
- crates/kornia-imgproc/src/contours.rs[528-571]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@mathurojus
Copy link
Author

Thanks for the review! I've addressed all the issues:

  • Added contours to README
  • Replaced magic number with constant
  • Fixed empty labels panic
  • Improved tree parent selection
  • Ran cargo fmt
  • Added doctest examples
  • Optimized image scans

Just pushed - please take a look when you get a chance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement find_countours

1 participant