Skip to content

new v0.8.0 release - #121

Merged
kalwalt merged 20 commits into
mainfrom
dev
Sep 2, 2026
Merged

new v0.8.0 release#121
kalwalt merged 20 commits into
mainfrom
dev

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 2, 2026

Copy link
Copy Markdown
Member

This pull request prepares the 0.8.0 release of PureCV, introducing a new histogram module with SIMD and parallel support, various bug fixes, documentation updates, and CI improvements. It also updates the release process documentation and pins the Rust toolchain to version 1.98.0 in CI and release workflows. The most important changes are summarized below.


New Features and Performance Improvements

  • Added a new histogram module to imgproc, including calc_hist, calc_back_project, compare_hist (with all 6 OpenCV methods), equalize_hist, and Clahe for contrast enhancement, all with SIMD acceleration and parallel support where applicable. [1] [2] [3] [4]
  • Added benchmarks for histogram and contrast functions in benches/imgproc_bench.rs. [1] [2]

Documentation and Examples

  • Updated README.md and crates/wasm/README.md to document the new histogram/CLAHE features, usage examples, and revised installation/version instructions. [1] [2] [3] [4] [5] [6] [7]
  • Added release workflow and merge policy documentation in CLAUDE.md and clarified version bumping steps in MAINTAINERS.md. [1] [2]

Continuous Integration and Release Process

  • CI and release workflows now pin the Rust toolchain to version 1.98.0 instead of stable for reproducibility. [1] [2] [3] [4] [5] [6]
  • CI now runs the sequential (non-parallel) test suite in addition to parallel and simd tests.

Changelog and Versioning

  • Bumped version to 0.8.0 in Cargo.toml and workspace metadata, and added a detailed changelog entry for 0.8.0. [1] [2] [3]
  • Updated hardcoded version strings in documentation and installation snippets to "0.8". [1] [2] [3] [4]

Bug Fixes and Refactors

  • Various bug fixes and refactors in the histogram module, including improved guards and addressing review comments.

kalwalt and others added 18 commits August 20, 2026 18:18
Two things bit us during v0.7.1 and neither was written down.

`npm run build` regenerates crates/wasm/pkg/package.json, which is tracked
but had silently drifted a full release behind (0.6.1 while 0.7.0 shipped).

Merging the release PR with "Rebase and merge" replayed dev's commits as new
objects on main, so dev stopped being an ancestor of main — identical content,
divergent history. Repaired by resetting dev to main; documented so the next
release uses a merge commit instead.

Also notes the two version locations in Cargo.toml, the git-cliff invocation
and its missing-blank-line quirk, and that cliff.toml skips the release commit
itself — so other changes must land in their own commits first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…chunks lint

clippy 1.98 stabilized clippy::chunks_exact_to_as_chunks, which fires on
every constant-size chunks_exact/chunks_exact_mut call in color.rs and
broke CI on `dev` (and any unrelated open PR) once the runner picked up
the new stable release, since our workflows install `dtolnay/rust-toolchain@stable`
with no version pin.

- Pin all `dtolnay/rust-toolchain@stable` steps in ci.yml and release.yml to
  1.98.0 so a future clippy release can't retroactively break CI again.
- Allow the new lint at each call site (guarded with `unknown_lints` so it
  doesn't itself break on older toolchains). `[T]::as_chunks` isn't stable
  yet, so the suggested rewrite isn't available.
Qodo review on #105 flagged that #[allow(clippy::chunks_exact_to_as_chunks)]
violates this repo's compliance rule against introducing new Clippy allow
overrides. `[T]::as_chunks`/`as_chunks_mut` (clippy's own suggested rewrite)
is already stable, so rewrite the 12 flagged sites to use it directly and
drop the allow attributes instead of suppressing the lint.
Implement complete histogram processing module (src/imgproc/histogram.rs)
with OpenCV parity for imgproc histogram operations.

- calc_hist: generic over Matrix<T> (u8/u16/f32), supports uniform
  and non-uniform bins, multi-image channel indexing, mask, accumulate
- calc_back_project: generic over Matrix<T>, returns Matrix<f32>
- compare_hist: Correl, ChiSqr, ChiSqrAlt, Intersection,
  Bhattacharyya, KullbackLeibler
- equalize_hist: global histogram equalization for u8
- Clahe: Contrast Limited Adaptive Histogram Equalization
  with apply_u8 and apply_u16 support
- 23 unit tests covering all functions and error paths

Closes #98
- fix no_std import: replace use core::f64 with #[allow(unused_imports)] use num_traits::Float (15 E0599 in --no-default-features)
- validate calc_hist inputs: hist_size>0, Uniform lo<hi, NonUniform boundaries len==hist_size+1 and monotonic, accumulate hist length==product
- harden nonuniform_bin/uniform_bin against hist_size==0 and OOB indexing
- validate calc_back_project: reject empty hist instead of clamp(0, -1) panic
- validate Clahe tiles (handle negative Size2i via 0 sentinel) and return InvalidInput instead of divide-by-zero / overflow
- validate bit_shift (u8 0..=7, u16 0..=15) in apply impls, fix 256>>/65536>> overflow
- fix CLAHE padding parity with OpenCV: pad bottom/right with BORDER_REFLECT_101 instead of discarded top/left, remove stale _padded/pad params, use border_interpolate
- add tile_rows/tile_cols zero check and checked_mul overflow guards
- fix compare_hist empty histogram NaN -> InvalidInput
- clarify clip_and_redistribute is an exact port of OpenCV CLAHE_CalcLut_Body (clahe.cpp)
- refactor: generic pad_reflect101<T>, .max(0) tile sentinel, hoisted zero-capture lut_idx closure, Ordering import, functional hist_data init
- verified: cargo fmt --check ok, cargo clippy -- -D warnings and --no-default-features -- -D warnings clean, cargo test --lib histogram 29/29, cargo build --no-default-features ok
The histogram module (added in #99) had zero parallel/simd feature
gating, unlike the rest of imgproc. Add Rayon-backed fast paths behind
the existing parallel feature convention, with sequential fallbacks
preserved for no_std / parallel-disabled builds:

- calc_hist: row-chunked fold/reduce (partial per-chunk histograms
  merged elementwise), generic over all dims since the sequential code
  is already dims-agnostic.
- calc_back_project: row-chunked, mirrors the pattern in color.rs.
- equalize_hist: the final LUT-apply pass is now a flat par_iter_mut
  zip; the histogram/LUT build stays sequential (cheap, <=256 bins).
- Clahe::apply_u8/apply_u16: per-tile histogram + LUT construction
  parallelized via par_chunks_mut over the flat lut buffer (tiles are
  independent), and both interpolate_tiles_u8/u16 row-chunked the same
  way as calc_back_project.

Verified cargo fmt --check, clippy -D warnings under --all-features and
--no-default-features, cargo test --workspace (parallel-on) and
cargo test --lib histogram --no-default-features --features std
(sequential fallback) produce identical results, and a bare-metal
build (thumbv7em-none-eabihf) still compiles.

Adds calc_hist/calc_back_project/equalize_hist/Clahe::apply_u8 bench
functions to imgproc_bench.rs; benchmark_results.md numbers are left
for a follow-up run on the reference machine.

Closes #106
chunks_mut/par_chunks_mut panic on a zero chunk size regardless of
slice length. calc_back_project's new row-chunked parallel/sequential
dispatch used images[0].cols directly as the chunk size, so a
zero-width input (previously handled fine by the old nested loops,
which simply skipped the inner loop) now panicked instead of returning
an empty result.

Bail out with the already-correctly-shaped empty dst before reaching
the chunking dispatch, matching the original behavior. Confirmed the
other three functions touched in #106/#110 aren't affected: calc_hist
iterates by row-index range (safe at 0), equalize_hist uses flat
iter_mut/par_iter_mut (safe at 0), and Clahe already validates
cols == 0 via cv_bail! before reaching its interpolation loops.

Found by Qodo's review on #110.
- Moves SIMD helper from core to imgproc/simd.rs.
- Fixes numerical overflow by casting intermediate f32 to f64 for accumulations.
- Fixes epsilon classification of small f32 bins by using f64::EPSILON.
- Restores exact f64::min() behavior for Intersection method to handle NaNs correctly.
- Adds tests.rs coverage for compare_hist to satisfy Qodo rules.
- Implements MatVector class matching OpenCV.js conventions.
- Exposes calcHistUniform and calcHistNonUniform supporting u8 and f32 images.
- Exposes calcBackProjectUniform and calcBackProjectNonUniform.
- Exposes compareHist with HIST_CMP_* constants.
- Exposes equalizeHist with note on u8 single-channel support.
- Exposes Clahe class wrapper with note on u8 single-channel support.
- Adds unit tests in crates/wasm/tests/web.rs and updates README.md.

Closes #107
Qodo's review on this PR raised 10 findings; verified each against the
actual code (including compiling for the real wasm32-unknown-unknown
target and running the wasm test suite under Node) before acting.

Fixed (8 real issues):

- Doc comments contained raw control bytes (BEL replacing 'a' in
  "accumulate", tabs replacing 't' in "tile_grid_width/height"),
  corrupting generated API docs.
- crates/wasm/tests/web.rs failed to compile: it imported `Clahe`, but
  the Rust type is `WasmClahe` (`js_name` only renames the JS side).
  This went undetected because CI's "WASM Dual Build" job only runs
  `wasm-pack build`, never compiles the tests/ directory.
- accumulate=true was a no-op: calc_hist_uniform/calc_hist_non_uniform
  always passed None as the existing histogram to core, so accumulate
  produced identical output to accumulate=false regardless of its
  value. Both now take an existing_hist: Option<Mat> parameter.
- Multi-dimensional back-projection silently corrupted results (not a
  panic, contrary to the original report - verified with a probe test):
  calc_back_project inferred hist_size from hist's flat bin count alone,
  which is ambiguous for non-perfect-power shapes (8 bins could be [8]
  or [2, 4]). calc_back_project now takes an explicit hist_size
  parameter, matching calc_hist's existing convention, and the WASM
  wrapper functions thread it through instead of guessing.
- HIST_CMP_* comparison-method constants are unavoidably exposed as
  callable functions (wasm-bindgen has no support for exporting a
  `pub const`), matching the same pattern used throughout this file for
  every other constant group (MORPH_*, FAST_TYPE_*, COLOR_*, etc.) -
  kept as-is rather than special-cased.
- calc_back_project never validated that all input images have matching
  dimensions or that channel indices resolve (unlike calc_hist, which
  already does both). Fixed in core so every caller is protected.
- A multichannel mask was silently accepted by calc_hist and only its
  channel 0 was ever read. Core now rejects non-single-channel masks.

Not applied - false positives, verified directly rather than assumed:

- "&[usize] unsupported by wasm-bindgen": false. Compiled clean for
  wasm32-unknown-unknown directly and via the actual `wasm-pack build`
  CI runs.
- "Histogram APIs should return PureCvError, not JsError": false. Every
  wasm-exposed function in this file already returns JsError - that's
  the entire point of this being the boundary-adapter layer, and
  PureCvError doesn't implement Into<JsValue> anyway.
- Initial pass also "fixed" the camelCase/SCREAMING_CASE js_name
  overrides (calcHistUniform, HIST_CMP_*, etc.) toward snake_case,
  based on one README example. Reverted: an exhaustive grep of this
  file shows every single pre-existing export (add, cvtColor,
  gaussianBlur, MORPH_RECT, FAST_TYPE_5_8, ...) uses this exact pattern
  deliberately and consistently - the README's snake_case example was
  simply stale documentation, not the real convention.

Verified: cargo fmt --check and clippy -D warnings clean on both crates
(--all-features and --no-default-features for the core crate); core
`cargo test --workspace` 342 lib tests + 40 doc-tests passing; the real
`npm run build:wasm` (matching CI's WASM Dual Build job exactly)
succeeds; and `wasm-pack test --node` runs all 9 wasm tests (including
3 new regression tests for accumulate, 2D back-projection shape, and
mask/image-size validation) against the actual wasm32 runtime, not
just a type-check - all passing.
- examples/histogram.rs: calc_hist (uniform and non-uniform bins),
  calc_back_project, compare_hist across all 6 HistCompMethods,
  equalize_hist, and Clahe::apply_u8 at two tile-grid sizes. Loads
  examples/data/butterfly.jpg (matching filters.rs's convention) and
  saves output PNGs under examples/data/out/. The back-projection scale
  is derived from the histogram's own peak bin (255 / max_bin) rather
  than a fixed guess, matching OpenCV's typical demo pattern - visually
  verified each output image before settling on this.

- crates/wasm/www/example_histogram.html + .js: interactive demo
  matching the existing per-feature example_*.html/.js convention
  (example_pyramid, example_hough_circles, ...) and reusing
  cv_demo_utils.js. Shows grayscale/equalize_hist/CLAHE side by side, a
  live calc_hist bar chart, and compare_hist scores against a
  CLAHE-equalized histogram, with clip-limit and tile-grid sliders.
  Linked from crates/wasm/www/index.html's gallery. Verified end-to-end
  in a real browser (served crates/wasm/ over HTTP, built the actual
  wasm-pack output) including interactive slider updates re-triggering
  calc_hist/equalize_hist/Clahe/compare_hist and re-rendering correctly.

- Root README.md: new purecv-imgproc bullet for histograms/CLAHE, added
  `cargo run --example histogram` to the Running Examples list, and
  updated the Imgproc module test-coverage bullet (308 -> 342 unit
  tests, current count, plus a mention of the histogram/CLAHE tests).

Closes #108
Verified each of the 5 findings against the code before acting:

- Missing output directory (real, confirmed): the example wrote into
  examples/data/out/ without creating it first. It's gitignored, so a
  fresh checkout doesn't have it - the first save would fail with an
  IO error. corner_detection.rs already handles this correctly
  (std::fs::create_dir_all before saving); filters.rs, which this
  example was copied from, has the same latent bug. Reproduced by
  deleting examples/data/out/ and re-running - confirmed the fix.
- Deep imports (split verdict): `Size2i` is already re-exported at
  purecv::core::Size2i (one existing example, corner_detection.rs,
  already uses that path) - switched to it, harmless simplification.
  NOT adding a `tags` re-export or changing that import: `use
  purecv::core::logging::tags;` is the exhaustive, deliberate
  convention used in 14+ files across this crate, including 3 other
  pre-existing examples and the official usage examples in
  logging.rs's own doc comments. Changing it would make this example
  the only inconsistent one in the whole codebase.
- WASM object leak on error path, blob URL never revoked, and
  drag-and-drop advertised but not implemented (all real, confirmed):
  none of the other 9 example_*.js files in crates/wasm/www/ handle
  any of these three either, so these aren't regressions from an
  established good pattern - genuine bugs worth fixing in this file
  regardless. Allocated WASM objects are now tracked in variables
  declared outside the try block and freed in a finally block; the
  file-upload blob URL is now revoked once loadImage resolves (success
  or failure); dragover/drop handlers now share the same load path as
  the click-to-upload flow.

Verified: cargo fmt --check and clippy -D warnings clean; cargo test
--workspace 342 lib tests + 40 doc-tests passing; re-ran the example
against a deleted examples/data/out/ to confirm the directory-creation
fix; and re-verified the WASM demo end-to-end in a real browser -
normal rendering and slider interaction still work after the
try/finally refactor (no console errors, no double-free), and a
synthetic drop event (fetched the demo image, dispatched a real
DragEvent at the drop zone) confirmed the new drag-and-drop path loads
and processes the image correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI never actually executed the #[cfg(not(feature = "parallel"))] code
path for any module - it was compiled (via the no_std job's cargo
build/clippy) but never run under cargo test. cargo test --workspace
uses the crate's default = ["std", "parallel"] feature set, so the
sequential fallback in every module was untested at runtime.

Add a step running cargo test --workspace --no-default-features
--features std alongside the existing parallel/simd/ndarray feature
runs. Verified locally first: 342 lib tests + 40 doc-tests pass under
this config with no latent sequential/parallel divergence.

Closes #112

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other imgproc submodule (color.rs, edge.rs, filter.rs, morph.rs,
pyramid.rs, threshold.rs, hough.rs, geometric.rs, resize.rs,
derivatives.rs, feature.rs) keeps its tests solely in the shared
src/imgproc/tests.rs, per the module structure convention in
CLAUDE.md. histogram.rs was the only one with its own embedded
#[cfg(test)] mod tests block, left over from how #99 originally wrote
it.

Mechanical move, no behavior change: all 33 histogram tests already
went through the public API (calc_hist, calc_back_project,
compare_hist, equalize_hist, Clahe/create_clahe, RangeSpec,
HistCompMethods) with no dependency on private helpers, so they drop
straight into tests.rs's existing imgproc_tests module (which already
glob-imports crate::core::* and crate::imgproc::*) with no import
changes needed.

Verified: same 342 lib test count before and after (nothing lost or
duplicated), passing under both default and
--no-default-features --features std, fmt/clippy clean under
--all-features and --no-default-features.

Closes #111

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bump version 0.7.1 -> 0.8.0 in Cargo.toml ([package] and
  [workspace.package]) and root package.json. crates/wasm/Cargo.toml
  inherits via version.workspace = true, no separate edit needed.
- Regenerate crates/wasm/pkg/package.json via npm run build.
- Fix README.md's hardcoded install-snippet version strings (still
  said "0.6" in five places, e.g. `purecv = "0.6"` - these don't
  update automatically with the version bump). crates/wasm/README.md
  had none.
- Add a note to MAINTAINERS.md's release Step 2 about checking both
  READMEs for hardcoded version strings, so this doesn't drift again.
- Generate the v0.8.0 changelog entry via git-cliff, plus the blank
  line before the previous version heading that --prepend omits.

Minor: this is a minor version bump (0.7.1 -> 0.8.0) rather than a
patch, since this release adds a whole new feature area (the
histogram/CLAHE module: calc_hist, calc_back_project, compare_hist,
equalize_hist, Clahe, with parallel + SIMD support and WASM bindings)
with no breaking changes to previously-released APIs, matching this
project's own precedent (0.6 -> 0.7.0 was the last feature-sized bump;
0.7.0 -> 0.7.1 was CI/Miri-only).

Verified: cargo build --workspace and cargo test --workspace clean
(342 lib tests + 40 doc-tests) with the new version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kalwalt kalwalt self-assigned this Sep 2, 2026
@kalwalt kalwalt added enhancement New feature or request new-release labels Sep 2, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add histogram processing and prepare PureCV 0.8.0 release

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds histogram calculation, back-projection, comparison, equalization, and CLAHE with parallel and
 SIMD paths.
• Exposes histogram operations to WebAssembly with tests, benchmarks, and interactive
 Rust/JavaScript examples.
• Prepares 0.8.0 metadata, documentation, reproducible CI toolchains, and safer release guidance.
Diagram

graph TD
  R["Rust Clients"] --> A["Imgproc API"] --> H["Histogram Core"] --> M["Matrix Results"]
  J["Web Clients"] --> W["WASM Bindings"] --> H
  H --> P["Rayon Paths"]
  H --> S["SIMD Compare"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Shape-aware histogram type
  • ➕ Preserves multidimensional bin shape and ranges with histogram data.
  • ➕ Eliminates repeated hist_size arguments during back-projection.
  • ➕ Provides stronger native and WebAssembly validation.
  • ➖ Adds another public type and conversion layer.
  • ➖ Diverges from the library's existing Matrix-centered API.
  • ➖ Complicates OpenCV-style WebAssembly interoperability and release scope.

Recommendation: The flattened Matrix approach is appropriate for 0.8.0 because it matches existing PureCV and OpenCV-style APIs, remains no_std-friendly, and crosses the WebAssembly boundary simply. Requiring and validating explicit hist_size resolves the immediate shape ambiguity; a shape-aware wrapper can be considered later if multidimensional histogram workflows expand.

Files changed (23) +3779 / -33

Enhancement (4) +2096 / -0
lib.rsExpose histogram processing through WebAssembly +628/-0

Expose histogram processing through WebAssembly

• Adds MatVector, histogram comparison constants, uniform and non-uniform histogram APIs, back-projection, equalization, and a configurable CLAHE wrapper. Validates matrix depths and converts JavaScript arguments into core histogram types.

crates/wasm/src/lib.rs

imgproc.rsExport the histogram module and public APIs +5/-0

Export the histogram module and public APIs

• Registers the histogram module and re-exports its functions, types, comparison methods, and CLAHE constructor from imgproc.

src/imgproc.rs

histogram.rsImplement histogram processing and CLAHE +1357/-0

Implement histogram processing and CLAHE

• Introduces multidimensional uniform and non-uniform histograms, masks, accumulation, back-projection, six comparison methods, global equalization, and u8/u16 CLAHE. Includes extensive validation plus optional Rayon execution and SIMD comparison dispatch.

src/imgproc/histogram.rs

simd.rsAdd SIMD-friendly histogram comparisons +106/-0

Add SIMD-friendly histogram comparisons

• Implements auto-vectorizable loops for correlation, chi-square variants, intersection, Bhattacharyya distance, and Kullback-Leibler divergence.

src/imgproc/simd.rs

Refactor (1) +28 / -12
color.rsAdopt fixed-size slice chunk APIs +28/-12

Adopt fixed-size slice chunk APIs

• Replaces constant-size chunks_exact loops with as_chunks variants to satisfy Rust 1.98 clippy without suppressing the lint.

src/imgproc/color.rs

Tests (2) +743 / -1
web.rsTest WebAssembly histogram bindings +91/-1

Test WebAssembly histogram bindings

• Covers equalization, CLAHE configuration, histogram accumulation and comparison, multidimensional back-projection shape, and invalid image or mask inputs.

crates/wasm/tests/web.rs

tests.rsAdd comprehensive histogram regression coverage +652/-0

Add comprehensive histogram regression coverage

• Tests histogram dimensions, ranges, masks, channel selection, accumulation, data types, comparison methods, equalization, CLAHE, back-projection, invalid inputs, and zero-width images.

src/imgproc/tests.rs

Documentation (10) +804 / -10
CHANGELOG.mdDocument the PureCV 0.8.0 release +42/-0

Document the PureCV 0.8.0 release

• Adds the 0.8.0 release notes covering histogram features, WebAssembly support, performance work, fixes, tests, and documentation.

CHANGELOG.md

CLAUDE.mdRecord safe release and merge procedures +23/-0

Record safe release and merge procedures

• Documents release preparation, generated package metadata, changelog generation, tagging, and the requirement to use merge commits for release PRs.

CLAUDE.md

MAINTAINERS.mdExpand version-bump verification guidance +5/-1

Expand version-bump verification guidance

• Adds root npm metadata and README installation snippets to the release version checklist.

MAINTAINERS.md

README.mdDocument histogram features and 0.8 installation +11/-7

Document histogram features and 0.8 installation

• Describes the new histogram and contrast APIs, adds the histogram example, updates dependency snippets, and refreshes test coverage counts.

README.md

README.mdDocument WebAssembly histogram APIs +34/-1

Document WebAssembly histogram APIs

• Adds histogram and CLAHE bindings to the supported API list with JavaScript usage, type limitations, accumulation, and shape guidance.

crates/wasm/README.md

README.mdPublish histogram documentation in the npm package +34/-1

Publish histogram documentation in the npm package

• Mirrors the WebAssembly histogram and contrast documentation in the generated npm package README.

crates/wasm/pkg/README.md

example_histogram.htmlAdd interactive histogram and CLAHE demo page +224/-0

Add interactive histogram and CLAHE demo page

• Creates a browser interface for image upload, CLAHE controls, output comparison, histogram visualization, and comparison scores.

crates/wasm/www/example_histogram.html

example_histogram.jsImplement the WebAssembly histogram demo +221/-0

Implement the WebAssembly histogram demo

• Loads images, computes histograms, applies global and adaptive equalization, compares distributions, renders outputs, and explicitly frees WebAssembly resources.

crates/wasm/www/example_histogram.js

index.htmlLink the histogram demo from the example index +5/-0

Link the histogram demo from the example index

• Adds a new Imgproc example card for histogram comparison and contrast enhancement.

crates/wasm/www/index.html

histogram.rsAdd a native histogram and CLAHE example +205/-0

Add a native histogram and CLAHE example

• Demonstrates uniform and non-uniform histograms, back-projection, all comparison methods, global equalization, and multiple CLAHE grids. Saves processed images for inspection.

examples/histogram.rs

Other (6) +108 / -10
ci.ymlPin Rust 1.98 and test sequential feature mode +7/-4

Pin Rust 1.98 and test sequential feature mode

• Pins all CI jobs to Rust 1.98.0 for reproducible clippy behavior. Adds workspace testing with parallelism disabled.

.github/workflows/ci.yml

release.ymlPin release jobs to Rust 1.98 +2/-2

Pin release jobs to Rust 1.98

• Uses Rust 1.98.0 for crates.io and npm publication jobs instead of the moving stable toolchain.

.github/workflows/release.yml

Cargo.tomlBump Rust package versions to 0.8.0 +2/-2

Bump Rust package versions to 0.8.0

• Updates both root package and workspace package versions from 0.7.1 to 0.8.0.

Cargo.toml

imgproc_bench.rsBenchmark histogram and contrast operations +95/-0

Benchmark histogram and contrast operations

• Adds Criterion benchmarks for histogram calculation, back-projection, equalization, CLAHE, and representative comparison methods.

benches/imgproc_bench.rs

package.jsonBump generated npm package to 0.8.0 +1/-1

Bump generated npm package to 0.8.0

• Updates the generated WebAssembly package version from 0.7.1 to 0.8.0.

crates/wasm/pkg/package.json

package.jsonBump root npm metadata to 0.8.0 +1/-1

Bump root npm metadata to 0.8.0

• Updates the repository package version from 0.7.1 to 0.8.0.

package.json

@qodo-code-review

qodo-code-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. CLAHE edge weights extrapolate ✓ Resolved 🐞 Bug ≡ Correctness
Description
CLAHE clamps the lower tile index before calculating interpolation fractions, so pixels in the top
and left half-tiles receive negative weights instead of a convex blend. This corrupts both u8 and
u16 CLAHE output across those image regions.
Code

src/imgproc/histogram.rs[R1222-1224]

+        let ty1 = (tyf.floor() as i32).max(0);
+        let ty2 = (ty1 + 1).min(tiles_y as i32 - 1);
+        let ya = tyf - ty1 as f64;
Evidence
At y=0, lines 1221-1225 calculate tyf=-0.5, clamp ty1 to 0, and then derive ya=-0.5; the
analogous x calculation does the same at the left edge. Those negative fractions are directly used
to blend four LUT entries, and the u16 implementation duplicates the calculation.

src/imgproc/histogram.rs[1221-1225]
src/imgproc/histogram.rs[1230-1247]
src/imgproc/histogram.rs[1288-1314]

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

## Issue description
CLAHE derives `ya` and `xa` from tile indices after those indices have been clamped. At the top and left edges this produces negative interpolation fractions and extrapolates between tile LUTs.

## Issue Context
For `y = 0`, `tyf` is `-0.5`, but clamping `floor(tyf)` to zero before calculating `ya` makes `ya = -0.5` and `ya1 = 1.5`. Preserve the original floor value for weight calculation, then independently clamp both LUT indices, and apply the equivalent correction to the u16 path.

## Fix Focus Areas
- src/imgproc/histogram.rs[1221-1247]
- src/imgproc/histogram.rs[1288-1314]

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



Remediation recommended

2. Backprojection accepts invalid ranges ✓ Resolved 🐞 Bug ≡ Correctness
Description
calc_back_project checks only the number of ranges and never validates uniform bounds or
non-uniform boundaries. A uniform range containing NaN, for example, maps every in-image value to
bin zero and returns a plausible but incorrect projection instead of an error.
Code

src/imgproc/histogram.rs[R462-465]

+    if ranges.len() != dims {
+        cv_bail!(
+            tags::IMGPROC,
+            InvalidInput,
Evidence
calc_hist validates lo < hi, boundary counts, and strict boundary ordering, whereas
calc_back_project proceeds from length checks directly to stride construction. In uniform_bin,
comparisons against NaN are false and casting the resulting NaN bin expression to i32 yields zero,
causing every value to select the first histogram bin.

src/imgproc/histogram.rs[120-128]
src/imgproc/histogram.rs[271-323]
src/imgproc/histogram.rs[462-520]

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

## Issue description
`calc_back_project` accepts malformed `RangeSpec` values that `calc_hist` rejects, allowing invalid ranges to produce silently incorrect output.

## Issue Context
Apply the same validation rules used by `calc_hist`: uniform bounds must be finite and ordered, while non-uniform boundaries must have `hist_size + 1` finite, strictly increasing entries. Prefer extracting a shared validator so the two operations cannot drift.

## Fix Focus Areas
- src/imgproc/histogram.rs[271-323]
- src/imgproc/histogram.rs[462-520]

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


3. read_pixel_f32 lacks required bounds 📘 Rule violation ≡ Correctness
Description
read_pixel_f32 converts values from Matrix<T> to f32 but omits the required FromPrimitive,
Send, and Sync bounds; the public histogram APIs also omit FromPrimitive. This violates the
required uniform bounds for numeric generic Matrix<T> operations.
Code

src/imgproc/histogram.rs[R105-106]

+fn read_pixel_f32<T: ToPrimitive + Clone + Default>(
+    images: &[&Matrix<T>],
Evidence
Rule 2966076 requires the complete Default + Clone + ToPrimitive + FromPrimitive + Send + Sync
bound set for generic functions performing numeric operations on Matrix<T>. The helper converts
matrix elements to f32 while declaring only ToPrimitive + Clone + Default, and both public
callers omit FromPrimitive.

Rule 2966076: Constrain generic Matrix<T> operations with required trait bounds
src/imgproc/histogram.rs[103-112]
src/imgproc/histogram.rs[192-200]
src/imgproc/histogram.rs[439-446]

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 generic histogram operations on `Matrix<T>` do not declare the complete required trait-bound set.

## Issue Context
`read_pixel_f32` performs primitive conversion through `ToPrimitive`, so rule 2966076 requires `T: Default + Clone + ToPrimitive + FromPrimitive + Send + Sync`. Apply the same complete bounds to the public generic APIs that call this helper.

## Fix Focus Areas
- src/imgproc/histogram.rs[103-112]
- src/imgproc/histogram.rs[192-200]
- src/imgproc/histogram.rs[439-446]

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


Grey Divider

Context sources
✅ Compliance rules (platform): 27 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/imgproc/histogram.rs
Comment thread src/imgproc/histogram.rs Outdated
Comment thread src/imgproc/histogram.rs
kalwalt and others added 2 commits September 2, 2026 20:24
…nge validation

Two real bugs found by Qodo's review on #121, verified before fixing
(a third finding on the same review - missing FromPrimitive bound -
is a false positive, same recurring rule-2966076 pattern already
identified on #110: the functions only ever read pixels via
ToPrimitive::to_f32(), never construct T from a primitive).

- CLAHE interpolation clamped the tile index *before* computing the
  blend fraction. At y=0 (tile_rows=4), tyf=-0.5 -> floor=-1 -> clamped
  to 0 -> ya = tyf - 0 = -0.5, ya1 = 1.5. Those aren't interpolation
  weights (they don't lie in [0,1]) - they extrapolate beyond the edge
  tile's LUT instead of blending within it, on the top row and left
  column of both the u8 and u16 paths. Fixed by deriving the fraction
  from the *unclamped* floor (always in [0,1) by construction) and
  clamping only the LUT-lookup indices separately. Extracted the
  duplicated u8/u16 weight math into a single tile_interp_weights()
  helper so both paths share one implementation and a direct unit test
  can check the invariant (weights in [0,1], sum to 1) across the
  actual coordinate range, including the exact y=0 case from the bug
  report. Confirmed the old formula fails that assertion (ya=-0.5).

- calc_back_project checked only ranges.len() == dims, never the
  contents - unlike calc_hist, which already validates Uniform(lo, hi)
  satisfies lo < hi (rejecting NaN via partial_cmp) and NonUniform
  boundaries have the right length and strict ordering. A malformed
  range (NaN bounds, wrong-length or unsorted boundaries) silently
  produced a plausible-but-wrong back-projection instead of an error.
  Extracted calc_hist's validation into a shared validate_hist_ranges()
  helper (parameterized by the caller's name for error messages) so
  calc_hist and calc_back_project can't drift apart again, per Qodo's
  own suggestion.

Verified: cargo fmt --check and clippy -D warnings clean under
--all-features and --no-default-features; cargo test --workspace
345 lib tests + 40 doc-tests (342 + 3 new regression tests) passing
under both the default (parallel) and --no-default-features --features
std (sequential) configs, identical results.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Qodo's review on #122 found that validate_hist_ranges computed
hist_size[d] + 1 unchecked, so hist_size[d] == usize::MAX panicked
with "attempt to add with overflow" (in debug/test builds) instead of
returning the InvalidInput error the function exists to produce -
violating the project's own "never panic! in library code" rule.
Confirmed by reproducing the panic directly (std::panic::catch_unwind)
before applying the fix.

The empty-boundaries underflow Qodo also flagged (boundaries.len() - 1
on an empty Vec) is a consequence of the same root cause: it's only
reachable when sz + 1 has already wrapped to 0 in a release build
without overflow checks, matching an empty boundaries.len(). Guarding
the addition with checked_add and returning InvalidInput on overflow
closes both paths at once - for any hist_size that doesn't overflow,
sz + 1 >= 1, so boundaries.len() == sz + 1 can never be 0.

Verified: cargo fmt --check and clippy -D warnings clean under
--all-features and --no-default-features; new regression test
(hist_size = usize::MAX, empty non-uniform boundaries) confirms
calc_back_project now returns Err instead of panicking; cargo test
--workspace 346 lib tests + 40 doc-tests passing under both the
default (parallel) and --no-default-features --features std
(sequential) configs, identical results.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kalwalt

kalwalt commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

This is a false positive, confirmed twice now — same finding as PR #110's review (rule 2966076, "read_pixel_f32 lacks required bounds") and again as finding #2 on #122's review.

read_pixel_f32 only ever reads a pixel value via ToPrimitive::to_f32() — it never constructs a T from a primitive anywhere in this file (grep -n "FromPrimitive\|T::from\|::from(" src/imgproc/histogram.rs returns nothing). FromPrimitive genuinely isn't needed here; the rule is matching on the function's shape ("converts via ToPrimitive") without checking whether a from_* construction actually occurs.

The other two findings from this same review (CLAHE edge extrapolation, calc_back_project range validation) were real and are fixed via #122, which is why they already show resolved here.

@kalwalt
kalwalt merged commit ea00ef5 into main Sep 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new-release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants