Skip to content

fix(imgproc): CLAHE edge-pixel extrapolation and calc_back_project range validation - #122

Merged
kalwalt merged 2 commits into
devfrom
fix/clahe-edge-and-backproject-validation
Sep 2, 2026
Merged

fix(imgproc): CLAHE edge-pixel extrapolation and calc_back_project range validation#122
kalwalt merged 2 commits into
devfrom
fix/clahe-edge-and-backproject-validation

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 2, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two real bugs found by Qodo's review on #121, verified against the actual
code before fixing (a third finding on that review — missing FromPrimitive
bound — is a false positive, the 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 edge-pixel extrapolation. Interpolation clamped the tile index
before computing the blend fraction. At y=0 (tile_rows=4), tyf=-0.5
floor=-1 → clamped to 0ya = 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.

calc_back_project range validation. It checked only ranges.len() == dims, never the contents — unlike calc_hist, which already validates
Uniform(lo, hi) satisfies lo < hi (rejecting NaN) and NonUniform
boundaries have the right length and strict ordering. Extracted calc_hist's
validation into a shared validate_hist_ranges() helper so the two functions
can't drift apart again.

Test plan

  • New unit test on tile_interp_weights checking weights stay in [0,1]
    and sum to 1 across the actual coordinate range (several tile counts),
    including the exact y=0 case from the bug report. Confirmed the old
    formula fails this assertion (ya=-0.5).
  • New tests for calc_back_project rejecting NaN/inverted uniform ranges
    and malformed non-uniform boundaries.
  • cargo fmt --check / clippy -D warnings clean under --all-features
    and --no-default-features.
  • cargo test --workspace: 345 lib tests + 40 doc-tests passing under
    both default (parallel) and --no-default-features --features std
    (sequential) configs, identical results.

🤖 Generated with Claude Code

…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-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix CLAHE edge interpolation and back-project range validation

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Keeps CLAHE edge interpolation weights bounded for both u8 and u16 paths.
• Shares strict histogram range validation across histogram and back-projection operations.
• Adds regressions for malformed ranges and edge-tile interpolation invariants.
Diagram

graph TD
  Input["Image Input"] --> Route{"Operation"} --> Hist["Histogram Path"] --> Validate["Range Validation"] --> Back["Back Projection"]
  Route --> CLAHE["CLAHE Path"] --> Weights["Tile Weights"] --> Blend["u8/u16 Blend"]
Loading
High-Level Assessment

The shared-helper approach is appropriate: it removes duplicated interpolation math across pixel depths and prevents histogram range validation from diverging between related APIs. Keeping separate implementations would increase regression risk without providing meaningful flexibility.

Files changed (2) +198 / -76

Bug fix (1) +100 / -76
histogram.rsCorrect CLAHE weights and centralize histogram range validation +100/-76

Correct CLAHE weights and centralize histogram range validation

• Extracts shared histogram range validation and applies it to both 'calc_hist' and 'calc_back_project', rejecting zero dimensions, invalid uniform ranges, and malformed non-uniform boundaries. Adds a shared CLAHE interpolation helper that derives blend fractions before clamping lookup indices, preventing edge extrapolation in both u8 and u16 paths.

src/imgproc/histogram.rs

Tests (1) +98 / -0
tests.rsAdd regressions for range validation and CLAHE edge weights +98/-0

Add regressions for range validation and CLAHE edge weights

• Verifies back projection rejects NaN, inverted, incorrectly sized, and unordered ranges. Exercises CLAHE interpolation across multiple tile counts and confirms indices remain valid while weights stay bounded and sum to one, including the reported edge coordinate.

src/imgproc/tests.rs

@kalwalt kalwalt self-assigned this Sep 2, 2026
@kalwalt kalwalt added enhancement New feature or request rust-code rust Pull requests that update rust code tests imgproc-module labels Sep 2, 2026
@qodo-code-review

qodo-code-review Bot commented Sep 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. Range validation can overflow ✓ Resolved 🐞 Bug ☼ Reliability
Description
For hist_size[d] == usize::MAX, validate_hist_ranges evaluates sz + 1, causing an overflow
panic before calc_back_project can return an input error. With overflow checks disabled and empty
boundaries, the wrapped expected length also reaches boundaries.len() - 1, which underflows and
ultimately panics.
Code

src/imgproc/histogram.rs[210]

+                if boundaries.len() != sz + 1 {
Evidence
The new validator directly computes sz + 1 without an overflow guard and then subtracts one from
the boundary length. calc_back_project now invokes this helper before computing total_bins or
checking the supplied histogram shape, so malformed input reaches the panic first.

src/imgproc/histogram.rs[209-223]
src/imgproc/histogram.rs[490-499]
src/imgproc/histogram.rs[519-535]

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

## Issue description
`validate_hist_ranges` performs unchecked `sz + 1` arithmetic for non-uniform ranges. A `hist_size` entry of `usize::MAX` therefore panics rather than producing the function's expected `InvalidInput` result.

## Issue Context
The new `calc_back_project` validation call exposes this panic before its histogram-shape check. Compute the expected boundary count with `checked_add`, return a tagged `InvalidInput` error on overflow, and reuse the checked value in both the comparison and error message. Add a regression test using `usize::MAX` and an empty non-uniform boundary list.

## Fix Focus Areas
- src/imgproc/histogram.rs[209-223]
- src/imgproc/tests.rs[1591-1617]

ⓘ 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: This changes image-processing interpolation behavior and shared histogram/back-projection validation across multiple code paths, so it carries meaningful correctness risk but is not broad or defect-dense enough to warrant redundant extended review.

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 Outdated
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

Confirmed real — reproduced it directly with std::panic::catch_unwind before fixing: hist_size[d] == usize::MAX panics with attempt to add with overflow at the sz + 1 in validate_hist_ranges, exactly as reported. The empty-boundaries underflow you also flagged is a consequence of the same root cause (only reachable once sz + 1 has already wrapped to 0 in a release build, matching an empty boundaries.len()), so guarding the addition closes both paths at once.

Fixed in f5da497: sz.checked_add(1) now returns a proper InvalidInput error on overflow instead of panicking, using cv_err! to stay consistent with the rest of the module's error handling. Added the regression test you suggested (hist_size = usize::MAX with empty non-uniform boundaries) — confirms calc_back_project now returns Err instead of panicking.

Verified: fmt/clippy clean under --all-features and --no-default-features; 346 lib tests + 40 doc-tests passing under both the default (parallel) and sequential configs.

@kalwalt
kalwalt merged commit e3e3613 into dev Sep 2, 2026
6 checks passed
@kalwalt kalwalt mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request imgproc-module rust Pull requests that update rust code rust-code tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant