From 8f494f0f46b3ba6a91ee1c76d2e93c88e5923d57 Mon Sep 17 00:00:00 2001 From: Yu Sun Date: Mon, 19 Jan 2026 13:46:02 +0800 Subject: [PATCH 1/6] chore: prepare for release with metadata and refined docs --- Cargo.toml | 5 +++ LICENSE | 2 +- src/contours.rs | 74 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 40 ++++++++++++++++++++-- src/rect.rs | 2 ++ src/region_labelling.rs | 18 ++++++++++ 6 files changed, 138 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df36087..1d74fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,11 @@ name = "image-debug-utils" version = "0.1.0" edition = "2024" license = "MIT" +description = "Niche but useful utilities for imageproc, including contour filtering, sorting, and visualization helpers." +repository = "https://github.com/bioinformatist/image-debug-utils" +readme = "README.md" +keywords = ["image", "imageproc", "debugging", "computer-vision", "visualization"] +categories = ["multimedia::images", "visualization", "development-tools::debugging"] [lib] path = "src/lib.rs" diff --git a/LICENSE b/LICENSE index a0c781f..8ee9f7e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2025 Richard Billyham +Copyright (c) 2026 Yu Sun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/contours.rs b/src/contours.rs index 1d2565e..79ebc04 100644 --- a/src/contours.rs +++ b/src/contours.rs @@ -1,3 +1,8 @@ +//! Specialized utilities for working with contours found by [`imageproc::contours`]. +//! +//! This module provides functions for filtering, sorting, and analyzing the hierarchy of +//! contours, complementing the core functionality in `imageproc`. + use imageproc::{ contours::{BorderType, Contour}, geometry::min_area_rect, @@ -31,6 +36,32 @@ use num_traits::AsPrimitive; /// A `Vec<(Contour, f64)>` sorted by the perimeter in descending order. /// Contours with 0 or 1 point will have a perimeter of `0.0`. /// +/// # Examples +/// +/// ``` +/// use imageproc::contours::{Contour, BorderType}; +/// use imageproc::point::Point; +/// use image_debug_utils::contours::sort_by_perimeters_owned; +/// +/// let c1 = Contour { +/// parent: None, +/// border_type: BorderType::Outer, +/// points: vec![Point::new(0,0), Point::new(10,0), Point::new(10,10), Point::new(0,10)] +/// }; // Perimeter 40.0 +/// +/// let c2 = Contour { +/// parent: None, +/// border_type: BorderType::Outer, +/// points: vec![Point::new(0,0), Point::new(10,0)] +/// }; // Perimeter 20.0 (10 + 10 back to start) +/// +/// let contours = vec![c2.clone(), c1.clone()]; +/// +/// let sorted = sort_by_perimeters_owned(contours); +/// assert_eq!(sorted[0].1, 40.0); +/// assert_eq!(sorted[1].1, 20.0); +/// ``` +/// pub fn sort_by_perimeters_owned(contours: Vec>) -> Vec<(Contour, f64)> where T: Num + NumCast + Copy + PartialEq + Eq + AsPrimitive, @@ -74,6 +105,32 @@ where /// * `max_aspect_ratio`: The maximum allowed aspect ratio. Must be a positive value. /// * `border_type`: An `Option` to filter contours by their border type. /// +/// # Examples +/// +/// ``` +/// use imageproc::contours::{Contour, BorderType}; +/// use imageproc::point::Point; +/// use image_debug_utils::contours::remove_hypotenuse_in_place; +/// +/// let mut contours = vec![ +/// // Square, aspect ratio 1.0 (keep) +/// Contour { +/// parent: None, +/// border_type: BorderType::Outer, +/// points: vec![Point::new(0,0), Point::new(10,0), Point::new(10,10), Point::new(0,10)] +/// }, +/// // Thin strip, high aspect ratio > 5.0 (remove) +/// Contour { +/// parent: None, +/// border_type: BorderType::Outer, +/// points: vec![Point::new(0,0), Point::new(100,0), Point::new(100,2), Point::new(0,2)] +/// } +/// ]; +/// +/// remove_hypotenuse_in_place(&mut contours, 5.0, None); +/// assert_eq!(contours.len(), 1); +/// ``` +/// /// # Panics /// /// # Type Parameters @@ -143,6 +200,23 @@ pub fn remove_hypotenuse_in_place( /// The time complexity is O(N log N), dominated by the final sort, where N is the number /// of contours. The memory overhead is minimal as no deep copies of contour data occur. /// +/// # Examples +/// +/// ``` +/// use imageproc::contours::{Contour, BorderType}; +/// use image_debug_utils::contours::sort_by_direct_children_count_owned; +/// +/// // Create a hierarchy where index 0 is parent of index 1 +/// let c0: Contour = Contour { parent: None, border_type: BorderType::Outer, points: vec![] }; +/// let c1: Contour = Contour { parent: Some(0), border_type: BorderType::Hole, points: vec![] }; +/// let contours = vec![c0, c1]; +/// +/// let sorted = sort_by_direct_children_count_owned(contours); +/// // c0 (sorted[0]) has 1 child, c1 (sorted[1]) has 0 +/// assert_eq!(sorted[0].1, 1); +/// assert_eq!(sorted[1].1, 0); +/// ``` +/// /// # Arguments /// /// * `contours` - A `Vec>` which will be consumed by the function. The caller diff --git a/src/lib.rs b/src/lib.rs index aa4cad3..dac68be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,42 @@ -//! A collection of debugging and visualization utilities for [imageproc]. +//! `image-debug-utils` is a collection of niche but highly practical utilities designed to complement +//! the [`imageproc`] crate. It focuses on easing the debugging and visualization of common computer +//! vision tasks. //! -//! The utility functions are organized into modules the same categories (as possible) as in [imageproc]. +//! To ensure a familiar developer experience, the modules and functions are organized to mirror +//! the structure of [`imageproc`] as closely as possible. +//! +//! # Main Modules +//! +//! * [`contours`]: Utilities for working with contours found by `imageproc::contours`, including +//! filtering by aspect ratio and hierarchical sorting. +//! * [`rect`]: Tools for geometric primitives, such as converting rotated rectangle vertices +//! into axis-aligned bounding boxes (useful for `imageproc::geometry::min_area_rect`). +//! * [`region_labelling`]: Helpers for visualizing results from `imageproc::region_labelling`, +//! such as coloring principal connected components. +//! +//! # Example: Filtering and Sorting Contours +//! +//! ```rust +//! use imageproc::contours::Contour; +//! use imageproc::contours::BorderType; +//! use image_debug_utils::contours::{remove_hypotenuse_in_place, sort_by_perimeters_owned}; +//! let mut contours: Vec> = Vec::new(); // Dummy data +//! // 1. Remove thin, "hypotenuse-like" artifacts from contours +//! remove_hypotenuse_in_place(&mut contours, 5.0, None); +//! +//! // 2. Sort remaining contours by their perimeter (descending) +//! let sorted = sort_by_perimeters_owned(contours); +//! ``` +//! +//! # Example: Visualizing Connected Components +//! +//! ```rust +//! use image_debug_utils::region_labelling::draw_principal_connected_components; +//! use image::{Rgba, ImageBuffer, Luma}; +//! let labelled_image = ImageBuffer::, Vec>::new(10, 10); // Dummy data +//! // Draw the top 5 largest connected components with contrasting colors +//! let colored_image = draw_principal_connected_components(&labelled_image, 5, Rgba([0, 0, 0, 255])); +//! ``` mod colors; pub mod contours; diff --git a/src/rect.rs b/src/rect.rs index 14e1bf5..84071bf 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -1,3 +1,5 @@ +//! Utilities for geometric primitives and bounding boxes, often used with `imageproc::geometry`. + use image::math::Rect; use imageproc::point::Point; use num_traits::{Num, ToPrimitive}; diff --git a/src/region_labelling.rs b/src/region_labelling.rs index a91151e..5a226b4 100644 --- a/src/region_labelling.rs +++ b/src/region_labelling.rs @@ -1,3 +1,5 @@ +//! Tools for visualizing and analyzing connected components results from `imageproc::region_labelling`. + use crate::colors::generate_contrasting_colors; use image::{ImageBuffer, Luma, Rgba, RgbaImage}; use std::collections::HashMap; @@ -9,6 +11,22 @@ use std::collections::HashMap; /// * `n` - The number of largest components to keep and color. /// * `background_color` - The color for the background and smaller, unselected components. /// +/// # Examples +/// +/// ``` +/// use image::{ImageBuffer, Luma, Rgba}; +/// use image_debug_utils::region_labelling::draw_principal_connected_components; +/// +/// let mut labelled_image = ImageBuffer::, Vec>::new(10, 10); +/// // Simulate a large component (label 1) and a small one (label 2) +/// labelled_image.put_pixel(0, 0, Luma([1])); +/// labelled_image.put_pixel(0, 1, Luma([1])); +/// labelled_image.put_pixel(5, 5, Luma([2])); +/// +/// // Keep top 1 component, use transparent black for background +/// let colored = draw_principal_connected_components(&labelled_image, 1, Rgba([0, 0, 0, 0])); +/// ``` +/// /// # Returns /// An `RgbaImage` where the `n` largest components are colored and the rest is background. pub fn draw_principal_connected_components( From a3fc1b3fc8e53f51b279febb70ebe575e409c84f Mon Sep 17 00:00:00 2001 From: Yu Sun Date: Mon, 19 Jan 2026 13:58:02 +0800 Subject: [PATCH 2/6] ci: add code coverage with tarpaulin and codecov --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e3e4ff..0c921b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,3 +32,17 @@ jobs: - name: Run Tests run: cargo test --all-features + + - name: Install cargo-tarpaulin + uses: taiki-e/install-action@v2 + with: + tool: cargo-tarpaulin + + - name: Generate Code Coverage + run: cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false From af3290192e92b372ba84dbf0274423056ffbc638 Mon Sep 17 00:00:00 2001 From: Yu Sun Date: Mon, 19 Jan 2026 14:20:04 +0800 Subject: [PATCH 3/6] test: add edge cases to improve code coverage --- src/contours.rs | 35 +++++++++++++++++++++++++++++++++++ src/rect.rs | 26 ++++++++++++++++++++++++++ src/region_labelling.rs | 12 ++++++++++++ 3 files changed, 73 insertions(+) diff --git a/src/contours.rs b/src/contours.rs index 79ebc04..40652d7 100644 --- a/src/contours.rs +++ b/src/contours.rs @@ -372,6 +372,21 @@ mod tests { contours5.is_empty(), "Should filter out the high-ratio contour" ); + + // --- Test Case 6: Degenerate contour (0-area) --- + let degenerate_contour_4pts = Contour { + points: vec![ + Point::new(0, 0), + Point::new(0, 0), + Point::new(1, 0), + Point::new(1, 0), + ], + border_type: BorderType::Outer, + parent: None, + }; + let mut contours6 = vec![degenerate_contour_4pts]; + remove_hypotenuse_in_place(&mut contours6, 5.0, None); + assert!(contours6.is_empty(), "0-area contour should be removed"); } #[test] @@ -412,6 +427,26 @@ mod tests { assert!(result[3..].iter().all(|(_, count)| *count == 0)); } + #[test] + fn test_sort_by_direct_children_count_empty() { + let contours: Vec> = Vec::new(); + let result = sort_by_direct_children_count_owned(contours); + assert!(result.is_empty()); + } + + #[test] + fn test_sort_by_direct_children_count_malformed_hierarchy() { + // Hierarchy with invalid parent index (out of bounds) + let contours = vec![Contour { + parent: Some(99), // Invalid index + border_type: BorderType::Outer, + points: vec![Point::new(1, 1)], + }]; + let result = sort_by_direct_children_count_owned(contours); + assert_eq!(result.len(), 1); + assert_eq!(result[0].1, 0); // Invalid parent index should be ignored + } + #[test] fn test_calculate_perimeters_and_sort_comprehensive() { // 1. Test with an empty vector diff --git a/src/rect.rs b/src/rect.rs index 84071bf..5287da1 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -174,4 +174,30 @@ mod tests { }; assert_eq!(to_axis_aligned_bounding_box(&vertices), expected); } + + #[test] + fn test_bounding_box_all_negative() { + // All points have negative coordinates. + // min_x = -100, max_x = -50, min_y = -80, max_y = -40 + let vertices = [ + Point { x: -50.0, y: -40.0 }, + Point { + x: -100.0, + y: -40.0, + }, + Point { + x: -100.0, + y: -80.0, + }, + Point { x: -50.0, y: -80.0 }, + ]; + // Everything should become 0. + let expected = Rect { + x: 0, + y: 0, + width: 0, + height: 0, + }; + assert_eq!(to_axis_aligned_bounding_box(&vertices), expected); + } } diff --git a/src/region_labelling.rs b/src/region_labelling.rs index 5a226b4..38d2122 100644 --- a/src/region_labelling.rs +++ b/src/region_labelling.rs @@ -134,4 +134,16 @@ mod tests { assert_eq!(result_image, expected_image); } + + #[test] + fn test_draw_principal_connected_components_empty() { + // All background image. + let labelled_image = ImageBuffer::, Vec>::new(5, 5); + let background = Rgba([255, 255, 255, 255]); + + // n = 0 + let result = draw_principal_connected_components(&labelled_image, 0, background); + assert_eq!(result.dimensions(), (5, 5)); + assert!(result.pixels().all(|p| *p == background)); + } } From f28d5fb0795cb82f6ecfab9411065666031b6211 Mon Sep 17 00:00:00 2001 From: Yu Sun Date: Mon, 19 Jan 2026 16:11:55 +0800 Subject: [PATCH 4/6] docs: add initial CHANGELOG and CI check for enforced updates --- .github/workflows/changelog_check.yml | 39 +++++++++++++++++++++++++++ CHANGELOG.md | 15 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .github/workflows/changelog_check.yml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/changelog_check.yml b/.github/workflows/changelog_check.yml new file mode 100644 index 0000000..7043365 --- /dev/null +++ b/.github/workflows/changelog_check.yml @@ -0,0 +1,39 @@ +name: Changelog Check + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'src/**' + - 'CHANGELOG.md' + +jobs: + check-changelog: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify Changelog Update + run: | + # Get the base branch to compare against + BASE_BRANCH="origin/${{ github.base_ref }}" + + echo "Checking for changes in src/ compared to $BASE_BRANCH..." + + SRC_CHANGED=$(git diff --name-only $BASE_BRANCH...HEAD | grep "^src/" || true) + CHANGELOG_CHANGED=$(git diff --name-only $BASE_BRANCH...HEAD | grep "^CHANGELOG.md$" || true) + + if [ -n "$SRC_CHANGED" ]; then + if [ -z "$CHANGELOG_CHANGED" ]; then + echo "❌ Changes detected in 'src/' but 'CHANGELOG.md' was not updated." + echo "Please add a summary of your changes to CHANGELOG.md." + exit 1 + else + echo "✅ 'src/' changed and 'CHANGELOG.md' was updated." + fi + else + echo "✅ No changes in 'src/', skipping mandatory changelog update check." + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d56fb42 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2025-01-19 + +### Added + +- **Contours**: Utilities for filtering by aspect ratio and sorting by perimeter or child count. +- **Rect**: Conversion from rotated rectangle vertices to axis-aligned bounding box (`Rect`). +- **Region Labelling**: Visualization tool to draw the N largest connected components with contrasting colors. +- **CI/CD**: Comprehensive pipeline with code coverage (Tarpaulin), clippy, and automated testing. From c65292a82aa2decd4618af0c6c596f192ff2c782 Mon Sep 17 00:00:00 2001 From: Yu Sun Date: Mon, 19 Jan 2026 16:12:34 +0800 Subject: [PATCH 5/6] ci: Add `--engine Llvm` to `cargo tarpaulin` command for code coverage generation. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c921b7..7056f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: tool: cargo-tarpaulin - name: Generate Code Coverage - run: cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + run: cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml --engine Llvm - name: Upload to Codecov uses: codecov/codecov-action@v5 From 78df3629d287f3d2d1374a9e664f5fe583e772f4 Mon Sep 17 00:00:00 2001 From: Yu Sun Date: Mon, 19 Jan 2026 16:37:39 +0800 Subject: [PATCH 6/6] docs: use token-based Codecov badge in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 58926ba..5be1848 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bioinformatist/image-debug-utils) [![Rust CI](https://github.com/bioinformatist/image-debug-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/bioinformatist/image-debug-utils/actions/workflows/ci.yml) [![Build and Deploy to Pages](https://github.com/bioinformatist/image-debug-utils/actions/workflows/pages.yml/badge.svg)](https://github.com/bioinformatist/image-debug-utils/actions/workflows/pages.yml) +[![codecov](https://codecov.io/gh/bioinformatist/image-debug-utils/graph/badge.svg?token=23U4M79DJH)](https://codecov.io/gh/bioinformatist/image-debug-utils) Some niche but useful utilities for [`imageproc`](https://github.com/image-rs/imageproc).