Fact
bilinear_interp (src/video/optical_flow.rs) clamps the base index to the image, but computes the interpolation weight from the unclamped coordinate:
let x0 = (x.floor() as i32).clamp(0, cols as i32 - 1) as usize;
let x1 = (x0 + 1).min(cols - 1);
let ax = x - x.floor(); // from the *unclamped* x
For a sample left of (or above) the image this blends the border pixel with its interior neighbour instead of replicating the border:
sample x |
floor |
x0, x1 |
ax |
result |
border-replicate would give |
| −0.5 |
−1 |
0, 1 |
0.5 |
0.5·p0 + 0.5·p1 |
p0 |
| −1.5 |
−2 |
0, 1 |
0.5 |
0.5·p0 + 0.5·p1 |
p0 |
| −1.0 |
−1 |
0, 1 |
0.0 |
p0 |
p0 |
The right/bottom edge is unaffected: there x0 = x1 = cols − 1, so any weight yields the border pixel. The behaviour is therefore asymmetric between edges, and at the top/left it matches neither replicate nor any other standard border mode. The same applies to y.
When it triggers
Any LK sample that lands at a negative non-integer coordinate:
Affected quantities: the gradient matrix H (so min_eigen and the determinant guard, and with them the lost/tracked decision), the Newton mismatch b, and the tracking error. Qodo's review of #153 gives a concrete case: a 4×4 window at (0, 0) on an x + y ramp gets a minimum eigenvalue below the default 1e-4 threshold, where border-replicated sampling would keep it above.
Suggested fix
Clamp the coordinate before deriving indices and weights, which gives consistent border replication on all four edges:
let x = x.clamp(0.0, (cols - 1) as f32);
let y = y.clamp(0.0, (rows - 1) as f32);
// then floor / x0 / x1 / ax exactly as today
Regression tests (hand-derivable):
- a sample at
x = −0.5 (and −1.5) must equal the border pixel p0;
- an even window (e.g. 4×4) at an integer corner point:
min_eigen / status must match border-replicated sampling;
- the existing right/bottom behaviour must stay unchanged.
Related: OpenCV parity at the border (separate, larger)
Clamping gives replicate semantics, which fixes the inconsistency but is not full OpenCV parity. As far as I know, OpenCV's calcOpticalFlowPyrLK samples from pyramid levels padded by winSize with border extrapolation (BORDER_REFLECT_101 for the image) and marks a point lost at level 0 when its window falls outside the padded area (iprevPt.x < -winSize.width || ... in lkpyramid.cpp). This should be checked against the source before acting on it. Worth its own issue if exact border parity matters; the fix above stands on its own.
Found in Qodo's review of the v0.9.0 release PR (#153, finding 4). Verified by reading bilinear_interp on dev (ef13ed8).
Fact
bilinear_interp(src/video/optical_flow.rs) clamps the base index to the image, but computes the interpolation weight from the unclamped coordinate:For a sample left of (or above) the image this blends the border pixel with its interior neighbour instead of replicating the border:
xfloorx0,x1ax0.5·p0 + 0.5·p1p00.5·p0 + 0.5·p1p0p0p0The right/bottom edge is unaffected: there
x0 = x1 = cols − 1, so any weight yields the border pixel. The behaviour is therefore asymmetric between edges, and at the top/left it matches neither replicate nor any other standard border mode. The same applies toy.When it triggers
Any LK sample that lands at a negative non-integer coordinate:
px = 0.3with an 11-wide window samples at −4.7 …).win_sizeis even, because the window offsets are half-integers (k − (n−1)/2).Affected quantities: the gradient matrix
H(somin_eigenand the determinant guard, and with them the lost/tracked decision), the Newton mismatchb, and the tracking error. Qodo's review of #153 gives a concrete case: a 4×4 window at (0, 0) on anx + yramp gets a minimum eigenvalue below the default1e-4threshold, where border-replicated sampling would keep it above.Suggested fix
Clamp the coordinate before deriving indices and weights, which gives consistent border replication on all four edges:
Regression tests (hand-derivable):
x = −0.5(and−1.5) must equal the border pixelp0;min_eigen/statusmust match border-replicated sampling;Related: OpenCV parity at the border (separate, larger)
Clamping gives replicate semantics, which fixes the inconsistency but is not full OpenCV parity. As far as I know, OpenCV's
calcOpticalFlowPyrLKsamples from pyramid levels padded bywinSizewith border extrapolation (BORDER_REFLECT_101for the image) and marks a point lost at level 0 when its window falls outside the padded area (iprevPt.x < -winSize.width || ...inlkpyramid.cpp). This should be checked against the source before acting on it. Worth its own issue if exact border parity matters; the fix above stands on its own.Found in Qodo's review of the v0.9.0 release PR (#153, finding 4). Verified by reading
bilinear_interpondev(ef13ed8).