gps-plus-slam-app-framework 1.3.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/absolute-orientation-CAEh840t.d.ts +54 -0
- package/dist/{app-selectors-B8VQoMDY.d.ts → app-selectors-ikTUQZpe.d.ts} +1 -1
- package/dist/ar/bresenham3d.d.ts +1 -1
- package/dist/ar/camera-blit-capture.d.ts +2 -2
- package/dist/ar/camera-blit-capture.js +74 -9
- package/dist/ar/capability-checker.d.ts +1 -1
- package/dist/ar/capture-failure-tracker.d.ts +1 -1
- package/dist/ar/chromium-camera-access-workaround.d.ts +1 -1
- package/dist/ar/depth-grid-lookup.d.ts +2 -0
- package/dist/ar/depth-grid-lookup.js +43 -0
- package/dist/ar/depth-sampler.d.ts +2 -2
- package/dist/ar/depth-sampler.js +2 -4
- package/dist/ar/depth-unprojection.d.ts +1 -1
- package/dist/ar/enable-gps-ar.d.ts +1 -1
- package/dist/ar/enable-gps-ar.js +26 -1
- package/dist/ar/frame-loop.d.ts +1 -1
- package/dist/ar/image-capture.d.ts +2 -2
- package/dist/ar/image-capture.js +1 -154
- package/dist/ar/image-quality.d.ts +2 -0
- package/dist/ar/image-quality.js +168 -0
- package/dist/ar/index.d.ts +31 -19
- package/dist/ar/index.js +17 -5
- package/dist/ar/occupancy-grid.d.ts +1 -1
- package/dist/ar/occupancy-grid.js +29 -2
- package/dist/ar/planar-pnp.d.ts +2 -0
- package/dist/ar/planar-pnp.js +425 -0
- package/dist/ar/qr-debug-view.d.ts +2 -0
- package/dist/ar/qr-debug-view.js +89 -0
- package/dist/ar/qr-derived-pose.d.ts +2 -0
- package/dist/ar/qr-derived-pose.js +114 -0
- package/dist/ar/qr-detection-controller.d.ts +2 -0
- package/dist/ar/qr-detection-controller.js +2 -0
- package/dist/ar/qr-frontend.d.ts +2 -0
- package/dist/ar/qr-frontend.js +60 -0
- package/dist/ar/qr-pose.d.ts +2 -0
- package/dist/ar/qr-pose.js +267 -0
- package/dist/ar/qr-size-depth-context.d.ts +3 -0
- package/dist/ar/qr-size-depth-context.js +42 -0
- package/dist/ar/qr-size-measurer.d.ts +2 -0
- package/dist/ar/qr-size-measurer.js +2 -0
- package/dist/ar/replay-scene.d.ts +1 -1
- package/dist/ar/replay-scene.js +1 -1
- package/dist/ar/scene-node-names.d.ts +1 -1
- package/dist/ar/webxr-nue-basis.d.ts +1 -1
- package/dist/ar/webxr-session.d.ts +4 -4
- package/dist/ar/webxr-session.js +2 -2
- package/dist/ar/xr-camera-texture.d.ts +1 -1
- package/dist/ar/xr-error-handler.d.ts +1 -1
- package/dist/ar/xr-frame-loop.d.ts +1 -1
- package/dist/ar-MFpFlV7j.js +382 -0
- package/dist/{ar-world-group-alignment-C9lwwIyI.d.ts → ar-world-group-alignment-69fgnB39.d.ts} +1 -1
- package/dist/{camera-blit-capture-CnxUDbKx.d.ts → camera-blit-capture-NKT9H5-e.d.ts} +57 -4
- package/dist/capture-motion-gate-0Yl2NpQG.d.ts +24 -0
- package/dist/capture-motion-gate-4EscNZCw.js +94 -0
- package/dist/concurrency-By0oOTZt.d.ts +50 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/create-slam-app-store-DkCHOpvb.js +149 -0
- package/dist/{create-slam-app-store-BljusOua.d.ts → create-slam-app-store-n_VF63D5.d.ts} +50 -40
- package/dist/depth-grid-lookup-DQ-IYhO3.d.ts +21 -0
- package/dist/{depth-sampler-Bhanfwla.d.ts → depth-sampler-asc8exHt.d.ts} +1 -1
- package/dist/{depth-unprojection-Y6i_mJuT.d.ts → depth-unprojection-DjsbJW9L.d.ts} +1 -1
- package/dist/{enable-gps-ar-BfUJOgpW.d.ts → enable-gps-ar-DkNhFv4Y.d.ts} +4 -4
- package/dist/{frame-conversions-BfsafGWR.d.ts → frame-conversions-DObhj8Wf.d.ts} +1 -1
- package/dist/{fused-path-DJoVu98b.d.ts → fused-path-v6eOmLl3.d.ts} +1 -1
- package/dist/geo/h3-proximity.d.ts +2 -2
- package/dist/geo/h3-proximity.js +61 -2
- package/dist/geo/index.d.ts +2 -2
- package/dist/geo/index.js +2 -2
- package/dist/geodesic-angle-BTldfFgs.js +39 -0
- package/dist/{gps-anchor-dYeCvvp5.d.ts → gps-anchor-BeFO2RYa.d.ts} +1 -1
- package/dist/{gps-ar-pose-sampler-DVb96iOy.d.ts → gps-ar-pose-sampler-DHMZmYAc.d.ts} +1 -1
- package/dist/{gps-event-coordinator-CWIDOk4U.d.ts → gps-event-coordinator-CjWW636x.d.ts} +21 -11
- package/dist/{gps-event-markers-Dc9FT-q1.d.ts → gps-event-markers-Cz9RMA2k.d.ts} +18 -0
- package/dist/{h3-proximity-I1msGPgE.d.ts → h3-proximity-C-j0pw1j.d.ts} +39 -1
- package/dist/image-capture-B2-b6tKK.js +374 -0
- package/dist/{image-capture-qAZ5Fh5X.d.ts → image-capture-CNZDrQXG.d.ts} +103 -3
- package/dist/image-quality-smCBoFj1.d.ts +158 -0
- package/dist/index-BAPokxMb.d.ts +22 -0
- package/dist/{index-BYYUdf9v.d.ts → index-BODDUTvD.d.ts} +1 -1
- package/dist/index-BneSj4_5.d.ts +399 -0
- package/dist/index-_Rhgoqkh.d.ts +2 -0
- package/dist/index.d.ts +80 -67
- package/dist/index.js +24 -12
- package/dist/{leaflet-map-overlay-6CJC-E1l.d.ts → leaflet-map-overlay-CmIyiE8n.d.ts} +1 -1
- package/dist/{map-data-CkloBxIl.d.ts → map-data-B4oEehUh.d.ts} +1 -1
- package/dist/{map-overlay-draw-krOKwDAq.d.ts → map-overlay-draw-ymz1s7fG.d.ts} +1 -1
- package/dist/{null-storage-backend-uKQE8tl7.d.ts → null-storage-backend-B9rtyDfG.d.ts} +2 -2
- package/dist/{occupancy-grid-BRNRaPyT.d.ts → occupancy-grid-DnOc-6PU.d.ts} +12 -2
- package/dist/{opfs-storage-D5zAn_lx.d.ts → opfs-storage-3VZh-qH6.d.ts} +23 -4
- package/dist/{opfs-storage-backend-6aYMUZlJ.d.ts → opfs-storage-backend-JJiUJo-3.d.ts} +3 -3
- package/dist/{persistence-middleware-B9ei4Nhx.d.ts → persistence-middleware-DAPTTwB2.d.ts} +1 -1
- package/dist/planar-pnp-my9bqULS.d.ts +67 -0
- package/dist/qr-debug-view-Brl73HGO.d.ts +29 -0
- package/dist/qr-derived-pose-CmxTP2m4.d.ts +122 -0
- package/dist/qr-detected-slice-VQssZmQJ.d.ts +166 -0
- package/dist/qr-detection-controller-B-tLVd-S.js +119 -0
- package/dist/qr-detection-controller-CDurHv8E.d.ts +48 -0
- package/dist/qr-frontend--fFX25mV.d.ts +58 -0
- package/dist/qr-pose-D2rfu5DA.d.ts +138 -0
- package/dist/qr-pose-aggregation-B7tWJq7j.d.ts +90 -0
- package/dist/qr-pose-aggregation-DBiaLW0O.js +191 -0
- package/dist/qr-size-depth-context-CeAG53fy.d.ts +13 -0
- package/dist/qr-size-measurer-DLb33geH.js +596 -0
- package/dist/qr-size-measurer-DQdAsid1.d.ts +165 -0
- package/dist/recording-options-BPCpMHLK.d.ts +514 -0
- package/dist/{recording-replayer-r0DZ9ALp.d.ts → recording-replayer-DQKpAGyQ.d.ts} +2 -2
- package/dist/recording-slice-DaxHZvNu.d.ts +55 -0
- package/dist/{replay-engine-XpDa0dy1.d.ts → replay-engine-CmKh_4wf.d.ts} +7 -4
- package/dist/{replay-scene-Bw58VL6p.d.ts → replay-scene-DBlDrV_x.d.ts} +3 -3
- package/dist/sensors/absolute-orientation.d.ts +2 -0
- package/dist/sensors/absolute-orientation.js +145 -0
- package/dist/sensors/gps-error-handler.d.ts +1 -1
- package/dist/sensors/gps.d.ts +1 -1
- package/dist/sensors/index.d.ts +5 -4
- package/dist/sensors/index.js +2 -1
- package/dist/sensors/permission-checker.d.ts +1 -1
- package/dist/sensors/permission-checker.js +1 -1
- package/dist/state/app-selectors.d.ts +1 -1
- package/dist/state/combined-root-state.d.ts +1 -1
- package/dist/state/create-slam-app-store.d.ts +1 -1
- package/dist/state/create-slam-app-store.js +1 -74
- package/dist/state/gps-ar-pose-sampler.d.ts +3 -3
- package/dist/state/gps-event-coordinator.d.ts +2 -2
- package/dist/state/gps-event-coordinator.js +27 -17
- package/dist/state/index.d.ts +20 -17
- package/dist/state/index.js +5 -4
- package/dist/state/persistence-middleware.d.ts +1 -1
- package/dist/state/persistence-middleware.js +30 -19
- package/dist/state/qr-detected-slice.d.ts +4 -0
- package/dist/state/qr-detected-slice.js +311 -0
- package/dist/state/recording-options.d.ts +2 -2
- package/dist/state/recording-options.js +273 -7
- package/dist/state/recording-replayer.d.ts +2 -2
- package/dist/state/recording-replayer.js +1 -1
- package/dist/state/recording-slice.d.ts +1 -1
- package/dist/state/recording-slice.js +5 -1
- package/dist/state/replay-engine.d.ts +1 -1
- package/dist/state/replay-engine.js +7 -3
- package/dist/state/store-subscribers.d.ts +2 -2
- package/dist/state/subscribe-to-selector.d.ts +1 -1
- package/dist/state/tracking-quality.d.ts +2 -2
- package/dist/state/tracking-quality.js +40 -128
- package/dist/state/tracking-slice.d.ts +1 -1
- package/dist/{state-BkIfpiyA.js → state-E51QT_-j.js} +2 -1
- package/dist/storage/file-system-utils.d.ts +1 -1
- package/dist/storage/index.d.ts +9 -9
- package/dist/storage/index.js +4 -4
- package/dist/storage/null-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.d.ts +1 -1
- package/dist/storage/opfs-storage-backend.js +3 -3
- package/dist/storage/opfs-storage.d.ts +1 -1
- package/dist/storage/opfs-storage.js +5 -4
- package/dist/storage/storage-backend.d.ts +1 -1
- package/dist/storage/zip-coverage-embed.d.ts +2 -0
- package/dist/storage/zip-coverage-embed.js +88 -0
- package/dist/storage/zip-export.d.ts +2 -2
- package/dist/storage/zip-export.js +37 -59
- package/dist/storage/zip-reader.d.ts +1 -1
- package/dist/{storage-backend-Do6YDpNo.d.ts → storage-backend-BdVLD0dU.d.ts} +1 -1
- package/dist/{store-subscribers-0YWyOLvV.d.ts → store-subscribers-CA6AJH_8.d.ts} +2 -2
- package/dist/{subscribe-to-selector-CZ6k1NFP.d.ts → subscribe-to-selector-B6EUmQ3I.d.ts} +1 -1
- package/dist/test-utils/browser-mocks.d.ts +7 -1
- package/dist/test-utils/browser-mocks.js +6 -0
- package/dist/test-utils/zip-round-trip-helpers.d.ts +9 -1
- package/dist/test-utils/zip-round-trip-helpers.js +5 -1
- package/dist/{tracking-slice-CdK8c-8P.d.ts → tracking-slice-BuAZxXZ5.d.ts} +1 -1
- package/dist/types/ar-types.d.ts +1 -1
- package/dist/types/geo-types.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/concurrency.d.ts +2 -2
- package/dist/utils/concurrency.js +66 -2
- package/dist/utils/failure-tracker.d.ts +1 -1
- package/dist/utils/format-file-size.d.ts +1 -1
- package/dist/utils/fused-path.d.ts +1 -1
- package/dist/utils/index.d.ts +8 -7
- package/dist/utils/index.js +2 -1
- package/dist/utils/list-formatter.d.ts +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/visualization/accuracy-circles.d.ts +1 -1
- package/dist/visualization/alignment-lerper.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.d.ts +1 -1
- package/dist/visualization/ar-world-group-alignment.js +1 -1
- package/dist/visualization/camera-follower.d.ts +1 -1
- package/dist/visualization/css3d-renderer-manager.d.ts +1 -1
- package/dist/visualization/frame-conversions.d.ts +1 -1
- package/dist/visualization/frustum-visibility.d.ts +1 -1
- package/dist/visualization/gps-anchor.d.ts +1 -1
- package/dist/visualization/gps-compass-cubes.d.ts +1 -1
- package/dist/visualization/gps-event-markers.d.ts +1 -1
- package/dist/visualization/gps-event-markers.js +40 -5
- package/dist/visualization/hit-test-reticle.d.ts +1 -1
- package/dist/visualization/hit-test-reticle.js +1 -0
- package/dist/visualization/index.d.ts +18 -18
- package/dist/visualization/leaflet-map-overlay.d.ts +1 -1
- package/dist/visualization/leaflet-map-overlay.js +15 -3
- package/dist/visualization/lerp-utils.d.ts +1 -1
- package/dist/visualization/map-data.d.ts +1 -1
- package/dist/visualization/map-overlay-draw.d.ts +1 -1
- package/dist/visualization/map-overlay.d.ts +1 -1
- package/dist/visualization/three-dispose.d.ts +1 -1
- package/dist/visualization/vis-colors.d.ts +1 -1
- package/dist/{webxr-session-TIaD883f.js → webxr-session-DG-eNfny.js} +244 -11
- package/dist/{webxr-session-GjZ2BIpS.d.ts → webxr-session-Do5L6G3B.d.ts} +86 -8
- package/dist/zip-coverage-embed-DCAREVmw.d.ts +41 -0
- package/dist/{zip-export-G14hkSPG.d.ts → zip-export-ol0S_Ofd.d.ts} +27 -23
- package/package.json +2 -2
- package/dist/concurrency-CO1FQvs3.d.ts +0 -24
- package/dist/file-system-CwXTNHmB.d.ts +0 -114
- package/dist/index-CF05QV2m.d.ts +0 -2
- package/dist/recording-options-DJBXuMxV.d.ts +0 -182
- package/dist/recording-slice-89O07ebs.d.ts +0 -36
- package/dist/storage/file-system.d.ts +0 -2
- package/dist/storage/file-system.js +0 -289
- /package/dist/{accuracy-circles-BpwWAs8G.d.ts → accuracy-circles-BtqOP_v1.d.ts} +0 -0
- /package/dist/{alignment-lerper-Dm9G7JJO.d.ts → alignment-lerper-Bil6vbVw.d.ts} +0 -0
- /package/dist/{ar-types-Dan1MVTi.d.ts → ar-types-BFX2r1wk.d.ts} +0 -0
- /package/dist/{bresenham3d-CFfd7n6B.d.ts → bresenham3d-CU1Gv2W4.d.ts} +0 -0
- /package/dist/{camera-follower-C2mwEn7c.d.ts → camera-follower-CqJJ7zRj.d.ts} +0 -0
- /package/dist/{capability-checker-CQXJHxXg.d.ts → capability-checker-DUQLg6gq.d.ts} +0 -0
- /package/dist/{capture-failure-tracker-CUeU3L_M.d.ts → capture-failure-tracker-gsm8q7yi.d.ts} +0 -0
- /package/dist/{chromium-camera-access-workaround-Jqbn0apH.d.ts → chromium-camera-access-workaround-D4K2uwwQ.d.ts} +0 -0
- /package/dist/{css3d-renderer-manager-ptu_t3B3.d.ts → css3d-renderer-manager-B3kGNtcn.d.ts} +0 -0
- /package/dist/{failure-tracker-Cn8Fo9D4.d.ts → failure-tracker-BO7M7jF_.d.ts} +0 -0
- /package/dist/{file-system-utils-CXIoL-DM.d.ts → file-system-utils-DOajnMm-.d.ts} +0 -0
- /package/dist/{format-file-size-CLCZz3lC.d.ts → format-file-size-Dc98ulzB.d.ts} +0 -0
- /package/dist/{frame-loop-CMewS8cY.d.ts → frame-loop-CpVjQ1dH.d.ts} +0 -0
- /package/dist/{frustum-visibility-CmI96ESS.d.ts → frustum-visibility-CeoCZchO.d.ts} +0 -0
- /package/dist/{geo-types-SaPnIEUM.d.ts → geo-types-3Ip1NzVR.d.ts} +0 -0
- /package/dist/{gps-VuezJWK3.d.ts → gps-BBO3hKfT.d.ts} +0 -0
- /package/dist/{gps-compass-cubes-BM1uGiYV.d.ts → gps-compass-cubes-DSN87L8p.d.ts} +0 -0
- /package/dist/{gps-error-handler-CJl_2MVm.d.ts → gps-error-handler-BbPsfw-_.d.ts} +0 -0
- /package/dist/{hit-test-reticle-BEzEybxZ.d.ts → hit-test-reticle-DV-2TpB9.d.ts} +0 -0
- /package/dist/{lerp-utils-BpV-0Dtk.d.ts → lerp-utils-BLmHhkVe.d.ts} +0 -0
- /package/dist/{list-formatter-RRcBe8Vg.d.ts → list-formatter-KNMS0ay2.d.ts} +0 -0
- /package/dist/{logger-BEHXsnnq.d.ts → logger-CLnPrHiD.d.ts} +0 -0
- /package/dist/{map-overlay-bcogY5fK.d.ts → map-overlay-Dh2B97MP.d.ts} +0 -0
- /package/dist/{permission-checker-GpAiSCwd.d.ts → permission-checker-CDuI8BwC.d.ts} +0 -0
- /package/dist/{scene-node-names-Bu0S66H1.d.ts → scene-node-names-BiShOQDB.d.ts} +0 -0
- /package/dist/{session-disposers-M-oashRH.js → session-disposers-zv9OGXPB.js} +0 -0
- /package/dist/{three-dispose-D950t71B.d.ts → three-dispose-Bt1k7WKh.d.ts} +0 -0
- /package/dist/{vis-colors-D6omC64K.d.ts → vis-colors-Obw2pLqT.d.ts} +0 -0
- /package/dist/{webxr-nue-basis-CqKQmZzT.d.ts → webxr-nue-basis-3nfjDJJp.d.ts} +0 -0
- /package/dist/{xr-camera-texture-DBwCaKTd.d.ts → xr-camera-texture-Cva1q3Qj.d.ts} +0 -0
- /package/dist/{xr-error-handler-C0AQRmxs.d.ts → xr-error-handler-CtvHpjfm.d.ts} +0 -0
- /package/dist/{xr-frame-loop-BtYytZJg.d.ts → xr-frame-loop-DIYtRcUa.d.ts} +0 -0
- /package/dist/{zip-reader-DSEXt7K6.d.ts → zip-reader-D9iPQwdQ.d.ts} +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { vec3 } from "gl-matrix";
|
|
2
|
+
//#region ../src/ar/qr-size-from-depth.ts
|
|
3
|
+
/**
|
|
4
|
+
* QR size from depth — Note 4 of the QR-tracking follow-up plan
|
|
5
|
+
* (docs `2026-06-15-followup-qr-tracking-generalization-overlay-and-north.md`).
|
|
6
|
+
*
|
|
7
|
+
* MEASURE a QR's printed physical size directly from the depth map, so the
|
|
8
|
+
* QR content + printed size are irrelevant and `qr.physicalSizeM` need not be
|
|
9
|
+
* hand-authored. The 4 corner pixels (and a few interior samples to catch edge
|
|
10
|
+
* depth-bleed) are unprojected to 3D via the existing `createDepthUnprojector`
|
|
11
|
+
* (depth + projection + camera pose → raw-WebXR points); the pairwise side
|
|
12
|
+
* lengths of the unprojected square give a per-observation size estimate WITH
|
|
13
|
+
* metric scale from depth — no `solvePnP` scale assumption needed.
|
|
14
|
+
*
|
|
15
|
+
* Two pieces:
|
|
16
|
+
* - {@link estimateQrSizeFromDepth} — one observation → `{ sizeM, quality }`.
|
|
17
|
+
* `quality ∈ [0,1]` falls as the 4 sides disagree, the diagonals deviate from
|
|
18
|
+
* `√2·side`, or the corners/interior points are non-planar (depth noise / not
|
|
19
|
+
* a planar square facing us). A caller rejects low-quality reads.
|
|
20
|
+
* - {@link createQrSizeAccumulator} — a robust running MEDIAN over accepted
|
|
21
|
+
* observations, reporting the Note 3 size lifecycle (`unknown → measuring →
|
|
22
|
+
* estimated`). The median is robust to depth noise; the lifecycle gate
|
|
23
|
+
* (min sample count + low spread) is what later promotes a measured size to
|
|
24
|
+
* drive size-dependent features.
|
|
25
|
+
*
|
|
26
|
+
* The size-estimate VALUE types ({@link QrSizeStatus}, {@link QrSizeEstimate})
|
|
27
|
+
* live here (not in the `qrDetected` state slice) so the slice can import them
|
|
28
|
+
* WITHOUT the `ar` layer ever importing `state` — that would close a cycle.
|
|
29
|
+
*
|
|
30
|
+
* @see depth-unprojection.ts — `createDepthUnprojector` (the unprojection it composes).
|
|
31
|
+
* @see ../state/qr-detected-slice.ts — consumes `QrSizeEstimate` (the size lifecycle).
|
|
32
|
+
*/
|
|
33
|
+
const EPS$1 = 1e-9;
|
|
34
|
+
/**
|
|
35
|
+
* Absolute floor (meters) for the robust plane-fit inlier band, used when the
|
|
36
|
+
* MAD-derived σ is ~0 (a near-perfect plane). Comfortably above float round-
|
|
37
|
+
* trip noise yet far below a real depth outlier at QR scale. See WS-A.
|
|
38
|
+
*/
|
|
39
|
+
const PLANE_INLIER_FLOOR_M = .005;
|
|
40
|
+
function dist(a, b) {
|
|
41
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
42
|
+
}
|
|
43
|
+
/** Median of a numeric list (mean of the middle two for even n). */
|
|
44
|
+
function median(values) {
|
|
45
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
46
|
+
const n = sorted.length;
|
|
47
|
+
const mid = n >> 1;
|
|
48
|
+
return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Estimate a QR's physical side length from one detection's depth samples.
|
|
52
|
+
*
|
|
53
|
+
* @param corners - the 4 corner depth samples, ordered TL, TR, BR, BL (matching
|
|
54
|
+
* `buildObjectPoints` / the detector corner-order normalization).
|
|
55
|
+
* @param interiorSamples - a few interior depth samples (may be empty); used
|
|
56
|
+
* only to strengthen the planarity check against corner edge depth-bleed.
|
|
57
|
+
* @param unprojector - built once per depth sample via `createDepthUnprojector`.
|
|
58
|
+
* @returns `{ sizeM, quality }`, or `null` when a corner cannot be unprojected
|
|
59
|
+
* or the quad is degenerate (collinear / zero-area).
|
|
60
|
+
*/
|
|
61
|
+
function estimateQrSizeFromDepth(corners, interiorSamples, unprojector) {
|
|
62
|
+
const world = [];
|
|
63
|
+
for (const c of corners) {
|
|
64
|
+
const p = unprojector.unproject(c);
|
|
65
|
+
if (!p) return null;
|
|
66
|
+
world.push(p);
|
|
67
|
+
}
|
|
68
|
+
const [c0, c1, c2, c3] = world;
|
|
69
|
+
const edges = [
|
|
70
|
+
dist(c0, c1),
|
|
71
|
+
dist(c1, c2),
|
|
72
|
+
dist(c2, c3),
|
|
73
|
+
dist(c3, c0)
|
|
74
|
+
];
|
|
75
|
+
const meanEdge = (edges[0] + edges[1] + edges[2] + edges[3]) / 4;
|
|
76
|
+
if (!(meanEdge > EPS$1)) return null;
|
|
77
|
+
const u = vec3.subtract(vec3.create(), [
|
|
78
|
+
c1[0],
|
|
79
|
+
c1[1],
|
|
80
|
+
c1[2]
|
|
81
|
+
], [
|
|
82
|
+
c0[0],
|
|
83
|
+
c0[1],
|
|
84
|
+
c0[2]
|
|
85
|
+
]);
|
|
86
|
+
const v = vec3.subtract(vec3.create(), [
|
|
87
|
+
c3[0],
|
|
88
|
+
c3[1],
|
|
89
|
+
c3[2]
|
|
90
|
+
], [
|
|
91
|
+
c0[0],
|
|
92
|
+
c0[1],
|
|
93
|
+
c0[2]
|
|
94
|
+
]);
|
|
95
|
+
const normal = vec3.cross(vec3.create(), u, v);
|
|
96
|
+
const normalLen = vec3.length(normal);
|
|
97
|
+
if (!(normalLen > EPS$1)) return null;
|
|
98
|
+
vec3.scale(normal, normal, 1 / normalLen);
|
|
99
|
+
const planeOffset = (p) => Math.abs(normal[0] * (p[0] - c0[0]) + normal[1] * (p[1] - c0[1]) + normal[2] * (p[2] - c0[2]));
|
|
100
|
+
let maxPlaneOffset = planeOffset(c2);
|
|
101
|
+
for (const s of interiorSamples) {
|
|
102
|
+
const p = unprojector.unproject(s);
|
|
103
|
+
if (!p) continue;
|
|
104
|
+
maxPlaneOffset = Math.max(maxPlaneOffset, planeOffset(p));
|
|
105
|
+
}
|
|
106
|
+
const edgeErr = Math.max(...edges.map((e) => Math.abs(e - meanEdge))) / meanEdge;
|
|
107
|
+
const expectedDiag = meanEdge * Math.SQRT2;
|
|
108
|
+
const diagErr = Math.max(Math.abs(dist(c0, c2) - expectedDiag), Math.abs(dist(c1, c3) - expectedDiag)) / expectedDiag;
|
|
109
|
+
const planeErr = maxPlaneOffset / meanEdge;
|
|
110
|
+
const quality = Math.max(0, Math.min(1, 1 - Math.max(edgeErr, diagErr, planeErr)));
|
|
111
|
+
return {
|
|
112
|
+
sizeM: median(edges),
|
|
113
|
+
quality
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Least-squares plane through a point set, refined from a seed normal. Avoids a
|
|
118
|
+
* full eigensolver: the seed (from LMS) picks which axis is most aligned with
|
|
119
|
+
* the normal, that axis becomes the dependent variable of a 2-parameter linear
|
|
120
|
+
* fit through the centroid (well-conditioned precisely because the plane is not
|
|
121
|
+
* near-vertical in that axis), and the 2×2 normal equations are solved in closed
|
|
122
|
+
* form. Returns `null` for fewer than 3 points or a singular (collinear) fit.
|
|
123
|
+
*/
|
|
124
|
+
function fitPlaneLeastSquares(points, seedNormal) {
|
|
125
|
+
const n = points.length;
|
|
126
|
+
if (n < 3) return null;
|
|
127
|
+
let cx = 0;
|
|
128
|
+
let cy = 0;
|
|
129
|
+
let cz = 0;
|
|
130
|
+
for (const p of points) {
|
|
131
|
+
cx += p[0];
|
|
132
|
+
cy += p[1];
|
|
133
|
+
cz += p[2];
|
|
134
|
+
}
|
|
135
|
+
cx /= n;
|
|
136
|
+
cy /= n;
|
|
137
|
+
cz /= n;
|
|
138
|
+
const ax = Math.abs(seedNormal[0]);
|
|
139
|
+
const ay = Math.abs(seedNormal[1]);
|
|
140
|
+
const az = Math.abs(seedNormal[2]);
|
|
141
|
+
const dep = ax >= ay && ax >= az ? 0 : ay >= az ? 1 : 2;
|
|
142
|
+
const split = (p) => dep === 0 ? [
|
|
143
|
+
p[1] - cy,
|
|
144
|
+
p[2] - cz,
|
|
145
|
+
p[0] - cx
|
|
146
|
+
] : dep === 1 ? [
|
|
147
|
+
p[0] - cx,
|
|
148
|
+
p[2] - cz,
|
|
149
|
+
p[1] - cy
|
|
150
|
+
] : [
|
|
151
|
+
p[0] - cx,
|
|
152
|
+
p[1] - cy,
|
|
153
|
+
p[2] - cz
|
|
154
|
+
];
|
|
155
|
+
let Suu = 0;
|
|
156
|
+
let Suw = 0;
|
|
157
|
+
let Sww = 0;
|
|
158
|
+
let Sud = 0;
|
|
159
|
+
let Swd = 0;
|
|
160
|
+
for (const p of points) {
|
|
161
|
+
const [u, w, d] = split(p);
|
|
162
|
+
Suu += u * u;
|
|
163
|
+
Suw += u * w;
|
|
164
|
+
Sww += w * w;
|
|
165
|
+
Sud += u * d;
|
|
166
|
+
Swd += w * d;
|
|
167
|
+
}
|
|
168
|
+
const det = Suu * Sww - Suw * Suw;
|
|
169
|
+
if (Math.abs(det) < EPS$1) return null;
|
|
170
|
+
const a = (Sud * Sww - Swd * Suw) / det;
|
|
171
|
+
const b = (Swd * Suu - Sud * Suw) / det;
|
|
172
|
+
const nUWD = [
|
|
173
|
+
-a,
|
|
174
|
+
-b,
|
|
175
|
+
1
|
|
176
|
+
];
|
|
177
|
+
const nxyz = dep === 0 ? [
|
|
178
|
+
nUWD[2],
|
|
179
|
+
nUWD[0],
|
|
180
|
+
nUWD[1]
|
|
181
|
+
] : dep === 1 ? [
|
|
182
|
+
nUWD[0],
|
|
183
|
+
nUWD[2],
|
|
184
|
+
nUWD[1]
|
|
185
|
+
] : [
|
|
186
|
+
nUWD[0],
|
|
187
|
+
nUWD[1],
|
|
188
|
+
nUWD[2]
|
|
189
|
+
];
|
|
190
|
+
const len = Math.hypot(nxyz[0], nxyz[1], nxyz[2]);
|
|
191
|
+
if (!(len > EPS$1)) return null;
|
|
192
|
+
const sign = nxyz[0] * seedNormal[0] + nxyz[1] * seedNormal[1] + nxyz[2] * seedNormal[2] < 0 ? -1 : 1;
|
|
193
|
+
const normal = [
|
|
194
|
+
sign * nxyz[0] / len,
|
|
195
|
+
sign * nxyz[1] / len,
|
|
196
|
+
sign * nxyz[2] / len
|
|
197
|
+
];
|
|
198
|
+
return {
|
|
199
|
+
centroid: [
|
|
200
|
+
cx,
|
|
201
|
+
cy,
|
|
202
|
+
cz
|
|
203
|
+
],
|
|
204
|
+
normal
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Robustly fit a plane to 3D points. A plain least-squares (PCA) plane is
|
|
209
|
+
* fragile here: one gross depth outlier (edge depth-bleed, a stray background
|
|
210
|
+
* read) can dominate the off-plane variance and flip which axis PCA calls the
|
|
211
|
+
* normal, so MAD rejection then runs against a wrong normal. Instead we use
|
|
212
|
+
* Least-Median-of-Squares — pick the candidate plane (from a point triple) that
|
|
213
|
+
* minimizes the MEDIAN squared orthogonal residual (≈50% breakdown, no
|
|
214
|
+
* threshold) — derive a robust scale from that median, select inliers, then
|
|
215
|
+
* least-squares refit on the inliers for an accurate normal/centroid.
|
|
216
|
+
*
|
|
217
|
+
* Deterministic (fixed-seed triple sampling) so property tests are
|
|
218
|
+
* reproducible. Returns `null` for fewer than 3 points or a degenerate
|
|
219
|
+
* (collinear / coincident) set where no non-degenerate triple exists.
|
|
220
|
+
*/
|
|
221
|
+
function fitPlaneRobust(points) {
|
|
222
|
+
const n = points.length;
|
|
223
|
+
if (n < 3) return null;
|
|
224
|
+
let best = null;
|
|
225
|
+
let rng = 625341585;
|
|
226
|
+
const nextIdx = () => {
|
|
227
|
+
rng = rng * 1103515245 + 12345 & 2147483647;
|
|
228
|
+
return rng % n;
|
|
229
|
+
};
|
|
230
|
+
const TRIALS = 200;
|
|
231
|
+
for (let t = 0; t < TRIALS; t++) {
|
|
232
|
+
const i = nextIdx();
|
|
233
|
+
const j = nextIdx();
|
|
234
|
+
const k = nextIdx();
|
|
235
|
+
if (i === j || j === k || i === k) continue;
|
|
236
|
+
const a = points[i];
|
|
237
|
+
const b = points[j];
|
|
238
|
+
const c = points[k];
|
|
239
|
+
const ux = b[0] - a[0];
|
|
240
|
+
const uy = b[1] - a[1];
|
|
241
|
+
const uz = b[2] - a[2];
|
|
242
|
+
const vx = c[0] - a[0];
|
|
243
|
+
const vy = c[1] - a[1];
|
|
244
|
+
const vz = c[2] - a[2];
|
|
245
|
+
let nx = uy * vz - uz * vy;
|
|
246
|
+
let ny = uz * vx - ux * vz;
|
|
247
|
+
let nz = ux * vy - uy * vx;
|
|
248
|
+
const len = Math.hypot(nx, ny, nz);
|
|
249
|
+
if (!(len > EPS$1)) continue;
|
|
250
|
+
nx /= len;
|
|
251
|
+
ny /= len;
|
|
252
|
+
nz /= len;
|
|
253
|
+
const medSq = median(points.map((p) => {
|
|
254
|
+
const d = nx * (p[0] - a[0]) + ny * (p[1] - a[1]) + nz * (p[2] - a[2]);
|
|
255
|
+
return d * d;
|
|
256
|
+
}));
|
|
257
|
+
if (!best || medSq < best.medSq) best = {
|
|
258
|
+
normal: [
|
|
259
|
+
nx,
|
|
260
|
+
ny,
|
|
261
|
+
nz
|
|
262
|
+
],
|
|
263
|
+
p0: a,
|
|
264
|
+
medSq
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (!best) return null;
|
|
268
|
+
const sigma = 1.4826 * (1 + 5 / Math.max(1, n - 3)) * Math.sqrt(best.medSq);
|
|
269
|
+
const band = Math.max(2.5 * sigma, PLANE_INLIER_FLOOR_M);
|
|
270
|
+
const { normal: bn, p0 } = best;
|
|
271
|
+
let inliers = points.filter((p) => Math.abs(bn[0] * (p[0] - p0[0]) + bn[1] * (p[1] - p0[1]) + bn[2] * (p[2] - p0[2])) <= band);
|
|
272
|
+
if (inliers.length < 3) inliers = [...points];
|
|
273
|
+
const refit = fitPlaneLeastSquares(inliers, bn);
|
|
274
|
+
if (!refit) return null;
|
|
275
|
+
const refitDist = (p) => refit.normal[0] * (p[0] - refit.centroid[0]) + refit.normal[1] * (p[1] - refit.centroid[1]) + refit.normal[2] * (p[2] - refit.centroid[2]);
|
|
276
|
+
const rms = Math.sqrt(inliers.reduce((acc, p) => acc + refitDist(p) ** 2, 0) / inliers.length);
|
|
277
|
+
return {
|
|
278
|
+
point: refit.centroid,
|
|
279
|
+
normal: refit.normal,
|
|
280
|
+
inlierCount: inliers.length,
|
|
281
|
+
rms
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Intersect the camera ray through a normalized screen point with a plane.
|
|
286
|
+
* The ray is recovered by unprojecting the screen point at two distinct depths
|
|
287
|
+
* (both lie on the corner's pixel ray); no explicit camera center is needed.
|
|
288
|
+
* Returns the metric 3D intersection, or `null` for a parallel/degenerate ray.
|
|
289
|
+
*/
|
|
290
|
+
function intersectScreenRayWithPlane(screen, plane, unprojector) {
|
|
291
|
+
const a = unprojector.unproject({
|
|
292
|
+
...screen,
|
|
293
|
+
depthM: 1
|
|
294
|
+
});
|
|
295
|
+
const b = unprojector.unproject({
|
|
296
|
+
...screen,
|
|
297
|
+
depthM: 2
|
|
298
|
+
});
|
|
299
|
+
if (!a || !b) return null;
|
|
300
|
+
const dir = [
|
|
301
|
+
b[0] - a[0],
|
|
302
|
+
b[1] - a[1],
|
|
303
|
+
b[2] - a[2]
|
|
304
|
+
];
|
|
305
|
+
const { normal: n, point: pp } = plane;
|
|
306
|
+
const denom = n[0] * dir[0] + n[1] * dir[1] + n[2] * dir[2];
|
|
307
|
+
if (Math.abs(denom) < EPS$1) return null;
|
|
308
|
+
const t = (n[0] * (pp[0] - a[0]) + n[1] * (pp[1] - a[1]) + n[2] * (pp[2] - a[2])) / denom;
|
|
309
|
+
const hit = [
|
|
310
|
+
a[0] + t * dir[0],
|
|
311
|
+
a[1] + t * dir[1],
|
|
312
|
+
a[2] + t * dir[2]
|
|
313
|
+
];
|
|
314
|
+
return hit.every((v) => Number.isFinite(v)) ? hit : null;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Estimate a QR's physical side length by fitting the QR plane to many interior
|
|
318
|
+
* depth reads and intersecting the 4 corner pixel rays with that plane.
|
|
319
|
+
*
|
|
320
|
+
* @param cornerScreens - the 4 corner screen positions, ordered TL, TR, BR, BL.
|
|
321
|
+
* @param samples - interior (and optionally corner) depth reads across the QR
|
|
322
|
+
* face used for the robust plane fit. Need ≥3 non-collinear usable reads.
|
|
323
|
+
* @param unprojector - built once per depth sample via `createDepthUnprojector`.
|
|
324
|
+
* @returns `{ sizeM, quality }`, or `null` when the plane is under-determined
|
|
325
|
+
* (too few/collinear reads) or a corner ray cannot meet the plane.
|
|
326
|
+
*/
|
|
327
|
+
function estimateQrSizeFromDepthDense(cornerScreens, samples, unprojector) {
|
|
328
|
+
const points = [];
|
|
329
|
+
for (const s of samples) {
|
|
330
|
+
const p = unprojector.unproject(s);
|
|
331
|
+
if (p) points.push(p);
|
|
332
|
+
}
|
|
333
|
+
const plane = fitPlaneRobust(points);
|
|
334
|
+
if (!plane) return null;
|
|
335
|
+
const corners = [];
|
|
336
|
+
for (const screen of cornerScreens) {
|
|
337
|
+
const hit = intersectScreenRayWithPlane(screen, plane, unprojector);
|
|
338
|
+
if (!hit) return null;
|
|
339
|
+
corners.push(hit);
|
|
340
|
+
}
|
|
341
|
+
const [c0, c1, c2, c3] = corners;
|
|
342
|
+
const edges = [
|
|
343
|
+
dist(c0, c1),
|
|
344
|
+
dist(c1, c2),
|
|
345
|
+
dist(c2, c3),
|
|
346
|
+
dist(c3, c0)
|
|
347
|
+
];
|
|
348
|
+
const meanEdge = (edges[0] + edges[1] + edges[2] + edges[3]) / 4;
|
|
349
|
+
if (!(meanEdge > EPS$1)) return null;
|
|
350
|
+
const edgeErr = Math.max(...edges.map((e) => Math.abs(e - meanEdge))) / meanEdge;
|
|
351
|
+
const expectedDiag = meanEdge * Math.SQRT2;
|
|
352
|
+
const diagErr = Math.max(Math.abs(dist(c0, c2) - expectedDiag), Math.abs(dist(c1, c3) - expectedDiag)) / expectedDiag;
|
|
353
|
+
const planeErr = plane.rms / meanEdge;
|
|
354
|
+
const quality = Math.max(0, Math.min(1, 1 - Math.max(edgeErr, diagErr, planeErr)));
|
|
355
|
+
return {
|
|
356
|
+
sizeM: median(edges),
|
|
357
|
+
quality
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const UNKNOWN = {
|
|
361
|
+
status: "unknown",
|
|
362
|
+
estimateM: null,
|
|
363
|
+
sampleCount: 0,
|
|
364
|
+
spreadM: 0
|
|
365
|
+
};
|
|
366
|
+
function createQrSizeAccumulator(options = {}) {
|
|
367
|
+
const { qualityThreshold = .8, minSamples = 8, maxSpreadM = .01, maxSamples = Infinity } = options;
|
|
368
|
+
let sizes = [];
|
|
369
|
+
let everConverged = false;
|
|
370
|
+
function estimate() {
|
|
371
|
+
if (sizes.length === 0) return { ...UNKNOWN };
|
|
372
|
+
const med = median(sizes);
|
|
373
|
+
const spreadM = 1.4826 * median(sizes.map((s) => Math.abs(s - med))) / Math.sqrt(sizes.length);
|
|
374
|
+
if (sizes.length >= minSamples && spreadM <= maxSpreadM) everConverged = true;
|
|
375
|
+
return {
|
|
376
|
+
status: everConverged ? "estimated" : "measuring",
|
|
377
|
+
estimateM: med,
|
|
378
|
+
sampleCount: sizes.length,
|
|
379
|
+
spreadM
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
add(observation) {
|
|
384
|
+
if (observation && Number.isFinite(observation.sizeM) && observation.sizeM > 0 && observation.quality >= qualityThreshold) {
|
|
385
|
+
sizes.push(observation.sizeM);
|
|
386
|
+
if (sizes.length > maxSamples) sizes = sizes.slice(-maxSamples);
|
|
387
|
+
}
|
|
388
|
+
return estimate();
|
|
389
|
+
},
|
|
390
|
+
current: estimate,
|
|
391
|
+
reset() {
|
|
392
|
+
sizes = [];
|
|
393
|
+
everConverged = false;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region ../src/ar/qr-size-measurer.ts
|
|
399
|
+
const EPS = 1e-9;
|
|
400
|
+
/** Pixel corner → normalized screen point for the given frame. */
|
|
401
|
+
function toScreen(corner, image) {
|
|
402
|
+
return {
|
|
403
|
+
x: corner.x / image.width,
|
|
404
|
+
y: corner.y / image.height
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Depth at a corner with inset fallback: try the corner pixel first, then points
|
|
409
|
+
* progressively inset toward the centroid. The returned {@link DepthPoint} always
|
|
410
|
+
* carries the TRUE corner's screen position — only the depth VALUE may be borrowed
|
|
411
|
+
* from an inset sample (valid because the marker face is locally planar over the
|
|
412
|
+
* few-percent inset) — so the unprojected square keeps its real size. `null` when
|
|
413
|
+
* no inset offers a depth read either.
|
|
414
|
+
*/
|
|
415
|
+
function sampleCornerDepth(corner, centroid, image, depthAt, insetFractions) {
|
|
416
|
+
const s = toScreen(corner, image);
|
|
417
|
+
const exact = depthAt(s.x, s.y);
|
|
418
|
+
if (exact !== null) return {
|
|
419
|
+
screenX: s.x,
|
|
420
|
+
screenY: s.y,
|
|
421
|
+
depthM: exact
|
|
422
|
+
};
|
|
423
|
+
for (const f of insetFractions) {
|
|
424
|
+
const si = toScreen({
|
|
425
|
+
x: corner.x + f * (centroid.x - corner.x),
|
|
426
|
+
y: corner.y + f * (centroid.y - corner.y)
|
|
427
|
+
}, image);
|
|
428
|
+
const depthM = depthAt(si.x, si.y);
|
|
429
|
+
if (depthM !== null) return {
|
|
430
|
+
screenX: s.x,
|
|
431
|
+
screenY: s.y,
|
|
432
|
+
depthM
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Estimate a missing corner's depth by fitting a plane `depth = f(screenX,
|
|
439
|
+
* screenY)` through three known corner samples and evaluating it at the missing
|
|
440
|
+
* corner's screen position. `null` when the three are collinear in screen space
|
|
441
|
+
* (degenerate plane).
|
|
442
|
+
*/
|
|
443
|
+
function reconstructDepth(known, at) {
|
|
444
|
+
if (known.length < 3) return null;
|
|
445
|
+
const [a, b, c] = known;
|
|
446
|
+
const abx = b.screenX - a.screenX;
|
|
447
|
+
const aby = b.screenY - a.screenY;
|
|
448
|
+
const abz = b.depthM - a.depthM;
|
|
449
|
+
const acx = c.screenX - a.screenX;
|
|
450
|
+
const acy = c.screenY - a.screenY;
|
|
451
|
+
const acz = c.depthM - a.depthM;
|
|
452
|
+
const nx = aby * acz - abz * acy;
|
|
453
|
+
const ny = abz * acx - abx * acz;
|
|
454
|
+
const nz = abx * acy - aby * acx;
|
|
455
|
+
if (Math.abs(nz) < EPS) return null;
|
|
456
|
+
const depthM = a.depthM - (nx * (at.x - a.screenX) + ny * (at.y - a.screenY)) / nz;
|
|
457
|
+
return Number.isFinite(depthM) ? depthM : null;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Build the 4 corner depth samples with inset fallback and at-most-`maxRecon`
|
|
461
|
+
* planar reconstruction. `null` when more corners than allowed lack a depth read.
|
|
462
|
+
*/
|
|
463
|
+
function cornerDepthPoints(corners, image, depthAt, insetFractions, maxRecon) {
|
|
464
|
+
if (corners.length !== 4) return null;
|
|
465
|
+
const centroid = {
|
|
466
|
+
x: corners.reduce((s, c) => s + c.x, 0) / corners.length,
|
|
467
|
+
y: corners.reduce((s, c) => s + c.y, 0) / corners.length
|
|
468
|
+
};
|
|
469
|
+
const sampled = corners.map((corner) => sampleCornerDepth(corner, centroid, image, depthAt, insetFractions));
|
|
470
|
+
const missing = sampled.filter((p) => p === null).length;
|
|
471
|
+
if (missing === 0) return sampled;
|
|
472
|
+
if (missing > maxRecon) return null;
|
|
473
|
+
const known = sampled.filter((p) => p !== null);
|
|
474
|
+
const out = [];
|
|
475
|
+
for (let i = 0; i < 4; i++) {
|
|
476
|
+
const s = sampled[i];
|
|
477
|
+
if (s) {
|
|
478
|
+
out.push(s);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const at = toScreen(corners[i], image);
|
|
482
|
+
const depthM = reconstructDepth(known, at);
|
|
483
|
+
if (depthM === null) return null;
|
|
484
|
+
out.push({
|
|
485
|
+
screenX: at.x,
|
|
486
|
+
screenY: at.y,
|
|
487
|
+
depthM
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Sample an `n×n` interior lattice across the detected quad (bilinear in pixel
|
|
494
|
+
* space, strictly inside via `(i+0.5)/n` fractions so the high-contrast print
|
|
495
|
+
* boundary is avoided) and return the reads that have depth. These feed the
|
|
496
|
+
* dense plane fit; reads with no depth are simply skipped.
|
|
497
|
+
*/
|
|
498
|
+
function latticeDepthPoints(corners, image, depthAt, n) {
|
|
499
|
+
if (corners.length !== 4 || n < 1) return [];
|
|
500
|
+
const [tl, tr, br, bl] = corners;
|
|
501
|
+
const out = [];
|
|
502
|
+
for (let i = 0; i < n; i++) {
|
|
503
|
+
const u = (i + .5) / n;
|
|
504
|
+
const topX = tl.x + (tr.x - tl.x) * u;
|
|
505
|
+
const topY = tl.y + (tr.y - tl.y) * u;
|
|
506
|
+
const botX = bl.x + (br.x - bl.x) * u;
|
|
507
|
+
const botY = bl.y + (br.y - bl.y) * u;
|
|
508
|
+
for (let j = 0; j < n; j++) {
|
|
509
|
+
const v = (j + .5) / n;
|
|
510
|
+
const s = toScreen({
|
|
511
|
+
x: topX + (botX - topX) * v,
|
|
512
|
+
y: topY + (botY - topY) * v
|
|
513
|
+
}, image);
|
|
514
|
+
const depthM = depthAt(s.x, s.y);
|
|
515
|
+
if (depthM !== null) out.push({
|
|
516
|
+
screenX: s.x,
|
|
517
|
+
screenY: s.y,
|
|
518
|
+
depthM
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
/** The 4 corner screen positions (detector order) for the dense ray-plane fit. */
|
|
525
|
+
function cornerScreens(corners, image) {
|
|
526
|
+
if (corners.length !== 4) return null;
|
|
527
|
+
return corners.map((c) => {
|
|
528
|
+
const s = toScreen(c, image);
|
|
529
|
+
return {
|
|
530
|
+
screenX: s.x,
|
|
531
|
+
screenY: s.y
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/** The QR centroid as a single interior depth sample (may be empty). */
|
|
536
|
+
function interiorDepthPoints(corners, image, depthAt) {
|
|
537
|
+
const s = toScreen({
|
|
538
|
+
x: corners.reduce((s, c) => s + c.x, 0) / corners.length,
|
|
539
|
+
y: corners.reduce((s, c) => s + c.y, 0) / corners.length
|
|
540
|
+
}, image);
|
|
541
|
+
const depthM = depthAt(s.x, s.y);
|
|
542
|
+
return depthM === null ? [] : [{
|
|
543
|
+
screenX: s.x,
|
|
544
|
+
screenY: s.y,
|
|
545
|
+
depthM
|
|
546
|
+
}];
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Create a size measurer. Accumulator `options` (quality threshold, min samples,
|
|
550
|
+
* spread, cap) are forwarded to every per-marker {@link createQrSizeAccumulator};
|
|
551
|
+
* the depth-at-corners knobs ({@link QrSizeMeasurerOptions.cornerInsetFractions},
|
|
552
|
+
* {@link QrSizeMeasurerOptions.maxReconstructedCorners}) tune corner sampling.
|
|
553
|
+
*/
|
|
554
|
+
function createQrSizeMeasurer(options = {}) {
|
|
555
|
+
const insetFractions = options.cornerInsetFractions ?? [.12, .25];
|
|
556
|
+
const maxRecon = options.maxReconstructedCorners ?? 1;
|
|
557
|
+
const latticeSize = options.latticeSize ?? 7;
|
|
558
|
+
const accumulators = /* @__PURE__ */ new Map();
|
|
559
|
+
function accumulatorFor(text) {
|
|
560
|
+
let acc = accumulators.get(text);
|
|
561
|
+
if (!acc) {
|
|
562
|
+
acc = createQrSizeAccumulator(options);
|
|
563
|
+
accumulators.set(text, acc);
|
|
564
|
+
}
|
|
565
|
+
return acc;
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
measure(text, corners, image, ctx) {
|
|
569
|
+
const screens = cornerScreens(corners, image);
|
|
570
|
+
if (!screens) return null;
|
|
571
|
+
const cornerSamples = cornerDepthPoints(corners, image, ctx.depthAt, insetFractions, maxRecon);
|
|
572
|
+
const lattice = latticeDepthPoints(corners, image, ctx.depthAt, latticeSize);
|
|
573
|
+
let observation = estimateQrSizeFromDepthDense(screens, lattice, ctx.unprojector);
|
|
574
|
+
let interiorSamples = lattice;
|
|
575
|
+
if (observation === null) {
|
|
576
|
+
if (!cornerSamples) return null;
|
|
577
|
+
interiorSamples = interiorDepthPoints(corners, image, ctx.depthAt);
|
|
578
|
+
observation = estimateQrSizeFromDepth(cornerSamples, interiorSamples, ctx.unprojector);
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
estimate: accumulatorFor(text).add(observation),
|
|
582
|
+
cornerSamples,
|
|
583
|
+
interiorSamples
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
current(text) {
|
|
587
|
+
return accumulatorFor(text).current();
|
|
588
|
+
},
|
|
589
|
+
reset(text) {
|
|
590
|
+
if (text === void 0) accumulators.clear();
|
|
591
|
+
else accumulators.delete(text);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
export { createQrSizeAccumulator as n, estimateQrSizeFromDepth as r, createQrSizeMeasurer as t };
|