Skip to content

new release 0.9.0 - #73

Merged
kalwalt merged 10 commits into
masterfrom
dev
Sep 23, 2026
Merged

kalwalt merged 10 commits into
masterfrom
dev

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 23, 2026

Copy link
Copy Markdown
Member

This pull request introduces several important improvements and updates across the build system, versioning, and the core matching logic for handling multiple reference images per page. The changes enhance the flexibility of OpenCV integration, improve multi-marker support in the matching pipeline, and update dependencies and documentation for clarity and correctness.

Build and Dependency Updates:

  • Upgraded GitHub Actions workflow to use newer versions of actions/checkout (v7), actions/setup-node (v6), Node.js (22.x), and the Emscripten Docker image (3.1.69) for improved compatibility and performance. [1] [2]
  • Centralized OpenCV version and hash configuration in the new cmake/OpenCVEm.cmake file, supporting both native and multiple Emscripten builds (including SIMD), and updated CMake files to fetch OpenCV using these shared settings. [1] [2] [3] [4]

Versioning and Documentation:

  • Updated WebARKit version to 0.9.0 in code and tests to reflect new features and changes. [1] [2] [3] [4]
  • Improved CONTRIBUTING.md with clearer instructions for merging long-lived branches, emphasizing the use of merge commits for branch syncs to prevent history issues.

Multi-Marker Matching Enhancements:

  • Refactored the matcher logic to keep all reference images that pass geometric verification, not just the single best match. The new image_match_t and image_matches_t types store all successful matches per query, enabling robust multi-marker (multi-page) support. [1] [2] [3] [4] [5] [6] [7]
  • Updated the matching pipeline in kpmMatching.cpp to aggregate and process all valid matches per page, ensuring that pose estimation attempts all candidates and improves tracking reliability when multiple markers are visible. [1] [2] [3] [4]

Summary of Most Important Changes

Multi-Marker Matching Improvements:

  • Introduced image_match_t and image_matches_t types to store all geometrically verified matches, not just the best one, and refactored the matching pipeline to process all valid matches per page for robust multi-marker support. [1] [2] [3] [4] [5] [6] [7] [8]

Build System and Dependency Management:

  • Centralized and upgraded OpenCV dependency management via the new cmake/OpenCVEm.cmake, supporting native, Emscripten, and SIMD builds, and updated CMake and test configuration to use these settings. [1] [2] [3] [4]
  • Updated GitHub Actions workflow to use newer versions of tools and Docker images, improving build reliability and future-proofing the CI pipeline. [1] [2]

Versioning and Documentation:

  • Bumped WebARKit version to 0.9.0 in code and tests to match new features and changes. [1] [2] [3] [4]
  • Clarified branch merging procedures in CONTRIBUTING.md, highlighting the importance of merge commits for branch syncs to maintain correct git history.

kalwalt and others added 8 commits July 5, 2026 20:48
- Extract shared release coordinates into cmake/OpenCVEm.cmake so
  WebARKit/CMakeLists.txt and tests/CMakeLists.txt no longer hardcode
  the version independently.
- Add WEBARKIT_SIMD option (default OFF) to opt into the new
  SIMD-enabled emscripten build shipped in opencv-em 0.2.0.
- Pin FetchContent_Declare URLs with SHA256 URL_HASH using digests
  from GitHub's release API.
- Bump CI's emscripten/emsdk Docker tag 3.1.38 -> 3.1.69 to match the
  emcc version used to build the new opencv-em release.
dev had fallen behind master by 14 commits, including the WebARKitVideoLuma
module, the KPM matcher determinism fix and the 1.7.6 version bump. Consumers
building against master's layout could not build against dev: WebARKitVideoLuma
was moved to include/AR/videoLuma.h on dev but is still expected at
WebARKit/WebARKitVideoLuma.cpp.

Merges cleanly with no conflicts. dev keeps its own two commits, the
actions/checkout bump and the OpenCV 4.12.0 upgrade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A branch-sync PR - master back into dev after a release - must be merged with
"Create a merge commit", not squashed or rebased.

Squashing collapses the incoming commits into one new commit; rebasing replays
them under new SHAs. Either way the target branch ends up containing the code
without git recognising the two branches as related, so the next sync offers the
same commits again and usually conflicts. Only a merge commit makes them genuine
ancestors, which is what makes the following sync a no-op.

All three merge methods are enabled on the repository and the button remembers
the last one used, so this is easy to get wrong.

Written down because dev had drifted 14 commits behind master and needed
repairing in #68, during which consumers could not build against dev at all.

Also corrects the workflow section, which told contributors to avoid `main`;
the release branch is `master`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(kpm): treat matchedId 0 as a valid match, and add ARLOGd tracing
* feat(kpm): report every matched page, not only the best one

VisualDatabase::query() now keeps every reference image that passes the
inlier tests, exposed as matches(); matchedId()/inliers() still give the
best. kpmMatching writes one pose per page, keeping the best-supported
image of each page, and the dead per-page loop is removed.

Refs webarkit/jsartoolkitNFT#635

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

* fix(kpm): index results by page position and fall back across a page's images

Two review findings on #71:

- result[] is allocated densely, one slot per position in
  refDataSet.pageInfo, and kpmSetMatchingSkipPage() indexes it that way,
  but the per-page loop indexed it by the page's external number.
  Those only coincide when pages are numbered 0..n-1 in load order;
  kpmChangePageNoOfRefDataSet() can set any value. Record each db_id's
  page position (pageIndices) and index result[] by it, reporting the
  external pageNo in KpmResult::pageNo as before.

- If the pose of a page's best-supported image could not be fitted, the
  page was dropped even when another verified image of the same page
  would give a pose. Try the page's images in descending inlier order
  and keep the first that succeeds. No extra cost when the first
  succeeds.

Refs webarkit/jsartoolkitNFT#635

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
A minor bump, as defined in WebARKitConfig.cpp ("additions to the API,
or other significant backwards-compatible changes in runtime
functionality"): #71 added VisualDatabase::matches() / image_match_t,
and kpmMatching now reports one result per matched page instead of only
the best one. Updates WEBARKIT_HEADER_VERSION_STRING and _MINOR, and the
two gtest assertions that pin the version string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Release 0.9.0 with multi-page matching and OpenCV 4.12

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

Grey Divider

AI Description

• Retains every geometrically verified image match and estimates poses for each matched page.
• Centralizes OpenCV 4.12 artifacts, hashes, and optional Emscripten SIMD selection.
• Releases WebARKit 0.9.0 with refreshed CI and branch-sync guidance.
Diagram

graph TD
  Q["Query frame"] --> V["Visual database"] --> G{"Geometry valid?"}
  G -->|Yes| M["Image matches"] --> P["Page grouping"] --> S["Pose estimation"] --> R["Page results"]
  G -->|No| X["Discard candidate"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Aggregate matches inside VisualDatabase
  • ➕ Could return one ranked candidate collection per logical page.
  • ➕ Would reduce grouping logic in kpmMatching.cpp.
  • ➖ The generic visual database does not own KPM page metadata.
  • ➖ It would couple reusable image matching code to KPM-specific page semantics.
  • ➖ Existing consumers rely on database image identifiers rather than page identifiers.
2. Keep only the best image per page
  • ➕ Reduces retained match data and pose-estimation attempts.
  • ➕ Provides simpler downstream result processing.
  • ➖ Requires page metadata during matching or an additional filtering pass.
  • ➖ Loses fallback candidates when the strongest image match cannot produce a pose.
  • ➖ Could reduce tracking reliability for pages registered at multiple scales.

Recommendation: Keep the PR's separation: VisualDatabase should expose every verified image while KPM performs page-aware grouping and pose fallback. This preserves the existing single-best-match API for compatibility, avoids coupling the generic matcher to KPM page metadata, and retains alternate scale candidates when pose fitting fails.

Files changed (14) +152 / -68

Enhancement (4) +30 / -5
visual_database_facade.cppExpose all verified matches through the facade +5/-1

Expose all verified matches through the facade

• Implements the facade accessor that forwards the latest collection of geometrically verified image matches.

lib/SRC/KPM/FreakMatcher/facade/visual_database_facade.cpp

visual_database_facade.hDeclare the multi-match facade API +3/-1

Declare the multi-match facade API

• Adds a public matches() accessor so KPM consumers can retrieve every verified reference image rather than only the best match.

lib/SRC/KPM/FreakMatcher/facade/visual_database_facade.h

matcher_types.hDefine per-image verified match results +12/-1

Define per-image verified match results

• Introduces image_match_t to retain a reference image ID, inlier correspondences, and homography. Adds image_matches_t as the collection returned for each query.

lib/SRC/KPM/FreakMatcher/matchers/matcher_types.h

visual_database.hStore and expose multi-image query matches +10/-2

Store and expose multi-image query matches

• Adds the verified-match collection and its accessor to VisualDatabase while preserving matchedId(), inliers(), and matchedGeometry() as the single-best-match interface.

lib/SRC/KPM/FreakMatcher/matchers/visual_database.h

Bug fix (3) +58 / -43
visual_database-inline.hRetain every geometrically verified reference image +16/-5

Retain every geometrically verified reference image

• Clears multi-match state for each query and records every candidate meeting the inlier threshold. The legacy best-match fields remain populated with the strongest candidate for compatibility.

lib/SRC/KPM/FreakMatcher/matchers/visual_database-inline.h

kpmMatching.cppEstimate poses for every matched page +41/-38

Estimate poses for every matched page

• Maps database images to internal page indices, groups verified candidates by page, and tries each page's candidates in descending inlier order. Successful results now use the correct internal result index while retaining the page's external number.

lib/SRC/KPM/kpmMatching.cpp

kpmPrivate.hTrack database images by internal page index +1/-0

Track database images by internal page index

• Adds a database-image-to-page-index mapping so matching results address result[] independently of externally assigned page numbers.

lib/SRC/KPM/kpmPrivate.h

Tests (1) +2 / -2
webarkit_test.ccUpdate version assertions for WebARKit 0.9.0 +2/-2

Update version assertions for WebARKit 0.9.0

• Adjusts configuration and manager version expectations to verify the new 0.9.0 release identifier.

tests/webarkit_test.cc

Documentation (1) +18 / -1
CONTRIBUTING.mdDocument safe long-lived branch synchronization +18/-1

Document safe long-lived branch synchronization

• Corrects the target branch terminology and requires merge commits when synchronizing long-lived branches. It explains why squash or rebase merges break ancestry and cause repeated conflicts.

CONTRIBUTING.md

Other (5) +44 / -17
test.ymlRefresh CI actions, Node.js, and Emscripten +4/-4

Refresh CI actions, Node.js, and Emscripten

• Upgrades checkout and Node setup actions, moves CI to Node.js 22.x, and aligns the Emscripten container with the compiler used by the new OpenCV artifacts.

.github/workflows/test.yml

CMakeLists.txtSelect centralized native or Emscripten OpenCV artifacts +22/-10

Select centralized native or Emscripten OpenCV artifacts

• Loads shared OpenCV release coordinates, adds the WEBARKIT_SIMD option, and selects native, standard Emscripten, or SIMD Emscripten packages. Fetches are now protected by SHA-256 verification.

WebARKit/CMakeLists.txt

WebARKitConfig.cppBump the WebARKit runtime version to 0.9.0 +2/-2

Bump the WebARKit runtime version to 0.9.0

• Updates the exposed version string and minor-version constant from 0.8 to 0.9.

WebARKit/WebARKitTrackers/WebARKitOpticalTracking/WebARKitConfig.cpp

OpenCVEm.cmakeCentralize OpenCV 4.12 release coordinates +13/-0

Centralize OpenCV 4.12 release coordinates

• Defines the opencv-em 0.2.0 release URL and checksums for native, Emscripten, and SIMD Emscripten packages. This provides one upgrade point for production and test builds.

cmake/OpenCVEm.cmake

CMakeLists.txtUse shared verified OpenCV dependency settings +3/-1

Use shared verified OpenCV dependency settings

• Reuses the centralized native OpenCV 4.12 URL and SHA-256 hash instead of maintaining a separate hardcoded test dependency.

tests/CMakeLists.txt

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

qodo-free-for-open-source-projects Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Reloading a smaller dataset can crash ✓ Resolved 🐞 Bug ☼ Reliability
Description
kpmMatching() trusts every retained matcher ID when reading pageIndices and indexing result,
while kpmSetRefDataSet() reuses the existing visual database and only rewrites mappings for IDs
present in the newly loaded dataset. After replacing a dataset with fewer images, a stale higher-ID
keyframe that passes verification is processed by the new all-match loop and can resolve to an old
page index beyond the new result array.
Code

lib/SRC/KPM/kpmMatching.cpp[R653-654]

+for (const vision::image_match_t& imageMatch : imageMatches) {
+    candidatesPerPage[kpmHandle->pageIndices[imageMatch.id]].push_back(&imageMatch);
Evidence
The public API explicitly allows a later dataset load, but the setter only replaces KPM-owned
dataset and result storage before restarting database IDs at zero; it never clears freakMatcher.
The visual database rejects duplicate IDs without replacing existing keyframes, while the facade
still overwrites point data, and the new loop consumes every returned ID and indexes the newly sized
result array through its old page mapping.

include/KPM/kpm.h[223-241]
lib/SRC/KPM/kpmMatching.cpp[194-258]
lib/SRC/KPM/kpmMatching.cpp[272-300]
lib/SRC/KPM/FreakMatcher/matchers/visual_database-inline.h[139-151]
lib/SRC/KPM/FreakMatcher/facade/visual_database_facade.cpp[76-87]
lib/SRC/KPM/kpmMatching.cpp[651-660]

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

## Issue description
Reloading a reference dataset leaves old keyframes and 3D-point entries in the binary matcher. The new all-match loop processes those stale IDs and can index beyond the newly allocated result array.
## Fix Focus Areas
- lib/SRC/KPM/kpmMatching.cpp[242-300]
- lib/SRC/KPM/kpmMatching.cpp[651-660]
- lib/SRC/KPM/FreakMatcher/facade/visual_database_facade.cpp[42-87]
## Recommended Fix
Clear or recreate the binary visual database and its 3D-point map before registering a replacement reference dataset. Also validate every returned image ID and derived page index before using them to access `pageIndices` or `result`, returning or logging an explicit error when an invariant is violated.

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


2. Large reference sets corrupt memory 🐞 Bug ☼ Reliability
Description
kpmSetRefDataSet() writes pageIndices[db_id] once per reference image without checking
DB_IMAGE_MAX. When a dataset contains 1024 or more page images, the new mapping write runs beyond
the handle’s fixed array and later matching uses the corrupted mapping as a result index.
Code

lib/SRC/KPM/kpmMatching.cpp[298]

+                kpmHandle->pageIndices[db_id] = k;
Evidence
The new pageIndices mapping has the same fixed capacity as the existing page-ID mapping, but the
registration loop iterates every image declared by every page and increments db_id with no
capacity check. The matching path then dereferences the new mapping using matcher image IDs, making
the overflow affect result indexing.

lib/SRC/KPM/kpmPrivate.h[44-44]
lib/SRC/KPM/kpmPrivate.h[88-92]
lib/SRC/KPM/kpmMatching.cpp[275-300]
lib/SRC/KPM/kpmMatching.cpp[651-660]
include/KPM/kpm.h[78-109]

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

## Issue description
`kpmSetRefDataSet()` registers one matcher database ID per page image, but writes the new `pageIndices[db_id]` mapping into a fixed `DB_IMAGE_MAX` array without validating the aggregate image count. A dataset with at least 1024 images writes past the `KpmHandle` allocation, and `kpmMatching()` subsequently reads this mapping to index `result`.
## Fix Focus Areas
- lib/SRC/KPM/kpmMatching.cpp[275-300]
- lib/SRC/KPM/kpmPrivate.h[88-92]
- lib/SRC/KPM/kpmMatching.cpp[651-660]
## Recommended Fix
Before registering images, count the total `imageNum` values and reject datasets exceeding `DB_IMAGE_MAX` with an error, before writing either per-image mapping. Prefer replacing both fixed mapping arrays with vectors sized to the registered image count if supporting larger datasets is required; retain a bounds check before every mapping access.

ⓘ 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 reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread lib/SRC/KPM/kpmMatching.cpp Outdated
}
ARLOGi("page %d, image num %d, points - %d\n", k, m, points.size());
kpmHandle->pageIDs[db_id] = kpmHandle->refDataSet.pageInfo[k].pageNo;
kpmHandle->pageIndices[db_id] = k;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Large reference sets corrupt memory 🐞 Bug ☼ Reliability

kpmSetRefDataSet() writes pageIndices[db_id] once per reference image without checking
DB_IMAGE_MAX. When a dataset contains 1024 or more page images, the new mapping write runs beyond
the handle’s fixed array and later matching uses the corrupted mapping as a result index.
Agent Prompt
## Issue description
`kpmSetRefDataSet()` registers one matcher database ID per page image, but writes the new `pageIndices[db_id]` mapping into a fixed `DB_IMAGE_MAX` array without validating the aggregate image count. A dataset with at least 1024 images writes past the `KpmHandle` allocation, and `kpmMatching()` subsequently reads this mapping to index `result`.

## Fix Focus Areas
- lib/SRC/KPM/kpmMatching.cpp[275-300]
- lib/SRC/KPM/kpmPrivate.h[88-92]
- lib/SRC/KPM/kpmMatching.cpp[651-660]

## Recommended Fix
Before registering images, count the total `imageNum` values and reject datasets exceeding `DB_IMAGE_MAX` with an error, before writing either per-image mapping. Prefer replacing both fixed mapping arrays with vectors sized to the registered image count if supporting larger datasets is required; retain a bounds check before every mapping access.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and fixed in #74 (86b61a0). pageIDs[] and pageIndices[] hold DB_IMAGE_MAX (1024) entries, one per registered image (pages × scales), and nothing checked the total. kpmSetRefDataSet() now counts the images first and rejects the dataset with an ARLOGe before changing any state, so neither array can be overrun. pageIDs[] was already unguarded before #71. Moving to vectors would lift the limit, but the bound is enough for the realistic sizes (jsartoolkitNFT caps markers at PAGES_MAX = 20).

kalwalt and others added 2 commits September 24, 2026 00:10
Two review findings on #73 (pre-existing, widened by #71's per-page
results):

- kpmSetRefDataSet() never clears the FREAK matcher, so when a
  reference set is replaced by a smaller one its extra images stay in
  the matcher and are still matched. Their ids keep old pageIndices
  entries that can point past the new, smaller result[]. Record how
  many images the current set registered (dbImageNum) and ignore, with
  an error, any match at or above it, or mapping outside result[].
  Clearing the matcher on reload is the proper fix and belongs to the
  non-idempotent kpmSetRefDataSet (webarkit/jsartoolkitNFT#612).

- pageIDs[] and pageIndices[] hold DB_IMAGE_MAX (1024) entries, one per
  registered image, but nothing bounded the image count. Reject a
  dataset with more images than that before changing any state.

Built and tested through jsartoolkitNFT (the only build that compiles
lib/SRC/KPM): detection, multi-marker and marker-limit suites, 82/82.

Refs webarkit/jsartoolkitNFT#635, webarkit/jsartoolkitNFT#612

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on #74:

- Reusing the matcher across kpmSetRefDataSet() calls kept every
  keyframe it held. An id registered again was refused as a duplicate
  (the old keyframe stayed) while the facade still replaced its 3D
  points, so pose estimation could pair old matches with a new, shorter
  point list and index past it. The dbImageNum check cannot see this:
  the id is in range. Recreate the matcher before registering the new
  set; it carries no configuration beyond construction, so the new
  instance is equivalent. The id and page-position checks in
  kpmMatching stay as invariant guards.

- The DB_IMAGE_MAX preflight summed imageNum values in a signed int
  before comparing, so a corrupt dataset could overflow past the check.
  Check each count against the room left before adding it, and reject
  negative counts.

Built and tested through jsartoolkitNFT: detection, multi-marker and
marker-limit Vitest suites 82/82; Node example detects 10/10.

Refs webarkit/jsartoolkitNFT#612, webarkit/jsartoolkitNFT#635

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kalwalt
kalwalt merged commit fe4b069 into master Sep 23, 2026
1 check passed
kalwalt added a commit to webarkit/jsartoolkitNFT that referenced this pull request Sep 23, 2026
Moves the submodule from dev 5cbf69d to the WebARKitLib 0.9.0 tag
(fe4b069, the merge of webarkit/WebARKitLib#73 into master). The tag
keeps 5cbf69d in its history and its tree is identical, so the compiled
sources and the committed build/ and dist/ are unchanged.

Refs #635

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kalwalt added a commit to webarkit/jsartoolkitNFT that referenced this pull request Sep 24, 2026
* doc: design for multi-NFT-marker tracking

Records the decisions behind multi-marker support, building on the
2026-09-22 plan which holds the evidence.

Adds two corrections the plan does not account for: detection is gated
on detectedPage == -2, so #635 alone changes nothing until that gate
goes; and the _td variant is a separate architecture whose
trackingInitGetResult carries a single page by signature.

Four phases: #631 threshold, #635 multi-result, the JS/native
multi-marker change for main and simd, then _td. #612 is out of scope.

Refs #635, #631, #613, #611

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

* doc: implementation plan for multi-NFT-marker tracking

Refs #635, #631, #613, #611

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

* doc: record #631 measurements — kuva is in the test photo

The inlier-ratio measurements found no separating threshold because
there is no false positive: pinball-demo.jpg carries both printed
targets, so kuva's matches are real detections.

Refs #631

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

* feat: track every visible NFT marker, not only the first

The binding keeps a tracking state per marker instead of one
detectedPage/ftmi pair. KPM runs while any loaded marker is untracked,
and tracking happens once per frame in detectNFTMarker(), so
getNFTMarker(i) is now a pure read. Bumps WebARKitLib for per-page KPM
results. Adds a multi-marker suite over the default and SIMD builds.
Rebuilt build/ and dist/ included.

Rewrites the two #631 detection tests: pinball-demo.jpg carries both
printed targets, so both markers are expected, and the tests now pass.

Refs #635, #613
Refs #631

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

* fix: reject NFT marker loads beyond PAGES_MAX

The binding keeps per-marker state in fixed arrays of PAGES_MAX entries
(surfaceSet, and since the previous commit markerStates), and the
per-frame loops index them up to surfaceSetCount. addNFTMarkers()
checked only the size of the current batch, while surfaceSetCount
accumulates across calls, so repeated loads could push it past
PAGES_MAX and overrun those arrays and the skipPages stack buffer.

addNFTMarkers() now refuses a load when the running total would exceed
PAGES_MAX, before any state changes. The check replaces the per-batch
one, which also rejected a single batch of exactly PAGES_MAX markers
although that fits. Adds a marker-limit test over the default and SIMD
builds. Rebuilt build/ and dist/ included.

Refs #613

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

* feat: report each NFT marker lost on its own

The controllers tracked found/lost with one index and one timestamp, so
with two markers in view only one could be reported lost. A
MarkerLostTracker now does this per marker, and lostNFTMarker carries
that marker's last pose. Each getNFTMarker event also gets its own
matrix instead of a shared buffer the next marker overwrites. Applied
to the default, SIMD, threaded and Node controllers. Rebuilt dist/
included.

Refs #611

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

* feat(parallel): track every visible NFT marker in the threaded build

The detection worker reported only its single best page, and the
threaded getter reported every marker index as found whenever any page
was tracked. The worker now returns every matched page; detection and
tracking run once per frame in detectNFTMarker() with the per-marker
state shared with the default build. The multi-marker suite now covers
the threaded build, with the test page cross-origin isolated.
Also bounds the threaded addNFTMarkers by PAGES_MAX in total, returning
an empty result instead of calling exit(). Keeps trackingInitGetResult
as a compatibility wrapper for the legacy threaded binding; the threaded
tests run on a half-scale frame because that build's fixed 128 MB heap
cannot hold 2000x1500 frames.
Rebuilt build/ and dist/ included.

Refs #635, #613, #611

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

* doc: record multi-marker implementation notes and KPM cost

Refs #635

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

* feat: throttle NFT detection while markers are tracked

With several markers loaded and one in view, KPM ran on every frame
while any loaded marker was untracked: about 320 ms per frame at
2000x1500, where 1.12.0 stopped detecting once a marker was tracked.
Detection now runs on every frame only while nothing is tracked. While
some markers are tracked and some are not, it runs at most once per
interval, measured in milliseconds; while all are tracked it does not
run. Two new setters on every controller: setContinuousDetection(),
default true (false restores the 1.12.0 behaviour), and
setDetectionInterval(ms), default 300 ms in the default and SIMD builds.
The threaded build keeps its behaviour (interval 0, detection on the
worker) and applies the same gate to starting a worker search. The Node
controller gets both setters for API parity; its single-marker binding
cannot honour them, so they only warn once.

New tests cover a marker entering while another is held, continuous
detection off, and the interval, on all three browser builds.

Refs #635

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

* fix: pick the best KPM result in the legacy bindings

Since KPM reports one result per matched page, the single-marker loops
in ARToolKitJS.cpp, ARToolKitJS_td.cpp (commented-out code) and the
Python binding set detectedPage for every matched page, so the last page
won. With pinball and kuva both loaded, the Node build locked on kuva
(page 1, 32 inliers) instead of pinball (page 0, 36 inliers). Each loop
now picks one result: the most inliers, ties broken by the lowest error.

trackingInitGetResult, the single-page wrapper the legacy threaded
binding uses, chose the lowest error; it now uses the same rule, the one
the single-result KPM used, and TrackingInitResult carries inlierNum.
The detection worker also fetches the KPM result array after every
kpmMatching() instead of once at start-up, since kpmSetRefDataSet()
reallocates it when markers are loaded.

Refs #635

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

* fix(parallel): stop logging an error before markers load

detectNFTMarker() logged "Error: threadHandle" on every process() until
addNFTMarkers() created the detection worker. With no markers loaded it
now returns -1 quietly; the error is kept for markers loaded without a
worker.

Refs #635

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

* chore: rebuild build/ and dist/ for the detection policy and KPM fixes

Full Docker build (emsdk 4.0.17) and build-ts of the three previous
commits: the detection policy setters and throttle, the best-result
choice in the legacy bindings and the detection worker, and the quiet
threaded detectNFTMarker() before markers load.

Refs #635

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

* test: keep the KPM cost harness as a skipped suite

tests/vitest/kpm-cost.test.ts times process() on the default build at
camera-like sizes, with an unseen marker loaded, under detection on
every frame and under the 300 ms default. It measures rather than
asserts, so it is committed as describe.skip; its header says how to
run it. At 320x240 pinball is too small in the test photo to be
detected, so 340x255 stands in for it.

Refs #635

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

* doc: record the detection policy, its cost, and multi-marker usage

The design spec gets the detection policy the maintainer chose (time
throttle, 300 ms default, 0 in the threaded build, and the two setters),
the detection cost measured at 340x255 and 640x480, a dated correction
of the claim that skipped pages make KPM cheaper (the FREAK matcher
still matches every keyframe), and why KpmProcHalfSize is not a safe
way to cut the cost. The README gets a multi-marker tracking section,
including the note that getTransformationMatrix() now returns a fresh
array each frame.

Refs #635, #613, #611

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

* fix: count the detection interval from the end of a pass

The interval was measured from the start of one detection pass to the
start of the next. A pass slower than the interval (about 320 ms at
2000x1500 against the 300 ms default) was therefore due again on the
very next frame, so large frames paid detection on every frame.

It is now counted from when a pass finishes (threaded build: when a
finished search is collected), so every pass is followed by
tracking-only frames whatever the frame size. A new test pins this
with a 50 ms interval, far shorter than a pass: before the fix no frame
was tracking-only. Rebuilt build/ and dist/ included.

Refs #635

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

* fix: bump WebARKitLib for KPM result indexing and pose fallback

Picks up webarkit/WebARKitLib#71 review fixes (f3d4959):
- KPM results are indexed by the page's position in the reference data
  set rather than by its external page number, which only coincided
  for pages numbered 0..n-1 in load order;
- if the pose of a page's best-supported image cannot be fitted, the
  page's other verified images are tried instead of dropping the page.

Rebuilt build/ and dist/ included.

Refs #635

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

* test: require both markers from a single detection pass

"finds both markers in the same frame" pushed frames until both were
tracked, so a matcher returning one page per pass (#635) could pass it
over two passes. The new test loses both markers first, then requires
that the first process() finding any marker finds both. The old test is
renamed to what it proves: "tracks both markers at once".

Refs #635

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

* fix: pass only the marker index to getNFTData in the examples

getNFTData(index) takes one argument; the controller id is implied.
The examples called getNFTData(ar.id, i), so the controller id became
the index and the real index was dropped. Every call returned marker 0:
the multi-marker worker centred kuva's and chalk_multi's models using
pinball's size (893x1117 @120 dpi instead of 640x480 @72), shifting
them off their markers. Single-marker examples only worked because
the controller id happened to be 0.

Refs #613

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

* fix: stand the multi example's cone upright on its marker

cone.rotation.x = 90 is in radians (about 116.6 degrees), tilting the
cone ~27 degrees off vertical. Use Math.PI / 2, and lift the cone by
half its height, since ConeGeometry is centred on its mid-height and
was half sunk into the marker.

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

* feat: show every tracked marker at once in the ES6 multi example

The worker kept one result per frame, so each getNFTMarker event
overwrote the previous one, and one OneEuroFilter was shared by all
markers and reset whenever any was lost. It now reports every tracked
marker's pose per frame, with a filter and warm-up count per marker,
and sends each marker's own getNFTData. The page draws each model
under its own root at its own marker's pose, and stands the cone
upright on its marker.

Refs #611, #613

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

* fix: serve the Pthread examples reliably from python-server.py

python-server.py could not serve the threaded examples: pages failed
with net::ERR_CONNECTION_RESET (status 200) on the large scripts, and
the server stopped answering altogether.

- It was single-threaded, so one idle connection (a browser preconnect)
  blocked every other request. Use ThreadingHTTPServer.
- It spoke HTTP/1.0 and closed the socket after every response; on
  Windows that close could reset the connection before a large file was
  fully delivered. Use HTTP/1.1 keep-alive (Content-Length is always
  sent).

Measured with 3 idle connections held open plus 12 parallel downloads
of build/artoolkitNFT_thread.js and three.module.min.js: the old server
completed 0/12 and then hung; threading alone, 4-11/12; threading plus
HTTP/1.1, 12/12 in 5 of 5 runs. basic_threading.html now loads every
file and is cross-origin isolated.

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

* fix: keep the threaded examples' model on the marker on mobile

setCameraMatrix() runs on every frame and scaled camera_matrix in place
by ratioW/ratioH. Whenever the camera frame is not 4:3 one of those
ratios is not 1, so the projection compounded frame after frame until
the model was projected out of view: it flashed, then disappeared,
while tracking and the pose stayed correct. On a 4:3 desktop webcam
both ratios are exactly 1, which is why only phones were affected.

Scale a copy instead. Confirmed on an Android phone (OPPO A72) with
ARToolkitNFT_ES6_threading_example.html: the model now stays on the
marker. The other example pages already build their projection from a
fresh JSON.parse(msg.proj) once, so only this shared threaded page was
affected.

Refs #391

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

* chore: stop logging the pose on every frame in the threaded examples

threejs_wasm_thread.js logged the full 16-element world matrix on each
animation frame, flooding the console while a marker was tracked.

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

* chore: point WebARKitLib at the merged dev commit

webarkit/WebARKitLib#71 was squash-merged into dev as 4b5af7b. Its tree
is identical to f3d4959, which this branch pointed at, so the compiled
sources and the committed build/ and dist/ are unchanged; only the
pointer moves to a commit that is on WebARKitLib's dev history.

Refs #635

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

* fix: bump WebARKitLib for KPM matcher and image-count bounds

Points the submodule at WebARKitLib dev 5cbf69d, which adds (#74):
- kpmSetRefDataSet() registers each reference set in a fresh matcher,
  so a reloaded set can no longer pair old keyframes with new, shorter
  3D point lists (the WebARKitLib side of #612);
- a DB_IMAGE_MAX bound on the reference image count, checked before
  any state changes and safe against overflow;
- invariant checks on matcher image ids and page positions in
  kpmMatching;
plus the WebARKitLib 0.9.0 version bump (#72), which is not compiled
into these builds.

Rebuilt build/ and dist/ in Docker (emsdk 4.0.17). npm test: Vitest 118
passed, 13 skipped; all 7 Karma targets pass. The Node example detects.

Refs #612, #635

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

* chore: point WebARKitLib at the 0.9.0 release

Moves the submodule from dev 5cbf69d to the WebARKitLib 0.9.0 tag
(fe4b069, the merge of webarkit/WebARKitLib#73 into master). The tag
keeps 5cbf69d in its history and its tree is identical, so the compiled
sources and the committed build/ and dist/ are unchanged.

Refs #635

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@kalwalt kalwalt mentioned this pull request Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant