Skip to content

Release 1.1.0 - #244

Merged
kalwalt merged 8 commits into
mainfrom
dev
Oct 15, 2026
Merged

kalwalt merged 8 commits into
mainfrom
dev

Conversation

@kalwalt

@kalwalt kalwalt commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Promotes dev to main for 1.1.0 (MAINTAINERS.md §2, step 6). Milestone: 1.1.0.

1.1.0 is scoped to one consumer: the planar tracker in webarkit/webarkit needs a higher inlier ratio, and GMS is what raises it. TEBLID, FREAK and the tree-shakeable bundle moved to 1.2.0 (decision D12 in docs/features2d-expansion-plan.md).

What 1.1.0 ships

No public API is removed or changed; gms is a new singleton next to the existing ones.

Commits

commit PR
7635afe chore(release): bump version to 1.1.0 #243
33afeee chore(deps): upgrade dev dependencies and clear the npm audit findings #242
d8bf828 feat(gms): grid-based motion statistics match filter #237
ef426e7 test(bench): migrate the benchmark suite to the vitest 5 bench API #236
290e176 test(fixtures): OpenCV ground-truth harness, validated on compute_integral_image #235
cb6f03d chore(license): add an OpenCV/Apache-2.0 header variant, banner line and licence file #234
6d33058 fix(examples): detect ORB keypoints with a 20 px border, matching describe()'s margin #233
75d21de docs(plan): milestone 1.1.0 — GMS only (design, plan, D12) #231

Issues closed by this release: #130, #132, #134, #219, #225.

Verification

On dev at 7635afe (the release PR #243):

  • npm test: 33 files passed, 1 skipped; 561 tests passed, 1 skipped
  • npm run typecheck, npm run format-check, node scripts/check-license-headers.mjs: clean
  • npm run build-ts: both bundles carry jsfeatNext v1.1.0 and the OpenCV / Apache-2.0 banner line; jsfeatNext.VERSION === "1.1.0"
  • npm pack --dry-run: @webarkit/jsfeat-next@1.1.0, 45 files, LICENSES/Apache-2.0.txt included
  • CI and Build green on dev

After merging

Tag main with the bare version and push the tag (MAINTAINERS.md step 7):

git checkout main && git pull origin main
git tag -a 1.1.0 -m "1.1.0"
git push origin 1.1.0

release.yml then checks the tag against package.json, runs the tests and the build, creates the GitHub Release from the changelog and publishes to npm.

Milestone 1.1.0 stays open after the release. #136 — the filterMatches wiring in the webarkit/webarkit adapter — can start once 1.1.0 is on npm, and its tracker inlier-ratio measurement closes the milestone.

🤖 Generated with Claude Code

kalwalt and others added 8 commits October 14, 2026 23:42
* docs(plan): add the milestone 1.1.0 design (GMS only)

Scope 1.1.0 to the GMS match filter the webarkit planar tracker needs for
a higher inlier ratio: #130 (Apache-2.0 OpenCV licence variant), #132
(OpenCV fixtures), #134 (GMS), #136 (adapter wiring, tracked here, built in
webarkit/webarkit), plus #219 and #225. TEBLID, FREAK, tree-shaking and the
imgproc bench coverage move to a new 1.2.0 milestone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): pin the OpenCV fixture toolchain in a gitignored .venv

Replace the session-scratchpad venv with a repository .venv/ (to be
gitignored), a committed requirements file with exact pins, and a
generator that refuses to run on a different cv2 version, so fixtures
generated in different sessions cannot drift silently. Mark the design
approved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): add the milestone 1.1.0 implementation plan

Eleven tasks: docs PR and decision D12, milestone and issue housekeeping,
the GitHub project, then #225, #130, #132, #219, #134 (three tasks, one PR)
and the release preparation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(plan): record decision D12 (1.1.0 = GMS only) in the expansion plan

Add D12 to the decision log, replace the section 7 milestone allocation
table with the current state (#132 and #130 move to 1.1.0; #135, #80,
#137 and #168 move to 1.2.0; #136 is tracked here but built in
webarkit/webarkit), and note on section 3.3 that the CvBackend contract
and adapter live in webarkit/webarkit by design.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(plan): mark pre-D12 milestone lines as superseded

Add a note under section 5 (and a pointer from D12) saying the milestone
lines in D3/D3a, section 5 and section 9 record the 2026-08-17 plan and
are superseded by D12 and the section 7 table, flag the #136 entry as
re-scoped to GMS, and mark the open item about #130 being milestone-less
as resolved by D12. The historical entries themselves are untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(plan): fix the probe cleanup and record the GMS scale-fixture correction

Address the Copilot review on #231:
- Task 5 Step 2 deleted the licence probe with `git checkout -- .` and a
  directory-wide `git clean`, which would also discard unrelated work.
  Delete only src/gms/probe.ts.
- Task 8 Step 1's whole-image resizes are no scale change for GMS, which
  normalises by each image's size. Record the correction (Ruling R11):
  scale coverage comes from same-canvas zoom fixtures and gms-coverage,
  with numbers checked against OpenCV 4.14.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cribe()'s margin (#233)

The two ORB samples and the ic_angle TSDoc example detected YAPE06
keypoints with a 17 px border, below the 20 px edge margin orb.describe()
requires (#110/#146). Keypoints 17-19 px from an edge got descriptors
partly built from rectify_patch's constant fill of 128 instead of image
content. Use 20 in all three places and note why in the samples.

Closes #225

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…and licence file (#234)

* chore(license): add an OpenCV/Apache-2.0 header variant, banner line and licence file

The GMS matcher (#134) is ported from OpenCV's opencv_contrib gms.cpp, which is
Apache-2.0, not from jsfeat. With only the derived/original variants,
check-license-headers --fix would stamp it with the jsfeat MIT credit to Eugene
Zatepyakin (a false credit) and drop the Apache-2.0 notice.

- scripts/check-license-headers.mjs: new OPENCV_SRC map (path prefix ->
  attribution snippet). Files under src/gms/ get HEADER.txt with
  ATTRIBUTION-opencv-gms.txt in the {{ATTRIBUTION}} slot instead of the jsfeat
  block, and the check verifies it byte-for-byte. EXTENDED and ORIGINAL_SRC
  behave as before.
- ATTRIBUTION-opencv-gms.txt: the per-file Apache-2.0 attribution (source file,
  repo, licence, "modified version", GMS paper).
- LICENSES/Apache-2.0.txt: verbatim from opencv_contrib's LICENSE, added to the
  package.json "files" so it is in the npm tarball.
- vite.config.mts: the bundle banner now also credits OpenCV (Apache License 2.0)
  and points at LICENSES/Apache-2.0.txt.
- SKILL.md, AGENTS.md, CLAUDE.md, .github/copilot-instructions.md: document the
  third variant and the per-file "verify the upstream licence" rule.

Closes #130

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(license): describe the three header variants consistently in agent docs

.agents/instructions.md still described two templates; it now lists the
jsfeat-derived, OpenCV-derived (src/gms/) and original variants. AGENTS.md
now names the OpenCV variant as a family (opencv-<file>, currently
opencv-gms) to match scripts/check-license-headers.mjs and SKILL.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…egral_image (#235)

* test(fixtures): OpenCV ground-truth harness, validated on compute_integral_image

Adds a reproducible harness for OpenCV ground-truth fixtures, for the
algorithms the vendored jsfeat oracle cannot cover (GMS next, #134), and
validates it on compute_integral_image vs cv2.integral2.

- scripts/gen-opencv-fixtures.py: run by hand from a gitignored .venv, never
  by CI or npm ci. Parses the opencv-contrib-python pin from
  scripts/requirements-fixtures.txt and exits non-zero unless cv2.__version__
  matches it (numpy is checked too), so a wrong version is an explicit error.
  Output is deterministic: sorted keys, indent 1, LF.
- Generic fixture envelope: a common meta block (opencv_version, algorithm,
  params, image + sha256) plus an algorithm-specific payload. Neither 1.1.0
  consumer is a descriptor.
- tests/fixtures/opencv/loader.ts: loadFixture throws unless sha256(image)
  matches meta.sha256, so a stale fixture fails loudly; loadScene parses the
  committed binary PGM. node:fs and node:crypto only.
- scene-640x480.pgm: cornerScene(640, 480) dumped once from TypeScript
  (dump-scene.test.ts, skipped unless JSFEAT_DUMP_SCENE=1), so no RNG is
  reimplemented in Python and no image-encoder dependency is needed.
- integral-scene.json + tests/reference/opencv-integral.test.ts: sum and
  squared-sum tables bit-exact on a dirty destination, first row and column
  included (the #131 failure mode).
- .gitignore: .venv/ and __pycache__/. .gitattributes: the PGM is binary.

No npm dependency is added and CI is unchanged.

Closes #132

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(fixtures): require Python 3.12+ and point divergences at the registry

The pinned numpy==2.5.4 declares Requires-Python >=3.12, so "Python 3 is all
that is needed" was wrong; state the floor in the fixtures README and in the
requirements file header. The README also cited #102 as the policy for
unreachable bit-exactness, but #102 introduced the divergence registry in
tests/divergences.test.ts; say that deliberate divergences, from OpenCV as
well as from jsfeat, are registered there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
)

Upgrade vitest and @vitest/coverage-v8 to ^5.0.2 (supersedes #226) and port
the eight bench files, the ratio script, the scripts and the workflow to the
Vitest 5 benchmark API. Vitest 5 removed the top-level bench(), --outputJson
and benchmark.outputFile.

Bench files: each case keeps its describe (its name is the key in
bench/history.jsonl) and its setup, which still runs at collection time so
that `npm run bench:smoke` keeps checking it; the describe now holds one test
whose `bench` fixture runs the "jsfeatNext" and "jsfeat (reference)" cases
back to back with `.run()`. Workloads, case names and the setup outside the
timed region are unchanged. The setup/teardown options become the per-case
beforeAll/afterAll hooks.

scripts/bench-ratios.mjs reads the JSON reporter's per-test `benchmarks`
instead of files[].groups[].benchmarks[]. The ratio is defined as before
(jsfeatNext hz / jsfeat hz, hz = 1000 / mean latency, which is what Vitest 4
reported; Vitest 5's console "hz" column is a different estimator). It now
refuses input that is not Vitest 5 JSON or holds no results, so a stale or
wrong file can no longer append an empty record to history.jsonl.

vitest.config.mts sets benchmark.suppressExportGetterWarnings. This is not
cosmetic: while a benchmark runs, Vitest 5 otherwise instruments every export
of every transformed module, inside the timed region and only on the
jsfeatNext side, which inflates every module-crossing ratio (cache steady
state 2.49x instead of 1.07x; geomean 0.73 instead of 0.92).

Closes #219

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* feat(gms): grid-based motion statistics match filter (default path)

Port cv::xfeatures2d::matchGMS (opencv_contrib modules/xfeatures2d/src/gms.cpp,
4.5.0, unchanged through 4.14.0; Apache-2.0) as the jsfeatNext.gms singleton.
gms.filter(size1, size2, keypoints1, keypoints2, matches, with_rotation,
with_scale, threshold_factor) returns the kept matches as a subset of the
input, same objects, input order. This commit implements the default path;
with_rotation / with_scale throw until the next commit.

The grid arithmetic mirrors gms.cpp's float/double mix (Math.fround on the
Point2f normalisation and the float * int products, the + 0.5 in double).
Each pass costs O(matches): per-left-cell linked lists replace the dense
400 x N motion-statistics matrix, and the best right cell is kept
incrementally with OpenCV's lowest-index tie-break. Keypoints outside the
image are ignored (registered as an OpenCV divergence) and out-of-range
match indices throw RangeError.

Fixtures (scripts/gen-opencv-fixtures.py --only gms): the four search paths
from ORB matches on the committed scene, with the planned view-2 transforms
moved to the nearest values that keep a proper subset (matchGMS keeps
nothing for the planned ones on this scene; see the fixture README), plus
gms-boundaries, hand-placed points that pin the float arithmetic, the tie
rule and the threshold comparison, which the ORB fixtures barely constrain.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(fixtures): synthetic-geometry GMS fixtures for all four search paths

The ORB fixtures keep only 6-8 coincidental matches on the rotation and
scale paths, so they pin those searches weakly. Add four OpenCV 4.14.0
fixtures on dense, exact geometry instead, one per getInlierMask branch:
800 random points in 640x480 (float32, a fixed numpy seed each), an
inlier share mapped by the path's exact transform (shift (18, -11),
40 deg about the centre, x0.7 into 448x336, or both) and matched (i, i),
the rest outliers with random view-2 points and random trainIdx
(many-to-one included). thresholdFactor 6, no image.

Kept / matches: synth-default 296/780, synth-rotation 196/679,
synth-scale 373/800, synth-rotation-scale 265/689. The 60 % share keeps
only 45 on the rotation path (101 on the combined one), so both rotation
paths use 80 %; the README records this and which pass wins in each.

New generator key: --only gms-synthetic (gen_gms_synthetic). Output is
deterministic; regenerating everything leaves git status clean.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(gms): rotation and scale search paths, bit-exact on all four OpenCV fixtures

with_rotation and with_scale no longer throw. filter() now mirrors
getInlierMask: scale outer (right grids 20, 10, 14, 28, 40, from
cvRound(20 * mScaleRatios)), rotation inner (mRotationPatterns copied
verbatim); a run replaces the best mask only when it keeps strictly
more matches, from 0, so the first of tied runs wins and all-zero runs
give []. The best mask is swapped with the working one, never copied;
right cells are computed once per scale. Each pass stays O(matches).

Hardening from the Task 8 review: sizes (finite, > 0) and every keypoint
slot a match references are validated before the first get_buffer, so a
malformed size or a hole throws (RangeError / TypeError) with the pool
balanced, and the comment no longer overclaims.

Tests: the reference test is an it.each over the 4 ORB + 4 synthetic
fixtures (gms-boundaries keeps its own it) plus a flags-vs-name check.
New property tests: [] when no pass keeps anything (all paths), the
hole and size throws, a non-float32-exact coordinate whose double and
float32 readings fall in different cells, no state leak between calls
(vs a fresh new gms()), and a higher inlier ratio on a translated
cornerScene.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(fixtures): gms-coverage pins every rotation pattern and right grid

The fixtures so far let only patterns 1-2 and grids 20/14 win, so wrong
values in the other patterns or grids, ">=" for the best pass and a
swapped loop order all passed. Ruling R11:

- gms-synth-scale and gms-synth-rotation-scale zoom about the centre on
  the same 640x480 canvas instead of resizing view 2, which GMS
  normalises away. Zoom x0.7 makes the 28x28 grid win (248/800 kept);
  40 deg then zoom x sqrt 2 makes the 10x10 grid win with pattern 2
  (137/477).
- New gms-coverage fixture (--only gms-coverage, gen_gms_coverage): 33
  cases, 72-199 matches each. 45 deg rotation steps make patterns 1-8
  win in turn. Zooms 2, sqrt 2, 1, 1/sqrt 2 and 1/2 make grids 10, 14,
  20, 28 and 40 win. Two combined cases, two hand-placed ties (pattern
  1 vs 90 deg; scale 0 x 90 deg vs 10x10 x pattern 1) and 16 random
  configurations (sizes, flags, thresholdFactor in [2, 8], shares).
  Every case keeps at least 1 match and fewer than all.
- Synthetic coordinates are quarter pixels, and synthetic fixtures put
  each numeric array on one line. The four gms-synth-* files go from
  ~117 KB to ~33 KB each; gms-coverage is 211 KB.
- The reference test adds an it.each over the coverage cases.

All 32 mutants tried fail at least one fixture. These include a swap in
each of the 8 patterns, each right grid off by one either way, ">=" and
rotation-outer order. Regenerating is byte-identical, and the ORB,
boundary and integral fixtures are unchanged.

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(bench): time gms.filter against the matching step it follows (#134)

bench/gms.bench.ts runs the four GMS paths and the knnMatch k=2 +
ratio_test 0.75 baseline on one workload, the tracker's: the 300
strongest FAST corners of two 640x480 views (second view translated by
(16, 8)), ORB-described. jsfeat has no GMS, so there is no jsfeat side;
the result is the intra-library "gms: <path> costs N x matching".

cornerScene gives only 36 corners and 24 matches, too little for GMS to
do anything, so the views are two crops of one seeded noise canvas
(300 + 300 keypoints, 289 matches). Collection throws below 300
keypoints, 100 matches, or if a path keeps nothing.

scripts/bench-ratios.mjs reports a group with neither implementation
name on its own, relative to the group's first task, instead of as an
error row; the 36 A/B rows and the history record are unchanged and the
intra-library group is never recorded. bench/README.md gets the numbers
and the conditions they were taken under.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(gms): list gms in the README and AGENTS.md (#134)

README: gms in the module list and GMS filtering in the pipeline
sentence, after bfmatcher; the benchmarks note names the one bench file
that has no jsfeat side. AGENTS.md: 15 algorithm singletons, gms
included, and the roadmap pointer now reads 1.1.0 = GMS (#134) with
TEBLID, FREAK and tree-shaking in 1.2.0. The pose_estimator TSDoc that
counted the singletons says 15 too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(gms): return every borrowed buffer on any exit; fix the filter example (#134)

gms.filter validated its input before borrowing, but anything that threw
afterwards (a keypoint accessor, a non-numeric threshold_factor such as a
BigInt, a failed buffer growth) skipped the put_buffer calls and drained the
pool every module shares. Record each borrow as it is made and return them all
in a finally, and correct the comment that claimed only a throwing accessor
could escape.

The new property test throws after the borrows through both a hostile
keypoint accessor and a BigInt threshold_factor and relies on
tests/setup/pool-balance.ts; it fails (pool -24) if the buffers are only
returned on the normal path, and when the put_buffer calls are removed.

The TSDoc example allocated 500-entry keypoint pools, so fast_corners.detect
throws "corners pool is full" on a realistic frame (17,250 corners on a
blurred 640x480 textured one). It now sizes the pool to the image as
examples/sample_orb.html does, keeps the 300 strongest by score as the
tracker does, and guards find_homography on at least 4 kept matches. The
example was type-checked and run end to end against such a frame.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs: refresh the test count and note the gms.bench.ts exception (#134)

README said 350+ tests across 28 files; the suite is now 441 passing tests in
33 files. bench/README's "Read the ratio" section said every case runs through
both jsfeatNext and jsfeat, which gms.bench.ts does not (jsfeat has no GMS), as
the top-level README already says.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(fixtures): pin OpenCV 4.10.0, the reference build PureCV measures against

Align the fixture toolchain with PureCV's cpp_ref (opencv-contrib-python
4.10.0.84 built from opencv_contrib 1ed3dd2c, numpy 2.4.6), so both links
of the PureCV -> jsfeatNext -> OpenCV validation chain use one OpenCV.

Regenerating every fixture with 4.10.0 changed nothing but
meta.opencv_version: gms.cpp is byte-identical (blob c13ffe4) in 4.10.0 and
4.14.0, and the ORB, synthetic, coverage, boundary and integral inputs and
outputs are unchanged. The divergence probe in tests/divergences.test.ts was
re-checked: OpenCV 4.10.0 keeps it, as 4.14.0 did. numpy 2.4.6 needs Python
>= 3.11, so the README and requirements header now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(agents): require OpenCV 4.10.0 for every OpenCV comparison and port

Add an "OpenCV reference version" rule to AGENTS.md: comparisons with
OpenCV, ports from it, fixtures and "OpenCV does X" claims all use OpenCV
4.10.0 (opencv-contrib-python 4.10.0.84, opencv 71d3237a / opencv_contrib
1ed3dd2c), the build PureCV measures against. The exact build lives in
scripts/requirements-fixtures.txt; changing it takes its own issue (plus
the matching one in webarkit/purecv) and a PR that regenerates every
fixture and reports what changed.

CLAUDE.md, README.md, .github/copilot-instructions.md and
.agents/instructions.md state the rule with the version spelled out and
point at AGENTS.md; SKILL.md and the gms TSDoc cite the reference release.

tests/fixtures/opencv/pinned-version.test.ts enforces it: every committed
fixture's meta.opencv_version must equal the pin, AGENTS.md must name the
pinned wheel, and each entry-point file must state the pinned version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(fixtures): pin the generic CPU path, record build info, enforce pinned citations

Adopt three practices from PureCV's OpenCV reference policy:
- The generator switches Intel IPP off (cv2.ipp.setUseIPP(False)) and fails
  if it cannot, so fixtures pin OpenCV's generic CPU path. Regenerating with
  IPP off changed no fixture payload.
- Every fixture records the "Version control" lines of
  cv2.getBuildInformation() (opencv_build, opencv_contrib_build) and
  "ipp": false.
- OpenCV sources and docs are cited with links pinned to the release
  (blob/4.10.0/..., docs.opencv.org/4.10.0/...), never 4.x/master or a bare
  file.cpp:NN; the gms TSDoc now links gms.cpp at 4.10.0.

AGENTS.md names PureCV's CONTRIBUTING policy as the shared one and adds
the generic-CPU-path, build-info and citation rules. pinned-version.test.ts
now also checks every fixture's build lines and ipp flag, and that src/,
tests/, bench/, scripts/ and the entry-point docs cite OpenCV only at the
pinned version (docs/ is exempt: dated analyses name their version).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(bench): never append a history record without a successful A/B row

bench-ratios.mjs guarded --append with `rows.length === 0`, but `rows`
also holds error rows, so a run whose only results were failed or unpaired
tests passed the guard and appended `{"cases": {}, "geomean": null}` to
bench/history.jsonl. Require at least one successful A/B row and name the
error and intra-library counts in the message.

Reproduced with a failed-only Vitest JSON (appended an empty record, exit
0; now exit 2, nothing written). A mixed input still appends its valid
rows, and the output on the saved full bench run is unchanged.

Reported by Copilot review on #237.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(gms): cover the right-grid bound guard with a fractional image size

The guard in right_cells (x < grid_right && y < grid_right, else -1) was the
one branch no test reached, as Codecov reported on #237: with an integer
image size it cannot trigger. With a fractional one it can: x = 1000 inside
a 1000 + 1e-9 wide image normalises to exactly 1 in float32, so the column
equals the grid size and OpenCV's unguarded `x + y * 20` would wrap into the
next row.

The test puts the cluster's right side in the very cell the wrap would hit,
so without the guard the probe match is kept (verified: removing the guard
fails the test with 17 kept); with it, the probe is dropped on all four
paths and nothing throws. gms.ts now has no uncovered branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#242)

Supersede the open Dependabot PRs with one lockfile regeneration (npm 11):
- vite ^8.3.4 (#239), vite-plugin-dts ^5.1.2 (#229), prettier ~3.9.10 (#238)
- transitive: brace-expansion 5.0.12 (#241), markdown-it 14.3.2 (#240),
  source-map-js 1.2.2 -- `npm audit` goes from 3 findings (2 high, 1
  moderate, all dev-only) to 0.

TypeScript stays on 6.0.x: typedoc 0.28.20, the latest, declares
`typescript` peer support only up to 6.0.x, so 7.x would fail to install
and break `npm run docs`. dependabot.yml now ignores typescript major
updates, with the reason and when to drop the entry, so no PR is opened
that cannot pass. GitHub Actions are already on their latest majors.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1.1.0 ships the GMS match filter (#134, PR #237), bit-exact against OpenCV
4.10.0 on all four search paths, with its prerequisites: the OpenCV/Apache-2.0
licence variant (#130, PR #234), the OpenCV ground-truth fixture harness (#132,
PR #235), the vitest 5 bench API (#219, PR #236), the ORB border fix (#225,
PR #233), and the dev-dependency upgrade (PR #242). Changelog generated with
git-cliff.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@kalwalt kalwalt added this to the 1.1.0 milestone Oct 15, 2026
@kalwalt kalwalt self-assigned this Oct 15, 2026
@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

Copy link
Copy Markdown

PR Summary by Qodo

Release 1.1.0 with OpenCV-compatible GMS match filtering

✨ Enhancement 🧪 Tests 🐞 Bug fix 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add GMS filtering before homography estimation to improve tracker match quality.
• Validate GMS against pinned OpenCV fixtures and benchmark its cost relative to matching.
• Prepare release 1.1.0 with licensing, benchmark migration, ORB border corrections, and dependency
 updates.
Diagram

graph TD
  A["ORB matches"] --> B["GMS filter"] --> C["RANSAC input"]
  D["Pinned OpenCV"] --> E["Fixture generator"] --> F["Committed fixtures"] --> G["Parity tests"] --> B
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Mirror OpenCV's dense statistics matrix
  • ➕ Simplifies line-by-line comparison with upstream.
  • ➖ Scans and clears substantially more grid data per pass.
2. Call OpenCV at runtime
  • ➕ Avoids maintaining a TypeScript port.
  • ➖ Requires a native or WASM dependency for browser consumers.

Recommendation: Keep the sparse, pooled TypeScript port: it fits the existing browser-oriented singleton architecture, while pinned fixtures constrain its behavior. Review rounding, tie-breaking, sparse counting, and buffer cleanup closely.

Files changed (63) +18614 / -496

Enhancement (4) +564 / -0
core.tsDeclare the GMS namespace property +2/-0

Declare the GMS namespace property

• Adds the typed GMS singleton to the public core namespace.

src/core/core.ts

gms.tsImplement GMS filtering +558/-0

Implement GMS filtering

• Adds sparse grid statistics, rotation and scale searches, pooled scratch storage, and input validation.

src/gms/gms.ts

index.tsExport the GMS size type +1/-0

Export the GMS size type

• Exposes IGmsSize from the package entry point.

src/index.ts

jsfeatNext.tsAttach the GMS singleton +3/-0

Attach the GMS singleton

• Instantiates GMS alongside existing algorithms.

src/jsfeatNext.ts

Bug fix (2) +4 / -2
sample_orb.htmlIncrease ORB detection border +2/-1

Increase ORB detection border

• Uses the 20-pixel margin required by descriptor sampling.

examples/sample_orb.html

sample_orb_pinball.htmlIncrease pinball detection border +2/-1

Increase pinball detection border

• Uses the 20-pixel margin required by descriptor sampling.

examples/sample_orb_pinball.html

Documentation (17) +1936 / -50
instructions.mdDocument OpenCV provenance and version +3/-1

Document OpenCV provenance and version

• Directs agents to the GMS-specific license header and OpenCV 4.10.0 reference.

.agents/instructions.md

SKILL.mdExplain the OpenCV header variant +44/-7

Explain the OpenCV header variant

• Documents attribution selection and distribution of the Apache license.

.agents/skills/license-header-adder/SKILL.md

ATTRIBUTION-opencv-gms.txtAdd reusable GMS attribution +8/-0

Add reusable GMS attribution

• Credits the modified OpenCV GMS source and identifies its Apache-2.0 license.

.agents/skills/license-header-adder/resources/ATTRIBUTION-opencv-gms.txt

copilot-instructions.mdUpdate OpenCV contributor rules +2/-1

Update OpenCV contributor rules

• Adds GMS attribution and pinned-reference guidance.

.github/copilot-instructions.md

AGENTS.mdCodify GMS and OpenCV policies +19/-3

Codify GMS and OpenCV policies

• Lists the new singleton and defines licensing and reference-version requirements.

AGENTS.md

CHANGELOG.mdRecord release 1.1.0 +32/-0

Record release 1.1.0

• Summarizes GMS, fixtures, benchmark migration, licensing, fixes, and dependency changes.

CHANGELOG.md

CLAUDE.mdUpdate OpenCV agent guidance +2/-1

Update OpenCV agent guidance

• Specifies the GMS attribution variant and pinned OpenCV release.

CLAUDE.md

Apache-2.0.txtInclude the Apache 2.0 license +202/-0

Include the Apache 2.0 license

• Supplies the license text for OpenCV-derived source.

LICENSES/Apache-2.0.txt

README.mdPresent GMS in the feature pipeline +6/-4

Present GMS in the feature pipeline

• Adds GMS to the documented matching pipeline and describes its benchmarking and OpenCV reference.

README.md

README.mdDocument Vitest 5 and GMS measurements +227/-15

Document Vitest 5 and GMS measurements

• Explains ratio collection, benchmark caveats, and GMS cost relative to matching.

bench/README.md

features2d-expansion-plan.mdRecord the GMS-only milestone decision +27/-11

Record the GMS-only milestone decision

• Adds D12, clarifies adapter ownership, and defers other features to 1.2.0.

docs/features2d-expansion-plan.md

2026-10-14-milestone-1.1.0.mdPlan the GMS release +498/-0

Plan the GMS release

• Defines implementation tasks, prerequisites, review risks, and release steps.

docs/superpowers/plans/2026-10-14-milestone-1.1.0.md

2026-10-14-milestone-1.1.0-design.mdSpecify the GMS-only milestone +493/-0

Specify the GMS-only milestone

• Sets tracker-driven goals, parity expectations, and deferred scope.

docs/superpowers/specs/2026-10-14-milestone-1.1.0-design.md

orb.tsCorrect the ORB example margin +1/-1

Correct the ORB example margin

• Changes the documented detection border from 17 to 20 pixels.

src/orb/orb.ts

pose_estimator.tsUpdate singleton documentation +1/-1

Update singleton documentation

• Accounts for GMS in the algorithm-module count.

src/pose_estimator/pose_estimator.ts

README.mdDocument reference fixture provenance +365/-0

Document reference fixture provenance

• Explains the pinned toolchain, shared scene, regeneration, and fixture families.

tests/fixtures/opencv/README.md

vitest.bench-dist.config.mtsClarify built-bundle benchmarking +6/-5

Clarify built-bundle benchmarking

• Updates comments for Vitest 5's module runner.

vitest.bench-dist.config.mts

Other (40) +16110 / -444
.gitattributesTreat the fixture scene as binary +3/-0

Treat the fixture scene as binary

• Prevents line-ending conversion of the PGM image.

.gitattributes

dependabot.ymlDefer TypeScript major upgrades +8/-0

Defer TypeScript major upgrades

• Ignores incompatible TypeScript major updates until TypeDoc supports them.

.github/dependabot.yml

bench.ymlCollect Vitest 5 benchmark JSON +4/-1

Collect Vitest 5 benchmark JSON

• Uses the JSON reporter in place of the removed outputJson option.

.github/workflows/bench.yml

cache.bench.tsMigrate cache benchmarks +52/-47

Migrate cache benchmarks

• Uses Vitest 5 test-scoped runs and per-case buffer lifecycle hooks.

bench/cache.bench.ts

detectors.bench.tsMigrate detector benchmarks +33/-44

Migrate detector benchmarks

• Preserves paired workloads and configures FAST thresholds with per-case hooks.

bench/detectors.bench.ts

gms.bench.tsBenchmark four GMS search paths +188/-0

Benchmark four GMS search paths

• Measures GMS against tracker-style matching and guards against undersized workloads.

bench/gms.bench.ts

imgproc.bench.tsMigrate image-processing benchmarks +36/-26

Migrate image-processing benchmarks

• Moves existing paired measurements to Vitest 5's test-scoped API.

bench/imgproc.bench.ts

linalg.bench.tsMigrate linear-algebra benchmarks +81/-69

Migrate linear-algebra benchmarks

• Keeps paired measurements and resets mutable solver inputs through per-case hooks.

bench/linalg.bench.ts

math_matmath_transform.bench.tsMigrate math and transform benchmarks +99/-81

Migrate math and transform benchmarks

• Uses test-scoped runs and new input-reset hooks.

bench/math_matmath_transform.bench.ts

motion_estimator.bench.tsMigrate estimator benchmark hooks +70/-69

Migrate estimator benchmark hooks

• Seeds and restores Math.random around paired RANSAC and LMEDS measurements.

bench/motion_estimator.bench.ts

motion_model.bench.tsMigrate motion-model benchmarks +50/-36

Migrate motion-model benchmarks

• Moves homography and affine comparisons to Vitest 5 bench runs.

bench/motion_model.bench.ts

optical_flow_lk.bench.tsMigrate optical-flow benchmarks +30/-28

Migrate optical-flow benchmarks

• Uses test-scoped runs for paired Lucas–Kanade tracking measurements.

bench/optical_flow_lk.bench.ts

package.jsonPrepare package version and scripts +10/-9

Prepare package version and scripts

• Bumps to 1.1.0, updates development dependencies, packages LICENSES, and changes benchmark reporting.

package.json

bench-ratios.mjsReduce Vitest 5 benchmark output +171/-26

Reduce Vitest 5 benchmark output

• Reads JSON-reporter results, preserves A/B history, and reports GMS intra-library costs separately.

scripts/bench-ratios.mjs

check-license-headers.mjsSelect OpenCV attribution by path +38/-4

Select OpenCV attribution by path

• Applies the OpenCV-specific header to GMS instead of jsfeat credit.

scripts/check-license-headers.mjs

gen-opencv-fixtures.pyGenerate OpenCV ground truth +871/-0

Generate OpenCV ground truth

• Produces deterministic integral and GMS fixtures using pinned versions with Intel IPP disabled.

scripts/gen-opencv-fixtures.py

requirements-fixtures.txtPin fixture-generation dependencies +25/-0

Pin fixture-generation dependencies

• Pins opencv-contrib-python 4.10.0.84 and NumPy 2.4.6.

scripts/requirements-fixtures.txt

api-shape.test.tsAssert the GMS public method +1/-0

Assert the GMS public method

• Checks that jsfeatNext.gms.filter is available.

tests/api-shape.test.ts

divergences.test.tsTest intentional OpenCV divergence +46/-0

Test intentional OpenCV divergence

• Checks that an out-of-image keypoint is dropped rather than wrapped into another cell.

tests/divergences.test.ts

dump-scene.test.tsProvide opt-in scene generation +81/-0

Provide opt-in scene generation

• Dumps the shared test scene as a binary PGM only when explicitly enabled.

tests/fixtures/opencv/dump-scene.test.ts

gms-boundaries.jsonPin GMS boundary decisions +1642/-0

Pin GMS boundary decisions

• Stores OpenCV results for cell borders, ties, and an exact threshold.

tests/fixtures/opencv/gms-boundaries.json

gms-coverage.jsonPin GMS branch coverage +412/-0

Pin GMS branch coverage

• Stores 33 rotation, scale, tie-order, and randomized cases.

tests/fixtures/opencv/gms-coverage.json

gms-default.jsonPin default GMS output +2635/-0

Pin default GMS output

• Stores OpenCV results for translated ORB scene views with outliers.

tests/fixtures/opencv/gms-default.json

gms-rotation-scale.jsonPin combined GMS output +2348/-0

Pin combined GMS output

• Stores OpenCV results for rotated and resized ORB scene views.

tests/fixtures/opencv/gms-rotation-scale.json

gms-rotation.jsonPin rotation GMS output +2524/-0

Pin rotation GMS output

• Stores OpenCV results for rotated ORB scene views.

tests/fixtures/opencv/gms-rotation.json

gms-scale.jsonPin scale GMS output +2346/-0

Pin scale GMS output

• Stores OpenCV results for resized ORB scene views.

tests/fixtures/opencv/gms-scale.json

gms-synth-default.jsonPin synthetic default-path results +29/-0

Pin synthetic default-path results

• Stores OpenCV results for translated synthetic points and outliers.

tests/fixtures/opencv/gms-synth-default.json

gms-synth-rotation-scale.jsonPin synthetic combined-search results +29/-0

Pin synthetic combined-search results

• Stores OpenCV results for rotated and zoomed synthetic points.

tests/fixtures/opencv/gms-synth-rotation-scale.json

gms-synth-rotation.jsonPin synthetic rotation results +29/-0

Pin synthetic rotation results

• Stores OpenCV results for rotated synthetic points.

tests/fixtures/opencv/gms-synth-rotation.json

gms-synth-scale.jsonPin synthetic scale results +29/-0

Pin synthetic scale results

• Stores OpenCV results for zoomed synthetic points.

tests/fixtures/opencv/gms-synth-scale.json

integral-scene.jsonPin integral-image ground truth +1147/-0

Pin integral-image ground truth

• Stores OpenCV checksums and border values for the shared scene.

tests/fixtures/opencv/integral-scene.json

loader.tsVerify committed fixtures on load +166/-0

Verify committed fixtures on load

• Parses fixture envelopes and rejects image-backed fixtures with stale hashes.

tests/fixtures/opencv/loader.ts

pinned-version.test.tsEnforce reference-version consistency +146/-0

Enforce reference-version consistency

• Checks fixture metadata, pinned requirements, documentation, and citations.

tests/fixtures/opencv/pinned-version.test.ts

scene-640x480.pgmCommit the shared fixture scene +4/-0

Commit the shared fixture scene

• Provides identical image bytes to TypeScript tests and Python generation.

tests/fixtures/opencv/scene-640x480.pgm

api-surface.test.tsRegister GMS as an API addition +1/-0

Register GMS as an API addition

• Notes that GMS has no original-jsfeat counterpart.

tests/parity/api-surface.test.ts

gms.test.tsExercise GMS edge cases +419/-0

Exercise GMS edge cases

• Tests validation, rounding, order, pool cleanup, call independence, and inlier-ratio behavior.

tests/properties/gms.test.ts

gms.opencv.test.tsAssert exact OpenCV GMS results +154/-0

Assert exact OpenCV GMS results

• Checks kept-match indices across ORB, synthetic, boundary, and coverage fixtures.

tests/reference/gms.opencv.test.ts

opencv-integral.test.tsValidate the integral-image fixture harness +133/-0

Validate the integral-image fixture harness

• Cross-checks integral outputs and verifies rejection of altered fixture images.

tests/reference/opencv-integral.test.ts

vite.config.mtsCredit OpenCV in built bundles +4/-0

Credit OpenCV in built bundles

• Adds OpenCV attribution and the packaged Apache-license path to the banner.

vite.config.mts

vitest.config.mtsDisable benchmark getter instrumentation +16/-4

Disable benchmark getter instrumentation

• Suppresses Vitest 5 export-getter instrumentation that distorts source benchmark comparisons.

vitest.config.mts

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

qodo-free-for-open-source-projects Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Informational

1. Fixture tool rejects valid OpenCV PGM headers 🐞 Bug ☼ Reliability
Description
The PGM header parser in loadScene reads comments and whitespace while parsing the width, height
and maxval tokens, but once it reads maxval it advances exactly one byte and treats everything
after as raw pixel data. A header that contains a comment line or extra whitespace after maxval —
both legal in the PGM format — is misread, pulling header bytes into the pixel buffer and failing
the subsequent width * height length check.
Code

tests/fixtures/opencv/loader.ts[R134-138]

+    const token = (): string => {
+        for (;;) {
+            while (pos < bytes.length && isSpace(bytes[pos])) pos++;
+            if (bytes[pos] !== 0x23 /* # */) break;
+            while (pos < bytes.length && bytes[pos] !== 0x0a) pos++;
Evidence
token() in loader.ts handles comments while reading width/height/maxval, but the code immediately
following the maxval token only skips a single whitespace byte before starting to read pixel data,
so any additional header content (a trailing comment, multiple whitespace bytes) is silently
included in the pixel buffer.

tests/fixtures/opencv/loader.ts[100-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PGM loader in `tests/fixtures/opencv/loader.ts` advances exactly one byte after reading the `maxval` token before treating the rest of the buffer as raw pixel data. Any legal PGM header that includes a comment line or more than one whitespace byte after `maxval` is misparsed, pulling header bytes into the pixel data and failing the length check.
## Fix Focus Areas
- tests/fixtures/opencv/loader.ts[107-138]
## Recommended Fix
Reuse the same comment/whitespace-skipping logic used for the width/height/maxval tokens to skip any run of whitespace and comment lines immediately after the maxval token, before starting to read the binary pixel data, rather than unconditionally consuming a single byte.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. GMS fixture generator can't catch a mismatched OpenCV build 🐞 Bug ☼ Reliability
Description
The generator in gen-opencv-fixtures.py only compares cv2.__version__ against the pinned
major.minor.patch, while the build-specific details returned by
cv2.getBuildInformation()/build_version are recorded into the fixture metadata but never
validated against the pin. A locally rebuilt or vendor-patched OpenCV that reports the same version
string but differs from the pinned opencv-python-headless wheel can therefore regenerate fixtures
that silently drift from the reference the GMS and integral tests are meant to pin against.
Code

scripts/gen-opencv-fixtures.py[R94-104]

+def require_pinned_toolchain() -> Toolchain:
+    """Import cv2 and numpy, exiting non-zero unless they match the pins.
+
+    This runs before anything is read or written. A fixture generated with an
+    unpinned OpenCV would silently change the ground truth, so a mismatch is an
+    explicit error, never a warning.
+    """
+    requirements = REQUIREMENTS.read_text(encoding="utf-8")
+    cv2_pin = parse_pin(requirements, OPENCV_DIST)
+    np_pin = parse_pin(requirements, NUMPY_DIST)
+
Evidence
The version guard rejects only a mismatched cv2.__version__; the build-specific values captured
afterward are written into fixture metadata without any check that they correspond to the pinned
wheel, so fixtures generated from a differently-built OpenCV with the same version string pass this
guard.

scripts/gen-opencv-fixtures.py[94-120]
scripts/gen-opencv-fixtures.py[175-179]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scripts/gen-opencv-fixtures.py` only checks `cv2.__version__` against the pinned version before generating fixtures. Build-specific metadata (from `cv2.getBuildInformation()` or similar) is captured and written to fixture files but never compared against the pinned wheel, so a differently-built OpenCV with a matching version string can regenerate fixtures that silently diverge from the pinned reference.
## Fix Focus Areas
- scripts/gen-opencv-fixtures.py[94-120]
- scripts/gen-opencv-fixtures.py[175-179]
## Recommended Fix
After capturing the build-specific version details, compare them against an expected value derived from the pinned `opencv-python-headless` wheel (e.g. a hash or known build string) and abort generation with a clear error if they differ, mirroring the existing `cv2.__version__` guard.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread tests/fixtures/opencv/loader.ts
Comment thread scripts/gen-opencv-fixtures.py
@kalwalt

kalwalt commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Qodo review outcome for the 1.1.0 release PR (no change to this PR, which only promotes dev to main):

  1. PGM header parsing — kept as is. The Netpbm spec puts exactly one whitespace character between the maxval and the raster, so loadScene consumes one byte; skipping more would eat pixel data. Details: Release 1.1.0 #244 (comment) (dismissed as intentional).
  2. Fixture generator only checks cv2.__version__ — partly valid; an exact check of the installed opencv-contrib-python wheel is tracked in test(fixtures): check the exact opencv-contrib-python wheel, not only cv2.__version__ #245 (milestone 1.2.0, before the TEBLID/FREAK fixtures). Details: Release 1.1.0 #244 (comment) (dismissed as deferred).

The minor findings deferred from the earlier 1.1.0 task reviews are collected in #246 (milestone 1.2.0).

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

Copy link
Copy Markdown

Qodo review outcome for the 1.1.0 release PR (no change to this PR, which only promotes dev to main):

  1. PGM header parsing — kept as is. The Netpbm spec puts exactly one whitespace character between the maxval and the raster, so loadScene consumes one byte; skipping more would eat pixel data. Details: Release 1.1.0 #244 (comment) (dismissed as intentional).
  2. Fixture generator only checks cv2.__version__ — partly valid; an exact check of the installed opencv-contrib-python wheel is tracked in test(fixtures): check the exact opencv-contrib-python wheel, not only cv2.__version__ #245 (milestone 1.2.0, before the TEBLID/FREAK fixtures). Details: Release 1.1.0 #244 (comment) (dismissed as deferred).

The minor findings deferred from the earlier 1.1.0 task reviews are collected in #246 (milestone 1.2.0).

Acknowledged. For this PR, finding 1 (PGM parsing) is intentional and finding 2 (OpenCV build pinning) is deferred to #245; the remaining minor items are tracked in #246. No changes or further Qodo action are needed here.

@kalwalt
kalwalt merged commit c70e309 into main Oct 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant