Skip to content

feat(orb): expose ic_angle, the keypoint orientation step (#96) - #182

Merged
kalwalt merged 1 commit into
devfrom
feat/96-adapter-readiness
Aug 29, 2026
Merged

feat(orb): expose ic_angle, the keypoint orientation step (#96)#182
kalwalt merged 1 commit into
devfrom
feat/96-adapter-readiness

Conversation

@kalwalt

@kalwalt kalwalt commented Aug 27, 2026

Copy link
Copy Markdown
Member

Part of #96 — the jsfeatNext-side work item ("stream A": keep the modules adapter-ready). The CvBackend contract and the adapter itself live in webarkit/webarkit and are not part of this PR.

The audit

Four of the five primitives the adapter delegates to are already adapter-ready — public, neutral-mappable I/O, no hidden state:

contract method jsfeatNext primitive status
match bfmatcher.match / knnMatch / ratio_test ready (matrix_t in, match_t[] out)
estimateHomography motion_estimator.ransac + homography2d ready (mask is a matrix_tUint8Array)
poseFromHomography pose_estimator.estimate ready (matrix_t + Float64Array)
detect fast_corners / yape / yape06 ready
describe orb.describe ready

Useful incidental finding for whoever writes the adapter: point_t and keypoint_t are structurally identical (x, y, score, level, angle), so a single pre-allocated pool can be passed to both fast_corners.detect (typed point_t[]) and orb.describe (typed keypoint_t[]).

The gap this PR closes

Keypoint orientation was missing from the library. ic_angle — the intensity-centroid measure that makes ORB descriptors rotation-invariant — existed only in the example pages, duplicated verbatim in sample_orb.html and sample_orb_pinball.html. Nothing under src/ ever assigned keypoint_t.angle.

This is the same shape of gap #133 closed for match_pattern.

Why it matters for #96: the contract's Keypoint type has an angle field and detect() is expected to return it populated. jsfeatNext's detectors never set it, so an out-of-repo adapter would have to reimplement the orientation itself — leaving PureCV's Rust port with no TypeScript oracle to cross-validate against, which is half the reason the jsfeatNext backend exists.

To be precise about severity: this is not a hard blocker. An adapter could pass angle = 0 and the code would run — it would just produce descriptors that are not rotation-invariant.

ic_angle now lives in orb as a public method, with the u_max table at module scope so the hot loop allocates nothing per call. Placement follows the ORB paper (the "o" in oFAST is that paper's own contribution) and OpenCV, which keeps IC_Angle in orb.cpp; bit_pattern_31 is the existing precedent for ORB-specific data living here.

Both examples now call the library method instead of carrying their own copy.

Documentation fix

examples/orb_test.html claimed angle = -1 lets ORB work out the patch orientation itself. That was never true: rectify_patch feeds the value straight into cos/sin, so -1 rotates the patch by −1 radian (≈ −57°) rather than leaving it upright. The page now fixes the angle at 0 — it only compares a patch against itself under a brightness change — and says why. describe's own docs now state that orientation is a required prior step, not an optional one.

Tests

10 new (295 → 305, all green).

  • Parity against the examples' inline implementation — the only available oracle, since original jsfeat also kept ic_angle in its sample rather than the library. Same arrangement as tests/parity/bfmatcher.test.ts.
  • Invariants: gradient direction (+x ramp → 0, +y ramp → π/2, diagonal → π/4), brightness invariance, atan2(0,0) returning 0 rather than NaN on a flat patch, and range.
  • Rotation equivariance: turning the image a quarter turn turns the angle by exactly π/2 — to the last bit. This only holds on an odd-sized image, where the rotation's fixpoint lands on a pixel centre; an even size puts it on a half-pixel and the error grows to ~5°. Documented in the test.
  • A closing test pins the point of the method: 11 bits apart with orientation versus 87 without, out of 256.

One note on that last test, since the numbers look suspiciously convenient: the first version used a pixel-noise image and failed at 131 vs 130. That was not a defect in ic_angle — ORB rectifies the patch by bilinear resampling, which does not preserve content at the Nyquist limit, so two views of a noise field give uncorrelated descriptors however well the orientation is recovered. The test now uses smooth structure, and the comment explains the constraint.

Verification

  • npm test — 305 passed
  • npm run typecheck — clean
  • npm run format-check — clean
  • node scripts/check-license-headers.mjs — 100 files OK
  • Built locally and both webcam examples confirmed working by @kalwalt. dist/ and types/ are deliberately not committed — they are rebuilt at release time.

Not in this PR

Both are webarkit/webarkit work.

The intensity-centroid orientation ORB needs to be rotation-invariant lived
only in the example pages, duplicated verbatim in sample_orb.html and
sample_orb_pinball.html. Nothing in src/ ever set keypoint_t.angle, so the
library could not run detect -> describe on its own: describe() rotates the
sampling patch by whatever angle it is handed, and the keypoint_t default of
-1 rotates by -1 radian rather than leaving the patch upright.

This is the same gap #133 closed for match_pattern, and it blocks the #96
CvBackend adapter: an out-of-repo adapter cannot implement detect + describe
without reimplementing the orientation itself, which would leave PureCV's
Rust port with no TS oracle to cross-validate against.

Moves the routine into orb as a public method, with the u_max table at module
scope so the hot loop allocates nothing per call. Both examples now call it
instead of carrying their own copy.

Also corrects orb_test.html, which claimed angle = -1 lets ORB work out the
orientation itself. rectify_patch feeds the value straight into cos/sin, so
that was never true; the page now fixes the angle at 0 and says why.

Tests: parity against the examples' inline implementation (the only oracle -
original jsfeat kept ic_angle in its sample too), plus invariants: gradient
direction, brightness invariance, atan2(0,0) not NaN, range, and exact 90
degree rotation equivariance on a fixpoint-aligned odd-sized image. A
closing test pins the point of the method: 11 bits apart with orientation
versus 87 without, out of 256.
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@kalwalt kalwalt self-assigned this Aug 29, 2026
@kalwalt kalwalt added documentation Improvements or additions to documentation enhancement New feature or request Typescript all about Typescript code design labels Aug 29, 2026
@kalwalt kalwalt added this to the 1.0.0 milestone Aug 29, 2026
@kalwalt
kalwalt merged commit 3baaf09 into dev Aug 29, 2026
5 checks passed
@kalwalt
kalwalt deleted the feat/96-adapter-readiness branch August 29, 2026 16:52
kalwalt added a commit that referenced this pull request Sep 2, 2026
…ange

Adding the module-scope u_max table in #182 inserted it between the orb
class's JSDoc block and the class declaration, which orphaned the comment:
TSDoc binds a block to the declaration immediately following it, and that was
now u_max, which carries its own.

The effect reached the published artifacts. The ORB description was absent
from types/src/orb/orb.d.ts entirely, so editors showed nothing on hover and
TypeDoc would have rendered the class undocumented - and 0.15.0 was about to
ship that. Moving u_max above the class doc restores the binding.

Swept every other exported class in types/ for the same pattern; orb was the
only one affected.

Also drops a {@link u_max} from the ic_angle docs. u_max is module-private
and not exported, so the link had no resolvable target.

Reported by the Qodo review on #183.
kalwalt added a commit that referenced this pull request Sep 3, 2026
…ange

Adding the module-scope u_max table in #182 inserted it between the orb
class's JSDoc block and the class declaration, which orphaned the comment:
TSDoc binds a block to the declaration immediately following it, and that was
now u_max, which carries its own.

The effect reached the published artifacts. The ORB description was absent
from types/src/orb/orb.d.ts entirely, so editors showed nothing on hover and
TypeDoc would have rendered the class undocumented - and 0.15.0 was about to
ship that. Moving u_max above the class doc restores the binding.

Swept every other exported class in types/ for the same pattern; orb was the
only one affected.

Also drops a {@link u_max} from the ic_angle docs. u_max is module-private
and not exported, so the link had no resolvable target.

Reported by the Qodo review on #183.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code design documentation Improvements or additions to documentation enhancement New feature or request Typescript all about Typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant