Skip to content

new v0.9.0 release - #153

Merged
kalwalt merged 32 commits into
mainfrom
dev
Sep 27, 2026
Merged

kalwalt merged 32 commits into
mainfrom
dev

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 27, 2026

Copy link
Copy Markdown
Member

This pull request releases PureCV 0.9.0. It brings calc_optical_flow_pyramid_lk (pyramidal Lucas-Kanade) to OpenCV parity and moves npm publishing to trusted publishing. 32 commits, 13 files changed (+1142 / −146); the full list is in the new CHANGELOG.md entry.

⚠️ Merge with "Create a merge commit", not "Rebase and merge" or "Squash and merge". Rebasing would give dev's commits new SHAs on main and make the branches diverge (see CLAUDE.md, v0.7.1 / #95).


Lucas-Kanade optical flow: OpenCV parity (src/video/optical_flow.rs)

These change tracking results for existing callers, which is why this is a minor version bump. Each fix has regression tests with hand-derived expected values; 359 unit tests in total.

CI and release process

Versioning and changelog (#152)

  • Version bumped to 0.9.0 in Cargo.toml ([package] and [workspace.package]), package.json and crates/wasm/pkg/package.json; README install snippets updated to "0.9"; CHANGELOG.md entry generated with git-cliff.
  • Checked before merging chore(release): prepare for v0.9.0 #152: cargo publish --dry-run -p purecv succeeds, and a local npm run build:wasm (std + SIMD) regenerates crates/wasm/pkg/package.json byte-identical to the committed file.

After merging

  1. Tag v0.9.0 on the new main merge commit and push the tag. This publishes to crates.io and npm and can't be undone. The release workflow waits for the Build and Test, WASM, Benchmarks and both Miri checks before publishing.
  2. Issues LK pyramid uses Sobel instead of Scharr gradients (undocumented divergence from OpenCV) #130, LK iteration lacks OpenCV's oscillation half-step fallback #131, calc_optical_flow_pyramid_lk's min_eigen_threshold is not on OpenCV's scale (missing FLT_SCALE=2^-20 normalization) #138, lk_single_level's det.abs() < f64::EPSILON degeneracy guard is on the wrong scale after #130/#138 #142, LK tracking window sampling diverges from OpenCV for even win_size (integer half-window truncation) #144, calc_optical_flow_pyramid_lk aborts the whole pyramid on any level's degeneracy, OpenCV only at level 0 #145, LK Newton step is 1/32 of the full step: It is not scaled to match the Scharr-scaled gradients #149 and publish-npm job authenticates via npm config set + NPM_TOKEN; switch to OIDC trusted publishing #129 can be closed once the release is out.

🤖 Generated with Claude Code

kalwalt and others added 30 commits September 19, 2026 19:05
Closed-form regression test on a linear ramp: currently fails because
build_optical_flow_pyramid still uses Sobel.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
OpenCV's buildOpticalFlowPyramid computes derivatives unconditionally
with Scharr (calcScharrDeriv, lkpyramid.cpp:748) — there is no Sobel
path. purecv used sobel(..., 3, ...), an undocumented divergence.

scharr() already existed as sobel(ksize=-1) sharing the same
fast_deriv_3x3 path, so this is a drop-in swap under both the
parallel and simd features.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…130)

Reference computed independently via scharr() against the documented
H-matrix formula; currently fails because calc_optical_flow_pyramid_lk
still uses Sobel internally.

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

OpenCV's calcOpticalFlowPyrLK computes derivatives unconditionally
with Scharr (calcScharrDeriv, lkpyramid.cpp:1332) — no Sobel path
exists. purecv used sobel(..., 3, ...) for the previous-frame
derivatives, an undocumented divergence from the module's own
'Mirrors cv::calcOpticalFlowPyrLK' contract.

Scharr's unit-step response is ~4x Sobel's, so the spatial-gradient
matrix H (proportional to the derivative squared) was ~16x smaller
than OpenCV's on the same input. Any min_eigen_threshold tuned
against OpenCV (e.g. the commonly used 1e-4) was therefore ~16x
looser on purecv's old Sobel-scale H than intended, admitting
weak-texture tracks OpenCV would reject.

BREAKING BEHAVIOR: min_eigen_threshold values tuned against previous
purecv releases must be retuned against the new Scharr scale — see
the updated rustdoc.

Closes #130.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_calc_optical_flow_pyramid_lk_uses_scharr_derivatives compared
err[0] (f32) against an f64 reference with an absolute 1e-3 tolerance.
At this test's magnitude (~2.5M), f32's precision has an inherent
rounding step of ~0.125-0.25, exceeding that tolerance even with
correct Scharr derivatives. Switch to a 1e-5 relative tolerance,
which comfortably covers f32 rounding while remaining far tighter
than the ~1530% relative Sobel-vs-Scharr divergence this test is
designed to catch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Sobel-to-Scharr swap in build_optical_flow_pyramid and
calc_optical_flow_pyramid_lk left several doc comments unchanged,
found in final review: the OpticalFlowPyramid field docs, the wasm
bindings' doc comments, lk_single_level's param docs, and a bench
comment all still said Sobel. Also clarifies that min_eigen_threshold
is still not directly transferable from OpenCV even after this fix,
since OpenCV additionally applies a FLT_SCALE=2^-20 normalization
that purecv does not (a separate, pre-existing gap, out of scope
here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The count was already stale before this branch (dev tip has 346 unit
tests, not 342) and drifted further with the two regression tests
added here for #130. Bump to the current count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Remove the speculative #[cfg_attr(miri, ignore)] from both new
  regression tests: measured under Miri (std,simd) at ~0.8s and ~18s
  respectively, well under the project's >30s exclusion threshold
  (.agents/MIRI_PLAN.md §4), so per the plan's own "measured evidence
  only, never speculatively" policy they should not be excluded.
- Fix test_build_pyramid_with_derivatives's Miri coverage comment
  (in both src/video/tests.rs and MIRI_PLAN.md §4/§12): it now
  exercises the Scharr unsafe fast path, not Sobel, and the
  mitigating coverage is imgproc::tests::test_scharr, not test_sobel
  (verified: also fast under Miri, ~0.75s).
- Revert MIRI_PLAN.md's acceptance-criteria exclusion count back to
  9 (unaffected, since the two new tests are no longer excluded).
- benches/benchmark_results.md: the build_optical_flow_pyramid
  derivatives section described a Sobel pass; note the switch to
  Scharr and that performance is unchanged (same fast_deriv_3x3 path).
- crates/wasm/src/lib.rs: add the same FLT_SCALE=2^-20 caveat to the
  wasm calcOpticalFlowPyrLK min_eigen_threshold doc that already
  exists on the Rust side, so JS callers aren't misled either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit applied OpenCV's FLT_SCALE = 2^-20 but also changed
the divisor to `2 * win_area`, copying `lkpyramid.cpp`'s
`2*winSize.width*winSize.height` literally. That denominator pairs with
OpenCV's numerator, which is deliberately `2*lambda_min` -- it omits the
`/2` of the eigenvalue formula. purecv computes a true `lambda_min` (the
`* 0.5` in `min_eigen`), so dividing by `2 * win_area` applied the
halving twice and reported half of OpenCV's `minEig`.

Use `win_area` as the divisor, hoist FLT_SCALE to a documented
module-level constant, and record why the `2 *` must not be copied.

Nothing pinned the scale before: `test_lk_min_eigenvals_flag` asserts
only `is_finite()` and `>= 0.0`, so it passes with any factor. Add
`test_lk_min_eigen_matches_opencv_scale`, which uses a separable image
(for which the Scharr response is exact and one-dimensional) to derive a
closed-form `51200 * 2^-20 / 9`; the doubled divisor and the pre-fix
unscaled value both fail it.

Also update the reference block in the #130 Scharr test, which hardcoded
the window-area-only normalisation, and the `min_eigen_threshold` docs in
both `optical_flow.rs` and the wasm wrapper, which stated that thresholds
are not transferable to OpenCV -- with #130 and this change they are.

Closes #138

BREAKING CHANGE: `min_eigen_threshold` and the `err` values reported via
OPTFLOW_LK_GET_MIN_EIGENVALS are now on OpenCV's scale. OpenCV's `1e-4`
default transfers directly; thresholds tuned against earlier purecv
builds read about 2^20 times larger and must be retuned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Address Copilot review findings on PR #139:

- The migration factor was stated as `2^20`, ignoring that Scharr also
  multiplies the gradient matrix by 16 relative to the Sobel derivatives
  used before #130. Against a build predating both fixes the factor is
  `2^20 / 16 = 65536`. Corrected in `optical_flow.rs` and the wasm wrapper.
- `FLT_SCALE` is `pub(crate)`, so the intra-doc link to it from public
  API docs could not resolve; use plain code text instead.
- Derive the pinning test's expected value from a literal rather than
  from `FLT_SCALE`, so the test fails if that constant is ever changed
  instead of silently following it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulls the Newton-Raphson refinement loop out of lk_single_level into
a standalone pub(crate) function generic over a compute_mismatch
closure. Pure extraction, no behavior change (same test pass counts
before and after) -- done so the loop itself is directly unit-testable
without real image data, needed for #131's oscillation half-step fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hand-crafted mismatch closure that cancels exactly on iterations 0
and 1 (no real image data needed). Currently fails because lk_iterate
has no oscillation handling yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lk_iterate only broke on eta_u^2+eta_v^2 < eps, unlike OpenCV
(lkpyramid.cpp:620-626), which also detects when two consecutive
Newton steps nearly cancel and, in that case, undoes half of the
just-applied step instead of risking another full step that
overshoots again. An oscillating solve was therefore taking one
extra full step versus OpenCV, leaving a residual on the order of
~0.01-0.1px on high-contrast/repetitive texture.

Closes #131.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…131)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The oscillation-half-step test's doc comment pointed at 'this
function's own comment for the full trace', which didn't exist
anywhere -- found in final review. Replace it with the actual
iteration trace and the rationale for why this test uses a
hand-crafted closure instead of real image data (a systematic search
found no real-image scenario in this codebase that triggers the
branch, and OpenCV's own test suite has none either).

Also restores one-comment-per-argument on the lk_iterate(...) call,
which cargo fmt's reformat (c9f470c) had collapsed to trailing only
the last value of each 2-3-value group.

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

Pre-merge review of fix/lk-optical-flow into dev surfaced three
cross-cutting numbers that each individual fix's review couldn't
have caught, since each was locally correct at the time it landed:

- README.md: unit test count was 348 (post-#130), actual current
  count is 350 after #138 and #131 each added one more test.
- examples/optical_flow.rs: the save_flow_csv doc example's sample
  min_eigen value (88873.76) was on the pre-#138 scale; regenerated
  a real row from a fresh run (1.470835, on OpenCV's FLT_SCALE
  scale).
- .agents/MIRI_PLAN.md: #138 added a third test reaching the unsafe
  Scharr fast path via calc_optical_flow_pyramid_lk, measured at
  ~11.9s under Miri (std,simd) -- under the plan's own >30s
  exclusion threshold, so not ignored, matching the two #130 tests.
  Updated the "two more tests" acceptance-criteria line to three,
  and corrected a stale "293 tests" remaining-runtime count (dated
  from before #130/#131/#138 added tests) to the current 341.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#142)

Closed-form-verified regression test on a 3x3 near-degenerate window
(det=114620): currently fails because the determinant guard is still
on purecv's raw scale, not OpenCV's FLT_SCALE-scaled one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lk_single_level's degeneracy guard compared purecv's raw (unscaled)
det against f64::EPSILON, while OpenCV compares its FLT_SCALE-scaled
D against FLT_EPSILON -- roughly 10^21 looser. A rank-deficient
window (e.g. a clean 1-D edge with only a hairline of off-axis
texture) could pass purecv's guard and yield a near-singular
inv_det, a garbage flow estimate, and status = 1, where OpenCV would
mark the point lost.

det itself stays unscaled for the Newton solve a few lines below,
where FLT_SCALE cancels out algebraically -- only this one
comparison needed the factor applied.

Closes #142.

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

test_lk_rejects_near_degenerate_window's doc comment said the
effective raw determinant threshold was ~130795 -- a rounding
approximation. The exact value is f32::EPSILON / FLT_SCALE^2 =
2^-23 / 2^-40 = 2^17 = 131072, confirmed during task review.
Doc-only; det=114620 is comfortably below either number, so this
doesn't change the test's behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- README.md: unit test count was stale (350), actual current count
  is 351 -- this branch added one test.
- tests.rs: cleaned up awkward wrapping in the near-degenerate-window
  test's doc comment (introduced by the #142 threshold-value
  correction), and noted that det=114620 is specific to the Scharr
  kernel purecv uses (#130), making the comment self-verifying
  against a future derivative-operator change.
- optical_flow.rs: calc_optical_flow_pyramid_lk's docs now mention
  that a point can be marked lost via the determinant guard (matching
  OpenCV's D < FLT_EPSILON), not only via min_eigen_threshold.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OpenCV rejects a window when `D < FLT_EPSILON` on the signed,
FLT_SCALE-scaled determinant (lkpyramid.cpp:426). purecv compared
`det.abs()`, so a singular H whose determinant rounds to a large
negative value would pass the guard and be inverted. H is positive
semi-definite, so a negative det is always rounding noise.

Also names the effective raw threshold as `LK_DET_EPSILON`
(FLT_EPSILON / FLT_SCALE^2 = 2^17) instead of rescaling det inline on
every call, and makes the "tracking lost" debug log say which guard
fired and print the det threshold alongside the det value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_lk_rejects_near_degenerate_window only covers det below the
threshold. Add a companion 3x3 window with det = 139552, about 6.5%
above LK_DET_EPSILON = 2^17, that must still be tracked, so a future
over-rejecting guard (wrong FLT_SCALE power, flipped comparison) is
caught. Also asserts LK_DET_EPSILON == 131072 exactly.

README unit test count: 351 -> 352.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two regression tests on a period-3 texture that only exists at full
resolution: pyr_down's [1,4,6,4,1]/16 kernel has gain 1/16 at 2*pi/3,
so level 1's minimum eigenvalue is ~256x smaller than level 0's
(~0.005 vs 1.318 for a 9x9 window), and a threshold of 0.05 rejects
level 1 only.

- identical frames: the point must still be tracked (status = 1)
- 1-px shift with the true shift as initial flow: the guess must carry
  through the skipped level unchanged (x = 33.0); restarting level 0
  from zero only reaches ~32.61 on this texture

Both currently fail: calc_optical_flow_pyramid_lk marks the point lost
as soon as any level's window is degenerate.

README unit test count: 352 -> 354.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calc_optical_flow_pyramid_lk broke out of the pyramid loop and marked
the point lost whenever lk_single_level rejected any level's window.
OpenCV only clears status when the failing level is 0; at a coarser
level it skips refinement there and carries the propagated flow
estimate on to the finer levels.

lk_single_level already returns the propagated (u, v) unchanged on
rejection, so the fix is to stop breaking out and only clear `tracked`
at level 0. Documents the level-0-only behaviour on
min_eigen_threshold.

Closes #145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous version shifted the next frame by 1 px and relied on
level 0 only reaching u ~ 0.61 from a zero start. That figure is not a
property of the texture: it is 1 - (31/32)^30, a symptom of a separate
bug where each LK Newton step is 1/32 of the full step (It is not
scaled to match the Scharr-scaled gradients). Fixing that bug could
let a reset estimate converge anyway and silently disarm the test.

Use identical frames with a guess of one full texture period (3 px):
u = 3 and u = 0 are both exact zero-mismatch solutions, so level 0
stays where it starts. A carried guess gives x = 35 and a dropped or
reset one gives x = 32, whatever the step size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…144)

Closed-form fixtures on a quadratic bowl I = (x-16)^2 + (y-16)^2, whose
Scharr gradient is exactly 64 (x-16) and interpolates exactly:

- min_eigen (OPTFLOW_LK_GET_MIN_EIGENVALS) must be
  (min(W,H)^2 - 1) / 3072, i.e. exactly W x H samples at OpenCV's
  offsets k - (n-1)/2, normalised by W*H. The integer half-window
  currently gives 120/3072 for 10x10 (11x11 samples) and 48/3072 for
  10x6 (11x7).
- the tracking error (MAE, zero iterations) must be 10 * mean|dx|:
  25 for W = 10, currently 300/11 = 27.27.
- win_size below 3x3 must be rejected, like OpenCV's
  CV_Assert(winSize.width > 2 && winSize.height > 2).

Odd sizes (9x9) already match and are included as a guard.

README unit test count: 354 -> 357.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nCV (#144)

The LK window used an integer half-size (win_size / 2) and inclusive
loops, so it always sampled an odd 2*(n/2)+1 pixels per side: an even
win_size of 10 sampled 11, and the eigenvalue was normalised by that
area instead of the requested one.

Adopt OpenCV's convention, halfWin = (winSize - 1) * 0.5: exactly
width x height samples at offsets k - half for k = 0..n, which are
half-integers (bilinearly interpolated) for even sizes. A small
TrackingWindow helper owns the convention, and all five sampling loops
(H accumulation and both Newton closures, scalar and SIMD, plus the
tracking error) iterate its offsets in the same row-major order, so the
SIMD gather buffers stay aligned. win_area is now W*H.

Odd sizes produce the same samples as before. win_size below 3x3 is
now rejected with InvalidInput, matching OpenCV's assertion; with exact
sampling a non-positive size would otherwise give an empty window and a
NaN eigenvalue.

Closes #144.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_lk_single_newton_step_is_exact_on_quadratic_bowl: on the
  quadratic bowl translated by an integer (dx, dy), one Newton step over
  a symmetric window is exactly (dx, dy) in closed form
  (32 * 128 / 4096). It is currently (dx, dy) / 32 = 0.03125 px.
- test_lk_recovers_subpixel_shift_with_opencv_defaults: a smooth
  texture shifted by (0.6, -0.4) must be recovered to within 0.02 px
  with OpenCV's calcOpticalFlowPyrLK defaults. It currently comes out
  as (0.35, -0.23).
- test_lk_pure_translation_x: tighten the tolerance from +/-1.5 px to
  +/-0.05 px. The loose bound hid this bug (the estimate is 2.84 for a
  3 px shift).

README unit test count: 357 -> 359.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
H and b are built from Scharr derivatives, which are 32x the true image
gradient, but the temporal difference It = I2 - I1 in b was the raw
intensity difference. So H scaled by 32^2 and b by 32, and every Newton
step H^-1 b was 1/32 of the true step: with OpenCV's defaults a clean
1 px shift was tracked as ~0.70 px. The tiny steps also tripped the
oscillation fallback (#131) on ordinary same-direction steps.

OpenCV keeps its window samples at x32 fixed-point
(CV_DESCALE(..., W_BITS1-5)), so its diff carries the same factor and
it cancels. Do the equivalent here: scale the accumulated mismatch
vector b by SCHARR_GAIN = 32 in both the scalar and SIMD closures
(scaling the sum once instead of It per sample). H is untouched, so
min_eigen keeps its OpenCV-parity scale (#130, #138).

Closes #149.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kalwalt and others added 2 commits September 27, 2026 00:18
…TOKEN (#129)

The v0.8.0 release's publish-npm job failed with EOTP ("This operation
requires a one-time password"): npm accepted NPM_TOKEN but refused to
publish without 2FA, so 0.8.0 had to be published by hand. crates.io
had already been published by then, leaving a half-finished release.

Switch the job to npm Trusted Publishing, as laid out in #129:

- job-level `permissions: { contents: read, id-token: write }`
- actions/setup-node@v7 with Node 24, plus `npm install -g npm@11` and
  a guard that fails fast if npm < 11.5.1 (trusted publishing's minimum;
  Node 24.18.0 bundles npm 10.9.4)
- drop the `npm config set //registry.npmjs.org/:_authToken` line
- `npm publish --access public --provenance`

Needs a Trusted Publisher configured on npmjs.com for
@webarkit/purecv-wasm (owner webarkit, repository purecv, workflow
release.yml, no environment) before the next tag push.

Closes #129.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bump version 0.8.0 -> 0.9.0 in Cargo.toml ([package] and
  [workspace.package]) and root package.json. crates/wasm/Cargo.toml
  inherits via version.workspace = true.
- crates/wasm/pkg/package.json: bump its version line. This is exactly
  what `npm run build` would regenerate: none of its inputs (Cargo
  metadata, crates/wasm/README.md, build scripts, LICENSE) changed
  since v0.8.0, and v0.8.0's regeneration differed from 0.7.1 only in
  this line. (wasm-pack isn't installed on this machine.)
- README.md: bump the five hardcoded `purecv = "0.8"` install-snippet
  versions to "0.9" (crates/wasm/README.md has none).
- Generate the v0.9.0 changelog entry via git-cliff, plus the blank line
  before the previous version heading that --prepend omits.

Minor bump (0.8.0 -> 0.9.0): this release brings calc_optical_flow_pyramid_lk
to OpenCV parity (#130, #131, #138, #142, #144, #145, #149), which
changes tracking results for existing callers (eigenvalue scale, flow
estimates, even window sizes, win_size < 3 now rejected), and moves npm
publishing to trusted publishing (#129).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Release PureCV 0.9.0 with corrected LK tracking and npm publishing

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

Grey Divider

AI Description

• Align pyramidal Lucas–Kanade tracking with OpenCV, changing flow estimates and threshold behavior.
• Add regression tests for derivative scaling, window sampling, degeneracy, and convergence.
• Release version 0.9.0 and switch npm publishing to OIDC with provenance.
Diagram

graph TD
  A["Frame pairs"] --> B["Image pyramids"] --> C["Scharr gradients"] --> D["Window samples"] --> E{"Gradient valid?"}
  E -->|valid| F["LK refinement"] --> G["Flow and status"]
  E -->|coarse skip| D
  E -->|finest lost| G
  F -->|next level| D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Replicate OpenCV fixed-point sampling
  • ➕ Could provide closer numerical agreement with OpenCV's internal LK calculations.
  • ➖ Adds substantial complexity to the existing floating-point scalar and SIMD paths.
  • ➖ Requires a broader rewrite than the identified tracking corrections.

Recommendation: Keep the existing floating-point tracker and correct its Scharr, temporal-difference, and guard scales as this PR does. Consider fixed-point sampling only if future requirements call for closer numerical agreement than the regression tests establish.

Files changed (13) +1142 / -146

Bug fix (1) +277 / -111
optical_flow.rsCorrect pyramidal LK calculations and tracking decisions +277/-111

Correct pyramidal LK calculations and tracking decisions

• Uses Scharr derivatives, OpenCV-scaled eigenvalue and determinant checks, and exact-sized tracking windows. Restores full Gauss–Newton steps, adds an oscillation fallback, and lets points survive degenerate coarse levels while rejecting windows smaller than 3×3.

src/video/optical_flow.rs

Tests (1) +754 / -7
tests.rsPin LK behavior with numerical regression tests +754/-7

Pin LK behavior with numerical regression tests

• Adds closed-form and end-to-end tests for Scharr derivatives, eigenvalue scaling, determinant boundaries, coarse-level propagation, even and non-square windows, oscillation, and Newton-step accuracy. Tightens an existing translation-accuracy assertion.

src/video/tests.rs

Documentation (7) +85 / -22
MIRI_PLAN.mdReconcile Miri coverage notes with the Scharr tests +12/-8

Reconcile Miri coverage notes with the Scharr tests

• Updates the test-count and runtime rationale for Miri exclusions. Explains that retained Scharr tests cover the derivative fast path despite the slow pyramid test remaining excluded.

.agents/MIRI_PLAN.md

CHANGELOG.mdDocument the PureCV 0.9.0 release +48/-0

Document the PureCV 0.9.0 release

• Adds the 0.9.0 entry covering LK fixes, the threshold-scale breaking change, regression tests, documentation, and npm publishing.

CHANGELOG.md

README.mdRefresh installation examples and test count +6/-6

Refresh installation examples and test count

• Changes Rust dependency examples to version 0.9 and reports 359 unit tests.

README.md

benchmark_results.mdDescribe Scharr-based pyramid benchmark behavior +7/-4

Describe Scharr-based pyramid benchmark behavior

• Replaces stale Sobel references with Scharr and notes that both use the same derivative fast path.

benches/benchmark_results.md

video_bench.rsCorrect the pyramid benchmark description +1/-1

Correct the pyramid benchmark description

• Identifies the derivative work measured by the existing pyramid benchmark as Scharr rather than Sobel.

benches/video_bench.rs

lib.rsDocument WASM optical-flow derivative and threshold behavior +10/-2

Document WASM optical-flow derivative and threshold behavior

• Identifies pyramid derivatives as Scharr and documents the OpenCV-compatible minimum-eigenvalue scale and migration guidance for older thresholds.

crates/wasm/src/lib.rs

optical_flow.rsRefresh the optical-flow CSV example +1/-1

Refresh the optical-flow CSV example

• Adjusts the sample tracked coordinates and eigenvalue to reflect the corrected LK results.

examples/optical_flow.rs

Other (4) +26 / -6
release.ymlPublish npm package through trusted OIDC credentials +22/-2

Publish npm package through trusted OIDC credentials

• Grants the npm job OIDC permission, installs Node 24 and a verified npm version of at least 11.5.1, and publishes with provenance. Removes the long-lived NPM_TOKEN configuration.

.github/workflows/release.yml

Cargo.tomlBump Rust package and workspace versions to 0.9.0 +2/-2

Bump Rust package and workspace versions to 0.9.0

• Sets both the root package and shared workspace package version to 0.9.0.

Cargo.toml

package.jsonSet the publishable WASM package version to 0.9.0 +1/-1

Set the publishable WASM package version to 0.9.0

• Bumps the generated npm package manifest from 0.8.0 to 0.9.0.

crates/wasm/pkg/package.json

package.jsonBump the root npm package version to 0.9.0 +1/-1

Bump the root npm package version to 0.9.0

• Sets the private root package manifest version to 0.9.0.

package.json

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Large tracking windows panic on overflow 📘 Rule violation ≡ Correctness
Description
TrackingWindow::area multiplies two i32 window dimensions before converting the result to
usize, without checking for overflow. A non-empty tracking request with win_size of
1_073_741_824 by 3 passes the new minimum-size check but panics when the SIMD path sizes its
sample buffers in debug builds.
Code

src/video/optical_flow.rs[308]

+        (self.width * self.height) as usize
Evidence
The new validation rejects only dimensions below three, while the new area calculation multiplies
accepted dimensions as i32; the SIMD path calls it before sampling.

Rule 2965956: Library functions that can fail must return Result<T, PureCvError>
src/video/optical_flow.rs[747-753]
src/video/optical_flow.rs[306-309]
src/video/optical_flow.rs[391-405]

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

## Issue description
Accepted window dimensions can overflow the area calculation and panic during tracking.
## Fix Focus Areas
- src/video/optical_flow.rs[306-309]
- src/video/optical_flow.rs[747-753]
## Recommended Fix
Compute the area with checked arithmetic during input validation, return a `PureCvError` if it is not representable, and use the validated area throughout tracking.

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


2. New solver silences a Clippy warning 📘 Rule violation ✧ Quality
Description
lk_iterate adds #[allow(clippy::too_many_arguments)] rather than changing the new helper's
argument structure. The suppression applies whenever the solver is linted, so changes to its growing
parameter list will not be checked by that lint.
Code

src/video/optical_flow.rs[563]

+#[allow(clippy::too_many_arguments)]
Evidence
The changed line introduces a Clippy allow attribute on the new solver, which the checklist
explicitly prohibits adding to silence a lint.

Rule 2965968: Do not introduce new Clippy warnings in Rust code
src/video/optical_flow.rs[563-574]

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 solver suppresses a Clippy lint on its argument count.
## Fix Focus Areas
- src/video/optical_flow.rs[563-574]
## Recommended Fix
Group related solver parameters into small structs, update both callers and the direct test, and remove the new allow attribute.

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


3. One-pixel tracking accuracy goes untested 📎 Requirement gap ≡ Correctness
Description
test_lk_recovers_subpixel_shift_with_opencv_defaults uses a (0.6, -0.4) shift and accepts error
below 0.02 pixels rather than checking a one-pixel shift to approximately 0.01 pixels. The
single-step bowl test checks the step separately, but this end-to-end test can pass without
establishing the specified one-pixel accuracy under the default tracking parameters.
Code

src/video/tests.rs[1115]

+        let (sx, sy) = (0.6f32, -0.4f32);
Evidence
The checklist calls for an end-to-end smooth-texture one-pixel accuracy check; the added test
instead supplies a 0.6-pixel horizontal shift and a 0.02-pixel tolerance.

Test LK step size and tracking accuracy
src/video/tests.rs[1108-1139]

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 end-to-end regression test does not establish the specified one-pixel tracking accuracy.
## Fix Focus Areas
- src/video/tests.rs[1108-1139]
## Recommended Fix
Add a smooth-texture one-pixel shift case using the OpenCV default tracking parameters and assert accuracy of approximately 0.01 pixels. Keep the subpixel case if it provides separate coverage.

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


View medium (1)
4. Edge features are marked lost with even windows 🐞 Bug ≡ Correctness
Description
TrackingWindow::offsets emits negative half-integer coordinates for even-sized windows, but
bilinear_interp clamps only the base pixel index, leaving weights that blend the border pixel with
an interior pixel instead of replicating the border. At the left or top edge, those samples affect
the gradient matrix, flow refinement, and tracking error; for a 4×4 window at (0, 0) on an x + y
image, they lower the minimum eigenvalue below the default 1e-4 threshold when border-replicated
sampling would keep it above.
Code

src/video/optical_flow.rs[R316-317]

+            let dy = ky as f32 - self.half_h;
+            (0..self.width).map(move |kx| (kx as f32 - self.half_w, dy))
Evidence
An even-sized window at an integer-valued corner produces offsets including -1.5 and -0.5. For
x = -0.5, the interpolator clamps the floor-derived base index to x0 = 0 but retains ax = 0.5
from the out-of-range coordinate, blending pixels 0 and 1 rather than returning pixel 0. The
sampling loops use these interpolated values in the derivative matrix used for minimum-eigenvalue
rejection, Newton mismatch accumulation, and final tracking error.

src/video/optical_flow.rs[295-318]
src/video/optical_flow.rs[325-347]
src/video/optical_flow.rs[413-457]
src/video/optical_flow.rs[615-626]
src/video/optical_flow.rs[281-318]
src/video/optical_flow.rs[325-346]
src/video/optical_flow.rs[399-404]
src/video/optical_flow.rs[496-504]
src/video/optical_flow.rs[621-626]

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

## Issue description
Even-sized LK windows generate negative half-integer sample coordinates near the top and left image borders. `bilinear_interp` clamps the base pixel index but derives interpolation weights from the out-of-range coordinate, causing border samples to blend with adjacent interior pixels and changing tracking decisions.
## Fix Focus Areas
- src/video/optical_flow.rs[315-318]
- src/video/optical_flow.rs[325-347]
- src/video/tests.rs[905-950]
## Recommended Fix
Clamp the floating-point `x` and `y` sampling coordinates to valid image bounds before computing their floor indices, neighboring indices, and interpolation weights. Add regression coverage for an even-sized window at the top-left image corner: verify that a negative half-integer sample uses the border pixel rather than blending with its interior neighbor, and check the resulting minimum eigenvalue or tracking status.

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


Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/video/optical_flow.rs
Comment thread src/video/optical_flow.rs
Comment thread src/video/tests.rs
Comment thread src/video/optical_flow.rs
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.

1 participant