Skip to content

feat(kpm): report every matched page, not only the best one - #71

Merged
kalwalt merged 2 commits into
devfrom
feat/multi-nft-marker
Sep 23, 2026
Merged

kalwalt merged 2 commits into
devfrom
feat/multi-nft-marker

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 23, 2026

Copy link
Copy Markdown
Member

Summary

KPM's FREAK path could report one matched page per frame: VisualDatabase::query() kept a running maximum, and kpmMatching wrote a single result. This change makes it report every page that passes the matcher's tests, with one pose per page. That is the native half of multi-NFT-marker tracking in jsartoolkitNFT.

Companion PR: webarkit/jsartoolkitNFT#658. It bumps this submodule to this branch and makes the bindings track every reported page.

What changed (lib/SRC/KPM/)

  • matchers/matcher_types.h: new image_match_t { id, inliers, geometry[9] } and image_matches_t.
  • matchers/visual_database{.h,-inline.h}:
    • query() now collects every reference image that clears the existing inlier floor into mMatches, exposed as matches().
    • matchedId(), inliers() and matchedGeometry() are unchanged: they still return the single best match, so existing callers are unaffected.
  • facade/visual_database_facade.{h,cpp}: forwards matches() across the pimpl boundary.
  • kpmMatching.cpp:
    • Loops the matches and writes one pose per matched page.
    • Each page is registered as several keyframes (one per image scale; see kpmSetRefDataSet), so it keeps the best-supported keyframe per page.
    • The dead, commented-out per-page loop is removed. It called matchedId() inside the loop, so it gave every page the same id.

No thresholds changed. The inlier floor (kMinNumInliers = 8) is as before.

Behaviour change for single-result consumers

Consumers that looped over every camPoseF == 0 result and kept the last one used to be safe, because there was only ever one. Now they would pick the highest page number, not the best match. jsartoolkitNFT#658 updates its legacy bindings to pick the result with the most inliers (ties to the lowest error). Other consumers of kpmGetResult should do the same.

A note on lib/SRC/**

CONTRIBUTING.md asks contributors not to modify the vendored ARToolKit5 sources under lib/SRC/**. This change does, because the KPM matcher lives there and jsartoolkitNFT consumes it. It follows the precedent of #67 (fix/kpm-matched-id-sentinel), which changed lib/SRC/KPM/kpmMatching.cpp for the same consumer. The parked WebARKit/NFT relocation would move this code out of lib/SRC/; until then, KPM fixes land here.

Testing

Verified end-to-end through jsartoolkitNFT, since this repo's gtest suite does not cover KPM:

  • One KPM pass over a photo containing both the pinball and kuva targets now reports Page[0] and Page[1] (previously only the best).
  • jsartoolkitNFT's full suite passes against this commit: Vitest 112 passed, all 7 Karma targets green, and the Node example detects.

Refs webarkit/jsartoolkitNFT#635

🤖 Generated with Claude Code

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>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Report every matched KPM page with an independent pose

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Retains every geometrically verified reference-image match while preserving legacy best-match
 accessors.
• Groups scaled keyframes by page and estimates one pose for each matched page.
• Enables multi-marker KPM output without changing the existing eight-inlier threshold.
Diagram

graph TD
  Q["Query frame"] --> V["Visual database"] --> T{"Inlier threshold"} -->|Pass| M["Image matches"] --> G["Best per page"] --> P["Pose estimation"] --> R["KPM results"]
Loading
High-Level Assessment

The chosen approach is appropriate: retaining image-level matches keeps VisualDatabase generic, while page-level deduplication remains in KPM where keyframe-to-page mappings are known. Preserving matchedId(), inliers(), and matchedGeometry() also minimizes compatibility risk compared with replacing the existing single-best-match contract.

Files changed (6) +78 / -52

Enhancement (6) +78 / -52
visual_database_facade.cppForward all verified matches through the facade +5/-1

Forward all verified matches through the facade

• Implements the facade accessor that exposes the visual database's complete collection of geometrically verified image matches across the pimpl boundary.

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

visual_database_facade.hExpose multi-match results in the facade API +3/-1

Expose multi-match results in the facade API

• Declares the matches() accessor so KPM consumers can retrieve every accepted reference-image match while retaining existing best-match methods.

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

matcher_types.hDefine reference-image match result types +12/-1

Define reference-image match result types

• Adds image_match_t to associate a reference image ID with its 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-inline.hCollect every geometrically verified image match +16/-5

Collect every geometrically verified image match

• Clears the multi-match collection at the start of each query and records every reference image meeting the existing inlier threshold. The strongest candidate continues to populate the legacy single-best-match fields.

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

visual_database.hStore and expose all accepted database matches +10/-2

Store and expose all accepted database matches

• Adds query-scoped storage and a read-only accessor for all verified reference-image matches. Documents that legacy accessors continue to describe only the strongest match.

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

kpmMatching.cppProduce one pose for every matched page +32/-42

Produce one pose for every matched page

• Groups accepted image matches by page, retains the keyframe with the most inliers for each page, and estimates a separate pose for every unskipped page. Removes the obsolete commented single-match page loop and adds aggregate match logging.

lib/SRC/KPM/kpmMatching.cpp

@kalwalt kalwalt self-assigned this Sep 23, 2026
@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 (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Sparse page numbers corrupt results ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new multi-page loop uses the external pageNo from pageIDs directly as an index into the
densely allocated result array. When page numbers are nonzero, sparse, negative, or otherwise
differ from their positions in pageInfo, matching those pages accesses the wrong slot or memory
outside the allocation, and the new all-page behavior exposes this for every secondary match too.
Code

lib/SRC/KPM/kpmMatching.cpp[R659-660]

+    // result[] is indexed by page number, as it was before this change.
+    if (kpmHandle->result[pageNo].skipF) continue;
Evidence
result has exactly pageNum dense entries, while each matcher image is mapped to the independent
external pageInfo[k].pageNo; skip flags also demonstrate that result indexing is by k, not by
that external value. The changed loop nevertheless uses the external value for every result[]
access.

lib/SRC/KPM/kpmMatching.cpp[239-250]
lib/SRC/KPM/kpmMatching.cpp[270-298]
lib/SRC/KPM/kpmMatching.cpp[344-353]
lib/SRC/KPM/kpmMatching.cpp[646-672]

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 multi-page matching path indexes `result` by the dataset's external page number, although `result` is allocated and managed by dense page position. Arbitrary page numbers therefore select the wrong result or access outside the allocation.
## Fix Focus Areas
- lib/SRC/KPM/kpmMatching.cpp[239-250]
- lib/SRC/KPM/kpmMatching.cpp[270-298]
- lib/SRC/KPM/kpmMatching.cpp[646-672]
- lib/SRC/KPM/kpmMatching.cpp[344-353]
## Recommended Fix
Map each database image ID to its dense `pageInfo` index, group and index `result` with that dense index, and write the corresponding external `pageInfo[index].pageNo` into `KpmResult::pageNo`. Keep skip handling keyed to the same dense result slot.

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



Remediation recommended

2. Valid page poses can be discarded ✓ Resolved 🐞 Bug ≡ Correctness
Description
The bestPerPage reduction discards every same-page keyframe except the one with the largest inlier
count before calling kpmUtilGetPose_binary. If initialization or pose fitting fails for that
candidate, another geometrically verified keyframe that could produce a valid pose is never
attempted, leaving that page unreported.
Code

lib/SRC/KPM/kpmMatching.cpp[R649-651]

+    auto best = bestPerPage.find(pageNo);
+    if (best == bestPerPage.end() || imageMatch.inliers.size() > best->second->inliers.size()) {
+        bestPerPage[pageNo] = &imageMatch;
Evidence
The matcher now retains all geometrically verified image matches, but the changed KPM loop selects
only one per page by inlier count and invokes pose estimation only for that selection.
kpmUtilGetPose_binary can still return failure during planar initialization, handle creation, or
pose fitting, and the caller has no fallback path to the discarded candidates.

lib/SRC/KPM/FreakMatcher/matchers/visual_database-inline.h[341-353]
lib/SRC/KPM/kpmMatching.cpp[646-671]
lib/SRC/KPM/kpmMatching.cpp[698-780]

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

## Issue description
Same-page matches are reduced by inlier count before pose estimation, so failure of the selected candidate suppresses all alternatives. Pose validity and error must be considered before choosing the page's result.
## Fix Focus Areas
- lib/SRC/KPM/kpmMatching.cpp[646-671]
- lib/SRC/KPM/kpmMatching.cpp[698-790]
## Recommended Fix
Group all image matches by page, run pose estimation for each candidate into temporary pose and error storage, discard failed estimates, and commit the best successful candidate for each page. Rank successful candidates by the intended quality policy, such as inlier count followed by lower pose error.

ⓘ 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
Comment thread lib/SRC/KPM/kpmMatching.cpp Outdated
@kalwalt kalwalt added enhancement New feature or request C/C++ code concerning the C/C++ code design and improvements Emscripten labels Sep 23, 2026
@kalwalt kalwalt moved this from To do to Review in progress in New markerless image tracking Sep 23, 2026
…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>
kalwalt added a commit to webarkit/jsartoolkitNFT that referenced this pull request Sep 23, 2026
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>
@kalwalt
kalwalt merged commit 4b5af7b into dev Sep 23, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Review in progress to Done in New markerless image tracking Sep 23, 2026
kalwalt added a commit to webarkit/jsartoolkitNFT that referenced this pull request Sep 23, 2026
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>
kalwalt added a commit that referenced this pull request Sep 23, 2026
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>
kalwalt added a commit that referenced this pull request Sep 23, 2026
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C/C++ code concerning the C/C++ code design and improvements Emscripten enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant