fix(bfmatcher,index): reject mismatched descriptor widths and export the new types - #184
Conversation
…the new types Three findings from the Qodo review on #183, all of which would otherwise ship in 0.15.0. bfmatcher silently corrupted matches on mismatched widths. match() and knnMatch() derived one stride from query.cols and used it to address BOTH matrices, so a train set of a different width was read at offsets computed from the query. The reads walk across train row boundaries and, past the end, an out-of-range Int32Array index yields undefined, which XOR coerces to 0. Nothing threw: the matcher returned a full set of confident, meaningless Hamming distances. The existing check only verified each width was a multiple of 4 and never compared the two. A new pairWords() validates the pair in one place for both methods. The new public types were unreachable. src/index.ts re-exports the data structures so consumers can annotate without deep paths, but match_t/IMatch_T and pose_t/IPose_T were never added, and the exports map only exposes the root and package.json - so deep imports cannot compensate. There was no way for a consumer to name those types at all. This is not hypothetical: the CvBackend adapter being written against this library needs both. pose_estimator allocated its B scratch every call. An estimator is built once and reused across frames, so at 30-60 fps that was a fresh array per frame; it is now an instance field. The review suggested the shared cache instead, which is the wrong tool here - that pool is for image-sized buffers, and balancing a get/put across the degenerate early return to save 72 bytes costs more in bookkeeping than it saves. Two further findings in the same review - that pose_estimator must extend core and must not require `new` - are rejected. It is a stateful class constructed with a K, so it belongs with matrix_t and keypoint_t rather than the stateless algorithm singletons, and its static intrinsics() factory depends on the class itself sitting on the namespace. AGENTS.md listed the constructor classes without mentioning it, which is what invited the misreading, so it now says so explicitly. Tests cover the new guard on both methods, that the message names both widths, and a reproduction of the wrong distances the old stride produced.
PR Summary by QodoValidate BFMatcher widths and expose matcher and pose types
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The width guard added in the previous commit compared `cols`, which does not determine a descriptor row's storage. `matrix_t.allocate` sizes its buffer as `cols * sizeof(type) * channel * rows`, so a U8/C1 and a U8/C2 both at cols = 32 occupy 32 and 64 bytes per row and passed the check unchanged - then hit exactly the stride corruption the guard was written to prevent, just via the channel axis instead of the column one. An element-type difference slips through the same way. Compares rowBytes() now. Also requires both matrices to be U8: Hamming over an i32 view is only meaningful for packed bytes, and a F32 matrix would have its float bit patterns XORed and popcounted into numbers unrelated to descriptor similarity. That matches what the method already documented and what orb.describe produces, so no supported call site changes. The multiple-of-4 check moves to the row width for the same reason; it too was reading cols, and would have rejected a valid 2-column/2-channel row while accepting rows it should not. Found by the Qodo review on this PR - the one finding of the three that was correct.
Merges dev and regenerates dist/ and types/, so the published artifacts carry
the correctness fixes rather than the build made before them:
- bfmatcher now rejects mismatched descriptor row widths instead of reading
train rows at a query-derived stride and returning silently wrong Hamming
distances
- match_t/IMatch_T and pose_t/IPose_T are reachable from the package root,
which they were not - with the exports map limited to the root, consumers
had no way to name the types of two of the modules this release exists to
publish
- pose_estimator no longer allocates its B scratch per frame
Also refreshes the 0.15.0 changelog section to cover those commits, keeping
CHANGELOG.md in agreement with the notes the tag workflow generates.
Merges dev and regenerates dist/ and types/, so the published artifacts carry
the correctness fixes rather than the build made before them:
- bfmatcher now rejects mismatched descriptor row widths instead of reading
train rows at a query-derived stride and returning silently wrong Hamming
distances
- match_t/IMatch_T and pose_t/IPose_T are reachable from the package root,
which they were not - with the exports map limited to the root, consumers
had no way to name the types of two of the modules this release exists to
publish
- pose_estimator no longer allocates its B scratch per frame
Also refreshes the 0.15.0 changelog section to cover those commits, keeping
CHANGELOG.md in agreement with the notes the tag workflow generates.
Addresses the Qodo review on #183. Kept separate from the release PR: these are library bugs, not release work, so they belong in
devon their own and land in the changelog under the right heading.These block the 0.15.0 tag. Two of the three affect the published surface of exactly the modules 0.15.0 exists to ship.
Fixed
bfmatchersilently corrupted matches on mismatched widths 🐞match()andknnMatch()derived one stride fromquery.colsand used it to address both matrices:With
train.cols !== query.colsthe train reads walk across row boundaries, and past the end an out-of-rangeInt32Arrayindex yieldsundefined— which XOR coerces to0. So nothing threw: the matcher returned a complete set of confident, meaningless Hamming distances, and the caller's RANSAC just never found consensus.The pre-existing
words()check only verified each width was a multiple of 4; it never compared them. The docs said "same row width asquery" but nothing enforced it. A newpairWords()validates the pair once, for both methods.Worth noting this is the same failure mode the
CvBackendcontract guards withDescriptorMismatchError— the library underneath had the same hole one level down.The new public types were unreachable 🐞
src/index.tsre-exports the data structures so consumers can annotate without deep paths (#92), butmatch_t/IMatch_Tandpose_t/IPose_Twere never added. Since the exports map only exposes.and./package.json, a deep import cannot compensate either — there was no way for a consumer to name those types.Not hypothetical: the
CvBackendadapter being written against this library inwebarkit/webarkitneeds both.pose_estimatorallocated its scratch every frame ⚡B(theK⁻¹·Hproduct) was a freshFloat64Array(9)per call. An estimator is constructed once and reused across frames, so at 30–60 fps that is one allocation per frame for no benefit; it is now an instance field.The review proposed borrowing from the shared cache instead. That is the wrong tool here — the pool exists for image-sized buffers, and balancing a
get_buffer/put_bufferacross the degenerate early return to save 72 bytes costs more in bookkeeping than it saves.Rejected, with the cause fixed
Two findings claim
pose_estimatorviolates the module rules: that it must extendcore, and that it must not requirenew.Both misclassify it. It is stateful — it holds
K⁻¹— and the public API constructs it with aK, so it belongs withmatrix_t,keypoint_tandransac_params_t, whichAGENTS.mdestablishes remain constructors. Itsstatic intrinsics()factory also requires the class itself to sit on the namespace; a singleton instance could not expose it.The root cause is worth fixing though:
AGENTS.mdlisted the constructor classes without mentioningpose_estimatorormatch_t, which is precisely what invited the misreading. It now names them and states the rationale, so the next reviewer — bot or human — does not repeat it.Verification
npm test— 308 passed (305 + 3 new)npm run typecheck,format-check,license-check— cleantypes/src/index.d.tsnow carries both new type exportsdist/andtypes/are deliberately not committed here, per the convention that they are rebuilt at release. #183 will regenerate them with these fixes included once this merges.Order
Merge this first, then update #183 (merge
devin, rebuild artifacts) before tagging0.15.0.