Skip to content

fix(video): scale LK's temporal difference to the Scharr gain - #150

Merged
kalwalt merged 2 commits into
devfrom
fix/149-lk-newton-step-scale
Sep 23, 2026
Merged

kalwalt merged 2 commits into
devfrom
fix/149-lk-newton-step-scale

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 23, 2026

Copy link
Copy Markdown
Member

Closes #149.

Problem

H and b are built from Scharr derivatives, which are 32× the true image gradient. But the temporal difference It = I2 − I1 in b was the raw intensity difference, so H ∝ 32² while b ∝ 32, and every Newton step H⁻¹b was 1/32 of the true step. With OpenCV's calcOpticalFlowPyrLK defaults, a clean 1 px shift was tracked as ≈0.70 px. The tiny steps also set off the oscillation fallback (#131) on ordinary same-direction steps, which stopped the solve early. Measurements are in #149.

Fix

OpenCV keeps its window samples at ×32 fixed-point (CV_DESCALE(..., W_BITS1-5)), so its diff carries the same factor as the derivatives and the factors cancel. The equivalent here is a new SCHARR_GAIN = 32 constant (documented next to FLT_SCALE) that scales the accumulated mismatch vector b, in both the scalar and SIMD closures. Scaling the sum once is equivalent to scaling It in every sample, and costs 2 multiplies per iteration instead of one per sample. H is untouched, so min_eigen keeps its OpenCV-parity scale (#130, #138), and lk_iterate (with its synthetic-mismatch tests from #131) is unchanged.

Behaviour change: every caller of calc_optical_flow_pyramid_lk now gets noticeably different, correct flow estimates. It usually converges in fewer iterations, too.

Tests

test fixed unfixed dev
test_lk_single_newton_step_is_exact_on_quadratic_bowl: one Newton step on the quadratic bowl translated by (1,0), (0,−1), (2,1); exact closed form 32·128/4096·(dx,dy) = (dx,dy) exact (dx,dy)/32, e.g. 0.03125
test_lk_recovers_subpixel_shift_with_opencv_defaults: smooth texture shifted by (0.6, −0.4), 21×21 window, maxLevel 3, 30 iterations / 0.01, 1e-4; tolerance 0.02 px (0.6076, −0.3980) (0.350, −0.230)
test_lk_pure_translation_x (existing): tolerance tightened from ±1.5 to ±0.05 px; the loose bound had hidden this bug 3.00 2.84

Results are identical with and without simd. No other test, example or doc tolerance needed retuning.

Checks

  • cargo fmt --check passes.
  • cargo clippy -D warnings is clean with default features, --no-default-features and --features simd,parallel.
  • cargo test: 359 unit tests and 40 doc-tests; 415 with simd,parallel; 359 with --no-default-features --features std.
  • The WASM crate builds.

🤖 Generated with Claude Code

kalwalt and others added 2 commits September 23, 2026 16:46
- 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>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Scale LK temporal differences to the Scharr derivative gain

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Correct LK Newton-step magnitude by matching temporal differences to the 32× Scharr derivative
 scale.
• Apply identical scaling across scalar and SIMD optical-flow paths.
• Add exact-step and subpixel regressions while tightening translation accuracy.
Diagram

graph TD
    A["Image Windows"] --> B["Scharr Gradients"] --> C["H Matrix"] --> G["Newton Solve"]
    A --> D{"Execution Path"} -->|SIMD| E["SIMD Mismatch"] --> F["Scale b ×32"] --> G
    D -->|Scalar| H["Scalar Mismatch"] --> F
Loading
High-Level Assessment

Scaling the accumulated mismatch vector once is the preferred approach: it restores the correct Gauss-Newton step while preserving H, minimum-eigenvalue thresholds, and OpenCV parity. Scaling every temporal sample would be equivalent but more expensive, while normalizing Scharr derivatives would unnecessarily alter established eigenvalue and determinant scales.

Files changed (3) +129 / -6

Bug fix (1) +18 / -2
optical_flow.rsCorrect LK mismatch scaling in scalar and SIMD paths +18/-2

Correct LK mismatch scaling in scalar and SIMD paths

• Introduces the documented 32× Scharr gain and applies it to the accumulated mismatch vector in both execution paths. This restores full-size Newton steps without changing the Hessian or minimum-eigenvalue scale.

src/video/optical_flow.rs

Tests (1) +110 / -3
tests.rsAdd LK Newton-step and subpixel accuracy regressions +110/-3

Add LK Newton-step and subpixel accuracy regressions

• Adds translated quadratic-bowl and smooth-texture fixtures that verify exact one-step behavior and OpenCV-default subpixel tracking. Tightens the existing horizontal translation tolerance from 1.5 pixels to 0.05 pixels.

src/video/tests.rs

Documentation (1) +1 / -1
README.mdUpdate documented unit-test count +1/-1

Update documented unit-test count

• Increases the documented unit-test total from 357 to 359 to include the new LK regressions.

README.md

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@kalwalt kalwalt self-assigned this Sep 23, 2026
@kalwalt kalwalt added bug Something isn't working enhancement New feature or request rust-code rust Pull requests that update rust code tests video-module labels Sep 23, 2026
@kalwalt
kalwalt merged commit 0d6582a into dev Sep 23, 2026
6 checks passed
@kalwalt kalwalt mentioned this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request rust Pull requests that update rust code rust-code tests video-module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant