fix(fast_corners): index the per-row candidate buffer consistently (#202) - #203
Conversation
orb.describe parity was asserting that the two FAST detectors agree before comparing descriptors, which duplicated the fast_corners parity test and coupled the descriptor test to detector behaviour. Feed both implementations the same corner list so the test measures only orb.describe. Refs #202 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
detect() wrote each row's corner columns into the cpbuf scratch buffer 1-based and read them back 0-based. One never-written cell per row therefore fed non-maximum suppression, and since the shared pool only grows and never zeroes, that cell held whatever an earlier, unrelated call left behind - making detect's output depend on process history rather than on the image. The row's real last candidate was never read at all, so genuine corners were dropped. Write 0-based, matching the read. detect is now a pure function of its inputs and recovers the dropped corners (42 vs 35 on the parity scene). jsfeat has the same defect, so this is an intentional divergence: the cross-implementation assertion moves from tests/parity to tests/divergences, where it pins the relation that does still hold - jsfeatNext's corners are a superset of jsfeat's, structurally, because cpbuf only enumerates candidates while every suppression decision reads scores the defect never touched. Closes #202 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs #202 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…set relation Refs #202 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
fast_corners.detect's off-by-one fix (#202) makes jsfeatNext's corner set a structural superset of jsfeat's rather than an equal one, so the bench smoke check's strict count-equality guards now fail on every run. - bench/detectors.bench.ts: fast_corners.detect IS the measured workload, so both sides must keep detecting independently. Replace the equality guard with containment (next >= orig) plus a bounded excess tolerance (5%, headroom over the ~0.7%/~2.8% observed at thresholds 20/60), so the guard still catches a genuine workload divergence. yape06/yape keep strict equality, unaffected by #202. - bench/optical_flow_lk.bench.ts: seed points are an input to the measured workload (optical_flow_lk.track), not the thing under test, so derive them once from jsfeatNext and feed the same points to both sides -- mirroring the ORB parity fix in 3a7f8ec -- making the equality guard unnecessary rather than merely relaxed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Follow-ups from the review of this PR, now tracked:
Also note the CI fix in 3a08720: the |
The defect
fast_corners.detectwrote each image row's corner columns into thecpbufscratch buffer 1-based and read them back 0-based one row later:Two consequences, on every image:
cpbuf[cornerpos + 0]was read without ever being written. The buffer comes from the shared pool, andget_bufferonly ever grows a node — it never zeroes one. That cell therefore held whatever an earlier, unrelated call left behind, sodetect's output depended on process history rather than on the image.(2) is why the same image with the same options could return different corner sets across calls. It is only observable when the recycled cell happens to hold a small non-negative integer — any other value indexes
bufout of bounds, yieldsundefined, and fails every comparison — which is why the defect looked intermittent rather than constant.The fix
Write 0-based, matching the read — the two lines swapped, in both the darker (
d & 1) and brighter (d & 2) branches.detectis now a pure function of(image, border, threshold)and recovers the dropped corners. No API change.The row's count cell
cpbuf[cornerpos + w]is also safer than before: the highest candidate index now written isw - 7, strictly below it.Why this is an intentional divergence, not a parity regression
The vendored jsfeat oracle carries the identical defect, so the correct behaviour cannot be bit-parity with jsfeat. Following the pattern already used for #102, #114, #119, #139 and #186, the cross-implementation assertion moves out of
tests/parity/intotests/divergences.test.ts.What replaces equality is containment: jsfeatNext's corner set always contains jsfeat's. That is structural, not an artefact of the chosen scenes. jsfeat emits a corner only if
score = buf[prev+j]is strictly greater than all eight neighbours. For a phantom columnjthat was not a real candidate of that row,buf[prev+j] === 0and0 > 0is false; for ajout of range, every comparison againstundefinedis false. So a phantom can pass suppression only if it is already a real candidate of that row — i.e. already in jsfeatNext's list.cpbufonly enumerates candidates; every suppression decision reads scores out ofbuf, which the defect never touched.Measured counts: 33 corners vs jsfeat's 27 on the divergence test's scene (threshold 20, border 3). The containment relation is asserted across seven scene seeds, so jsfeat's score arithmetic is still cross-checked bit-for-bit on the common subset — on seven scenes now, where the old parity test used one.
Coverage
Net stronger than what was removed. Three invariants are new, and each of them fails on the unfixed code (verified by reverting the swap):
detectis independent of what the shared buffer pool contains (poisons all 30 pool nodes with a plausible column index — note that poisoning with0, or with a byte fill, is invisible).detectdoes not drop the last corner candidate of a row.Plus a purity check across repeated calls with an unrelated
detectin between. Full suite: 356 tests, 28 files, green.Audit answers (the two questions in #202)
Do any fixtures carry expected values produced by the current
detect? No. Everyfast_cornerscomparison in this repository runs live against the vendored oracle; nothing static pinsdetectoutput. This change alters no committed fixture — only the six files in this diff.Does any other module share the pattern? No. All 44
get_buffercall sites across 10 files insrc/were walked: every borrowed buffer is fully written before it is read, and everyget_bufferis balanced by aput_bufferon every exit path.Notes for the reviewer
dist/is deliberately not rebuilt here. Whether a fix PR rebuildsdistis a release-convention decision; it needs a rebuild at release time.detectwrites into the caller'scornerspool without checkingcorners.length. Recovering the dropped corners raises the count, which makes that pre-existing hazard marginally more reachable.Closes #202
Unblocks step 4 of webarkit/webarkit#22, tracked in webarkit/webarkit#27.
🤖 Generated with Claude Code