react-native-image-stitcher 0.2.1 → 0.4.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +511 -1
  2. package/README.md +1 -1
  3. package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
  4. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
  5. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
  8. package/cpp/stitcher.cpp +101 -1
  9. package/cpp/stitcher.hpp +8 -0
  10. package/dist/camera/Camera.d.ts +9 -0
  11. package/dist/camera/Camera.js +165 -43
  12. package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
  13. package/dist/camera/CaptureDebugOverlay.js +146 -0
  14. package/dist/camera/CaptureKeyframePill.d.ts +28 -0
  15. package/dist/camera/CaptureKeyframePill.js +60 -0
  16. package/dist/camera/CaptureMemoryPill.d.ts +28 -0
  17. package/dist/camera/CaptureMemoryPill.js +109 -0
  18. package/dist/camera/CaptureOrientationPill.d.ts +22 -0
  19. package/dist/camera/CaptureOrientationPill.js +44 -0
  20. package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
  21. package/dist/camera/CaptureStitchStatsToast.js +133 -0
  22. package/dist/camera/PanoramaSettings.d.ts +478 -0
  23. package/dist/camera/PanoramaSettings.js +120 -0
  24. package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
  25. package/dist/camera/PanoramaSettingsBridge.js +208 -0
  26. package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
  27. package/dist/camera/PanoramaSettingsModal.js +189 -354
  28. package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
  29. package/dist/camera/buildPanoramaInitialSettings.js +97 -0
  30. package/dist/camera/lowMemDevice.d.ts +24 -0
  31. package/dist/camera/lowMemDevice.js +69 -0
  32. package/dist/index.d.ts +16 -2
  33. package/dist/index.js +37 -2
  34. package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
  35. package/dist/sensors/useIMUTranslationGate.js +83 -1
  36. package/dist/stitching/incremental.d.ts +25 -0
  37. package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
  38. package/dist/stitching/useIncrementalStitcher.js +7 -1
  39. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
  40. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
  41. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
  42. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
  43. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
  44. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
  45. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
  46. package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
  47. package/package.json +6 -2
  48. package/src/camera/Camera.tsx +220 -54
  49. package/src/camera/CaptureDebugOverlay.tsx +180 -0
  50. package/src/camera/CaptureKeyframePill.tsx +77 -0
  51. package/src/camera/CaptureMemoryPill.tsx +96 -0
  52. package/src/camera/CaptureOrientationPill.tsx +57 -0
  53. package/src/camera/CaptureStitchStatsToast.tsx +155 -0
  54. package/src/camera/PanoramaSettings.ts +605 -0
  55. package/src/camera/PanoramaSettingsBridge.ts +238 -0
  56. package/src/camera/PanoramaSettingsModal.tsx +296 -988
  57. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
  58. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
  59. package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
  60. package/src/camera/buildPanoramaInitialSettings.ts +139 -0
  61. package/src/camera/lowMemDevice.ts +71 -0
  62. package/src/index.ts +61 -3
  63. package/src/sensors/useIMUTranslationGate.ts +112 -1
  64. package/src/stitching/incremental.ts +25 -0
  65. package/src/stitching/useIncrementalStitcher.ts +18 -0
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * PanoramaSettingsBridge — JS-side adapters that convert the v0.4
5
+ * typed `PanoramaSettings` / `SlitscanSettings` / `HybridSettings`
6
+ * shape into the flat `configOverrides` dictionary the native
7
+ * bridges read.
8
+ *
9
+ * Why this file exists
10
+ * ────────────────────
11
+ *
12
+ * The v0.4 types use hierarchical sub-trees (`stitcher`,
13
+ * `frameSelection.flow`, `painting`, `registration.ncc1d`,
14
+ * `registration.ncc2d.emaSmoothing`, `plane`, …) to give consumers
15
+ * a clean, ergonomic settings surface that mirrors the native
16
+ * engine's domain. But the native bridges (iOS Swift's
17
+ * `applyConfigOverrides`, Android Kotlin's `IncrementalStitcher.start`)
18
+ * read a FLAT dictionary of native-named keys (e.g. `nccSearchRadius1d`,
19
+ * `enable1dNcc`, `ncc2dEmaAlpha`, `flowMaxTranslationCm`).
20
+ *
21
+ * Two semantic gaps to bridge:
22
+ *
23
+ * 1. **Naming.** JS `registration.ncc1d.searchRadius` →
24
+ * native `nccSearchRadius1d`. JS `painting.paintMode` →
25
+ * native `paintMode` (same). Etc.
26
+ *
27
+ * 2. **Presence-as-enable.** The native side reads explicit
28
+ * `enable1dNcc`, `enable2dNcc`, `enableNcc2dEmaSmoothing`,
29
+ * `enableNcc2dPanAxisLock` booleans. JS models these as
30
+ * optional sub-objects (sub-object present ⇒ enabled). This
31
+ * adapter flattens the booleans for the wire.
32
+ *
33
+ * 3. **Skipped engine defaults.** Hybrid engine presets internally
34
+ * clobber most fields (see HybridSettings JSDoc), so we don't
35
+ * send overrides that would be ignored — just the small useful
36
+ * surface.
37
+ *
38
+ * The Camera component calls `panoramaSettingsToNativeConfig` once
39
+ * per capture start to produce the value passed as
40
+ * `incremental.start({ config: … })`. Layer 2 callers building
41
+ * SlitscanSettings or HybridSettings call the matching adapter
42
+ * before reaching `incremental.start()`.
43
+ */
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.panoramaSettingsToNativeConfig = panoramaSettingsToNativeConfig;
46
+ exports.slitscanSettingsToNativeConfig = slitscanSettingsToNativeConfig;
47
+ exports.hybridSettingsToNativeConfig = hybridSettingsToNativeConfig;
48
+ const PanoramaSettings_1 = require("./PanoramaSettings");
49
+ /**
50
+ * Convert a v0.4 PanoramaSettings tree into the flat dict the
51
+ * batch-keyframe native side reads. Maps every consumed field
52
+ * exactly once and skips fields the engine doesn't reach.
53
+ *
54
+ * Verified against:
55
+ * - iOS `IncrementalStitcher.swift:810-960` (batch path)
56
+ * - Android `IncrementalStitcher.kt:280-430` (batch path)
57
+ */
58
+ function panoramaSettingsToNativeConfig(s) {
59
+ const cfg = {
60
+ // ── Cross-cutting ────────────────────────────────────────────
61
+ captureSource: s.captureSource,
62
+ // ── BatchStitcherSettings → cv::Stitcher knobs ───────────────
63
+ stitchMode: s.stitcher.stitchMode,
64
+ warperType: s.stitcher.warperType,
65
+ blenderType: s.stitcher.blenderType,
66
+ seamFinderType: s.stitcher.seamFinderType,
67
+ enableMaxInscribedRectCrop: s.stitcher.enableMaxInscribedRectCrop,
68
+ // ── FrameSelectionSettings → KeyframeGate knobs ──────────────
69
+ frameSelectionMode: s.frameSelection.mode,
70
+ keyframeMaxCount: s.frameSelection.maxKeyframes,
71
+ keyframeOverlapThreshold: s.frameSelection.overlapThreshold,
72
+ };
73
+ // Flow strategy knobs — always serialised, regardless of
74
+ // `frameSelection.mode`. Two reasons:
75
+ //
76
+ // 1. Mode-flip-mid-session: hosts can change `mode` without
77
+ // restarting capture; consistent flow serialisation means
78
+ // `'time-based' → 'flow-based'` mid-session doesn't slip
79
+ // back to stale native-side defaults. Native ignores these
80
+ // keys when the active mode doesn't use them.
81
+ //
82
+ // 2. **Native compiled-in defaults disagree with the JS
83
+ // defaults.** Specifically: native sets `flowMaxTranslationCm
84
+ // = 0` and `flowEvalEveryNFrames = 1` when the keys are
85
+ // missing (iOS `IncrementalStitcher.swift:1003-1029`,
86
+ // Android `IncrementalStitcher.kt:419-445`), whereas the JS
87
+ // `DEFAULT_PANORAMA_SETTINGS.frameSelection.flow` values are
88
+ // `50` and `5`. Hosts who write sparse settings literals
89
+ // (omitted `flow` sub-tree, legal per the optional `?`)
90
+ // would silently get IMU translation gate disabled and
91
+ // ~5× CPU on flow evaluation — a v0.3-style behaviour
92
+ // regression on the wire that the type system can't catch.
93
+ // Filling from `DEFAULT_FLOW_GATE_SETTINGS` here closes the
94
+ // gap; the JS defaults become the canonical defaults across
95
+ // both layers.
96
+ //
97
+ // See the F10 Phase 2 review (B1 + N3 + N6) for the full
98
+ // discussion of why this matters.
99
+ const f = s.frameSelection.flow ?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS;
100
+ cfg.flowNoveltyPercentile = f.noveltyPercentile;
101
+ cfg.flowEvalEveryNFrames = f.evalEveryNFrames;
102
+ cfg.flowMaxTranslationCm = f.maxTranslationCm;
103
+ cfg.flowMaxCorners = f.maxCorners;
104
+ cfg.flowQualityLevel = f.qualityLevel;
105
+ cfg.flowMinDistance = f.minDistance;
106
+ return cfg;
107
+ }
108
+ /**
109
+ * Convert a v0.4 SlitscanSettings tree into the flat dict the
110
+ * slit-scan / firstwins native engines read. Handles the
111
+ * "presence-as-enable" boolean expansion: a non-undefined
112
+ * `registration.ncc1d` means `enable1dNcc: true` on the wire,
113
+ * with the sub-object's `searchRadius` carried alongside.
114
+ *
115
+ * Verified against:
116
+ * - iOS `IncrementalStitcher.swift:1006-1100` (applyConfigOverrides)
117
+ * - iOS `OpenCVSlitScanStitcher.mm` (all numbered references in
118
+ * the audit ground-truth matrix)
119
+ */
120
+ function slitscanSettingsToNativeConfig(s) {
121
+ const cfg = {
122
+ captureSource: s.captureSource,
123
+ // The native side reads `engine: 'slitscan-…'` at start time
124
+ // from a separate top-level field, NOT from configOverrides.
125
+ // We still serialise the variant here for hosts that want to
126
+ // round-trip a single settings object through both surfaces.
127
+ engineVariant: s.variant,
128
+ // ── Painting ─────────────────────────────────────────────────
129
+ paintMode: s.painting.paintMode,
130
+ sliverPosition: s.painting.sliverPosition,
131
+ firstFrameFullFrame: s.painting.firstFrameFullFrame,
132
+ // ── Registration (explicit booleans) ─────────────────────────
133
+ enableTriangulation: s.registration.enableTriangulation,
134
+ enableTriAccumulator: s.registration.enableTriAccumulator,
135
+ enableRansacHomography: s.registration.enableRansacHomography,
136
+ // ── Plane projection ─────────────────────────────────────────
137
+ planeSource: s.plane.source,
138
+ };
139
+ // ── 1D NCC: presence-as-enable ─────────────────────────────────
140
+ if (s.registration.ncc1d) {
141
+ cfg.enable1dNcc = true;
142
+ cfg.nccSearchRadius1d = s.registration.ncc1d.searchRadius;
143
+ }
144
+ else {
145
+ cfg.enable1dNcc = false;
146
+ }
147
+ // ── 2D NCC: presence-as-enable + nested optionals ──────────────
148
+ if (s.registration.ncc2d) {
149
+ const n2 = s.registration.ncc2d;
150
+ cfg.enable2dNcc = true;
151
+ cfg.nccSearchMargin2d = n2.searchMargin;
152
+ cfg.nccConfidenceThreshold2d = n2.confidenceThreshold;
153
+ if (n2.emaSmoothing) {
154
+ cfg.enableNcc2dEmaSmoothing = true;
155
+ cfg.ncc2dEmaAlpha = n2.emaSmoothing.alpha;
156
+ }
157
+ else {
158
+ cfg.enableNcc2dEmaSmoothing = false;
159
+ }
160
+ if (n2.panAxisLock) {
161
+ cfg.enableNcc2dPanAxisLock = true;
162
+ cfg.ncc2dCrossAxisLockPx = n2.panAxisLock.crossAxisLockPx;
163
+ }
164
+ else {
165
+ cfg.enableNcc2dPanAxisLock = false;
166
+ }
167
+ }
168
+ else {
169
+ cfg.enable2dNcc = false;
170
+ }
171
+ // ── Plane optionals ────────────────────────────────────────────
172
+ // Only emit when `source` actually consumes the field. Native
173
+ // tolerates unsolicited keys but the modal also walks the dict
174
+ // to decide which sliders to render — extra keys would mislead.
175
+ if (s.plane.source !== 'Disabled' && s.plane.projectionStyle !== undefined) {
176
+ cfg.planeProjectionStyle = s.plane.projectionStyle;
177
+ }
178
+ if (s.plane.source === 'Virtual' && s.plane.virtualDepthMeters !== undefined) {
179
+ cfg.virtualPlaneDepthMeters = s.plane.virtualDepthMeters;
180
+ }
181
+ if (s.plane.source === 'ARKitDetected' && s.plane.alignmentThreshold !== undefined) {
182
+ cfg.arkitPlaneAlignmentThreshold = s.plane.alignmentThreshold;
183
+ }
184
+ // ── Advanced motion knobs (only emit if explicitly set) ────────
185
+ if (s.advanced?.panAxisFractionRect !== undefined) {
186
+ cfg.kPanAxisFractionRect = s.advanced.panAxisFractionRect;
187
+ }
188
+ if (s.advanced?.minAcceptDeltaPx !== undefined) {
189
+ cfg.kMinAcceptDeltaPx = s.advanced.minAcceptDeltaPx;
190
+ }
191
+ return cfg;
192
+ }
193
+ /**
194
+ * Convert a v0.4 HybridSettings tree into the flat dict the hybrid
195
+ * engine reads. Minimal surface — hybrid presets internally clobber
196
+ * almost everything; see HybridSettings JSDoc for context.
197
+ *
198
+ * Verified against:
199
+ * - iOS `OpenCVIncrementalStitcher.mm:139-180` (preset paths)
200
+ * - iOS `IncrementalStitcher.swift:1034-1040` (hybridProjection override)
201
+ */
202
+ function hybridSettingsToNativeConfig(s) {
203
+ return {
204
+ captureSource: s.captureSource,
205
+ hybridProjection: s.projection,
206
+ };
207
+ }
208
+ //# sourceMappingURL=PanoramaSettingsBridge.js.map
@@ -1,306 +1,58 @@
1
1
  /**
2
- * PanoramaSettingsModal — runtime A/B testing surface for the
3
- * stitcher pipeline. Operators in the field can toggle warper,
4
- * blender, and tuning constants between captures to see what
5
- * looks best on real shelf scenes.
2
+ * PanoramaSettingsModal — runtime tuning surface for <Camera>'s
3
+ * batch-keyframe panorama capture.
6
4
  *
7
- * The modal is presentational: the host owns the settings state
8
- * (typically `useState<PanoramaSettings>`) and renders the modal
9
- * with `visible` toggled by a gear-icon press in the capture
10
- * header. Settings flow OUT via `onChange` for each tweak.
5
+ * v0.4 rewrite (Phase 2 of F10):
6
+ * ──────────────────────────────
11
7
  *
12
- * Why expose this as an SDK component instead of leaving it to
13
- * each host? The set of tunable knobs IS the SDK's contract
14
- * if a new setting is added (e.g. registration MP) the SDK ships
15
- * the UI for it in lockstep with the param itself, instead of
16
- * forcing every host app to update its settings screen.
8
+ * The v0.3 modal exposed a flat 45-field surface that mixed
9
+ * batch-keyframe knobs with slit-scan, hybrid, and video-recording
10
+ * fallback fields the engine never reads in <Camera>'s
11
+ * `engine: 'batch-keyframe'` path. The 2026-05-22 audit (v0.3.0
12
+ * CHANGELOG) traced every field's native consumer and proved most of
13
+ * the cross-engine fields were dead surface in this modal.
14
+ *
15
+ * v0.4 narrows the modal to exactly the surface <Camera> consumes:
16
+ * the `PanoramaSettings` type defined in `./PanoramaSettings.ts`. Each
17
+ * section in the modal mirrors a sub-tree of that type — operators see
18
+ * the same shape in the UI as the code, and host apps that want to
19
+ * tune slit-scan or hybrid engines build their own analogous
20
+ * SlitscanSettingsModal / HybridSettingsModal on top of those types.
21
+ *
22
+ * UI structure (matches the type tree):
23
+ *
24
+ * - Debug (top-level, `debug`)
25
+ * - Frame selection (`frameSelection`, closed by default)
26
+ * - Mode
27
+ * - Max keyframes
28
+ * - Overlap threshold
29
+ * - Flow tunables (`frameSelection.flow`, only when
30
+ * mode === 'flow-based')
31
+ * - Max corners
32
+ * - Quality level
33
+ * - Min distance
34
+ * - Max translation cm
35
+ * - Novelty percentile
36
+ * - Eval every N frames
37
+ * - Stitcher (`stitcher`, closed by default)
38
+ * - Stitch mode
39
+ * - Warper type
40
+ * - Blender
41
+ * - Seam finder
42
+ * - Inscribed-rect crop
43
+ * - Reset to defaults (button)
44
+ *
45
+ * Note: `captureSource` (AR vs non-AR) is NOT surfaced here. The
46
+ * camera-screen AR toggle owns that state — Camera.tsx overrides the
47
+ * native bridge's `captureSource` with the derived
48
+ * `effectiveCaptureSource` so settings and runtime stay in sync.
49
+ *
50
+ * The reusable `Accordion` + `SectionHeader` + `SegmentedControl` +
51
+ * `Tag` helpers from the v0.3 modal are preserved verbatim — only the
52
+ * data-binding layer changed.
17
53
  */
18
54
  import React from 'react';
19
- export interface PanoramaSettings {
20
- warperType: 'plane' | 'cylindrical' | 'spherical';
21
- blenderType: 'multiband' | 'feather';
22
- /**
23
- * Seam finder strategy. "graphcut" finds optimal seams before
24
- * blending (cleaner output, pairs with multiband, more memory).
25
- * "skip" streams warp+feed (lower peak memory, fine with feather).
26
- */
27
- seamFinderType: 'graphcut' | 'skip';
28
- /**
29
- * V16 Phase 1b.fix5c (Ram's call 2026-05-10) — toggle the
30
- * max-inscribed-rectangle crop on the batch-keyframe output
31
- * panorama. When false (default), the output is cropped to the
32
- * bounding rectangle of non-black pixels only (cv::boundingRect)
33
- * — preserves all stitched content at the cost of some black
34
- * corners where cv::Stitcher's projection didn't fill. When
35
- * true, the post-stitch pipeline additionally runs
36
- * `MaxInscribedRectFromMask` to find the largest axis-aligned
37
- * rectangle entirely inside content, followed by the
38
- * column-projection second-pass. Inscribed-rect can be
39
- * over-aggressive on lopsided masks (field log showed a
40
- * 1146×1102 bbox shrinking to a 602×1102 strip), so default OFF
41
- * lets the operator see the full stitched scene; flip ON to
42
- * A/B against the cleaner-but-smaller output.
43
- */
44
- enableMaxInscribedRectCrop: boolean;
45
- /**
46
- * Phase 4.4 EXPERIMENTAL: when true, the host swaps the
47
- * vision-camera-backed CameraView for an ARKit-backed ARCameraView
48
- * during panorama capture. Default false (keeps the existing
49
- * stitcher flow untouched). Phase 5 will add AR-backed photo /
50
- * video capture and pose-driven stitching; until then this is
51
- * preview-only — useful for verifying the AR session renders
52
- * cleanly on the operator's device before we cut over.
53
- */
54
- useARPreview: boolean;
55
- /**
56
- * V15 — Incremental engine choice for live realtime stitching.
57
- * 'hybrid' — Whole-frame projection + feature matching;
58
- * planar by default (was cylindrical).
59
- * 'slitscan-rotate' — V13.0a baseline + 1D NCC for rotation
60
- * wobble correction.
61
- * 'slitscan-both' — DEFAULT. V13.0a + no accept gate +
62
- * feather blend. Iterate via per-stage
63
- * toggles below.
64
- *
65
- * All three are A/B-comparable on the same scene by toggling here
66
- * without restarting the app.
67
- */
68
- incrementalEngine: 'batch-keyframe' | 'hybrid' | 'slitscan-rotate' | 'slitscan-both';
69
- /**
70
- * V15 — Slit-scan slit width (fraction of pan-axis retained per
71
- * frame). Range 0.10 – 0.70. Smaller = less within-slit multi-
72
- * depth disagreement but tighter overlap budget at fast pans.
73
- * Default 0.30. Only applied to slitscan-* engines.
74
- */
75
- slitWidthFraction: number;
76
- /**
77
- * V15 — Per-stage correction toggles for slitscan-both. Settings
78
- * UI exposes these so iteration happens via toggles, not rebuilds.
79
- */
80
- acceptGate: 0 | 50;
81
- enableTriangulation: boolean;
82
- enableTriAccumulator: boolean;
83
- enable2dNcc: boolean;
84
- enableRansacHomography: boolean;
85
- paintMode: 'FirstPaintedWins' | 'FeatherBlend';
86
- hybridProjection: 'Cylindrical' | 'Planar';
87
- /** 1D NCC search radius (slitscan-rotate only). */
88
- nccSearchRadius1d: number;
89
- /** **DEPRECATED in V15.0d** — see `planeSource`. Kept on the type
90
- * for backward compat with stored settings. When `planeSource`
91
- * is 'Disabled' (default) and this is true, the engine treats it
92
- * as 'ARKitDetected'. */
93
- useDetectedPlane: boolean;
94
- /** V15.0d — source of the plane used by the V15.0b plane-projected
95
- * stitch path. Slit-scan modes only.
96
- *
97
- * - 'Disabled': no plane projection (plain slit-scan).
98
- * - 'ARKitDetected': use ARKit's first vertical plane that aligns
99
- * with the camera's view direction. Falls back to slit-scan
100
- * silently when no aligned plane is found.
101
- * - 'Virtual': synthesize a plane perpendicular to the camera at
102
- * `virtualPlaneDepthMeters` distance. Always works; loses
103
- * "real depth" advantage but immune to ARKit picking the wrong
104
- * surface (which is the common failure mode for ARKitDetected). */
105
- planeSource: 'Disabled' | 'ARKitDetected' | 'Virtual';
106
- /** V15.0d — depth (m) of the synthetic plane in front of the camera
107
- * when `planeSource = 'Virtual'`. 0.3 – 5.0 m. Default 1.5 m. */
108
- virtualPlaneDepthMeters: number;
109
- /** V15.0d — alignment threshold (cosine) for ARKit-detected planes.
110
- * Higher = stricter (fewer planes accepted). 0.0 – 1.0.
111
- * Default 0.6 (≈53° max angle off-camera). */
112
- arkitPlaneAlignmentThreshold: number;
113
- /** V15.0g — plane-projection rendering style. Trapezoidal is the
114
- * V15.0b legacy 3D-correct mapping; Rectified is V15.0g's clean-
115
- * rectangle paste that eliminates tilt-induced trapezoidal
116
- * distortion. Default Rectified. Ignored when planeSource =
117
- * Disabled. */
118
- planeProjectionStyle: 'Trapezoidal' | 'Rectified';
119
- /** V15.0d — 2D NCC search half-window in pixels. 4 – 30.
120
- * Default 12. */
121
- nccSearchMargin2d: number;
122
- /** V15.0d — 2D NCC confidence threshold below which corrections
123
- * are rejected. 0.30 – 0.99. Default 0.75. */
124
- nccConfidenceThreshold2d: number;
125
- /** V15.0d (1B) — EMA smoothing on 2D NCC corrections to damp
126
- * single-frame snaps. Default false. */
127
- enableNcc2dEmaSmoothing: boolean;
128
- /** V15.0d — EMA weight on the CURRENT-frame correction. 0.05 – 0.95.
129
- * Default 0.4 (60% prev / 40% current). */
130
- ncc2dEmaAlpha: number;
131
- /** V15.0d (1C) — pan-axis-aware 2D NCC: clamp the cross-axis
132
- * correction tighter than the pan-axis. Default false. */
133
- enableNcc2dPanAxisLock: boolean;
134
- /** V15.0d — cross-axis clamp (px) when pan-axis lock is on.
135
- * 0 – 30. Default 5. */
136
- ncc2dCrossAxisLockPx: number;
137
- /** V16 — frame-selection mode for the live engine.
138
- *
139
- * - 'time-based' (default): every ARFrame is forwarded to the
140
- * engine; the engine's own gate (kMinAcceptDeltaPx etc.) decides.
141
- * Backward-compatible with all prior versions.
142
- * - 'pose-based': frames are pre-filtered by a KeyframeGate that
143
- * projects each onto the latched ARKit plane and accepts only
144
- * when overlap with the previous keyframe is < 1 −
145
- * overlapThreshold. Bounded to keyframeMaxCount frames per
146
- * capture (matches iOS Camera / Samsung Pano architecture).
147
- * Requires planeSource != 'Disabled' to engage.
148
- * - 'flow-based' (V16 A2, DEFAULT): same KeyframeGate cap +
149
- * threshold but the novelty metric is sparse-Lucas-Kanade
150
- * optical flow on full-frame content instead of plane-projected
151
- * polygon overlap. Plane-independent (scale-invariant — works
152
- * regardless of latched plane size); the metric is "median
153
- * pan-axis feature displacement / pan-axis frame dim", which is
154
- * a direct measure of % new content on the leading edge. Falls
155
- * back to angular delta when feature tracking fails (texture-
156
- * poor scene / motion exceeds KLT pyramid window). */
157
- frameSelectionMode: 'time-based' | 'pose-based' | 'flow-based';
158
- /** V16 — required NEW-content fraction for a keyframe to be
159
- * accepted (pose-based AND flow-based modes share this knob;
160
- * both interpret 0.40 as "40 % new content"). Tuneable from
161
- * 0.20 to 0.60 in the modal. */
162
- keyframeOverlapThreshold: number;
163
- /** V16 — hard cap on keyframes per capture (pose-based + flow-
164
- * based modes). Default 6. Once reached, all further frames are
165
- * rejected and the host should auto-finalize. */
166
- keyframeMaxCount: number;
167
- /** V16 A2 — flow-based mode: max Shi-Tomasi corners to detect per
168
- * accepted keyframe. More = more robust median pan-axis
169
- * displacement but slower detect (~15-25 ms at 150 on iPhone 13
170
- * Pro). Range 50 – 300, default 150. */
171
- flowMaxCorners: number;
172
- /** V16 A2 — flow-based mode: Shi-Tomasi quality level (0, 1].
173
- * Lower = more (weaker) corners detected; higher = fewer
174
- * (stronger) corners. Default 0.01. Range 0.005 – 0.05 in the
175
- * modal. */
176
- flowQualityLevel: number;
177
- /** V16 A2 — flow-based mode: minimum pixel distance between
178
- * detected corners at WORKING resolution (the gate internally
179
- * downscales the frame to 720 px longest side for KLT). Higher
180
- * = more spatially-spread features. Default 10. */
181
- flowMinDistance: number;
182
- /** V16 — flow-based mode: translation budget in CENTIMETRES.
183
- * When > 0, the gate force-accepts a frame if the camera has
184
- * translated more than this distance (3D Euclidean) since the
185
- * last accepted keyframe — even when novelty < threshold.
186
- * Bounds the parallax between adjacent keyframes so the
187
- * downstream affine stitcher matcher can fit a homography.
188
- * Range 0 – 100 cm in the modal, default 0 = disabled.
189
- * Recommended starting value once enabled: 8 cm. */
190
- flowMaxTranslationCm: number;
191
- /** V16 — flow-based mode: percentile used to aggregate tracked-
192
- * feature absolute displacements into the novelty estimate.
193
- * Pre-V16 used median (0.50); 0.85 picks up leading-edge
194
- * motion sooner — matches user perception of "new content
195
- * visible" better. Range 0.50 – 0.99, default 0.85. */
196
- flowNoveltyPercentile: number;
197
- /** V16 — flow-based mode: eval-throttle. Gate evaluation runs
198
- * every Nth consumeFrame from the AR delegate instead of every
199
- * frame. Pure CPU/battery savings — doesn't change WHICH
200
- * frames are accepted, just samples less frequently. Range
201
- * 1 – 10, default 1 (every frame). */
202
- flowEvalEveryNFrames: number;
203
- /** V15.0c — sliver position within the camera frame. 'Center' is
204
- * V13.x default. 'Bottom' takes leading-edge content for top-to-
205
- * bottom pan; 'Top' for bottom-to-top pan. */
206
- sliverPosition: 'Center' | 'Bottom' | 'Top';
207
- /** V15.0c — paint full first frame, then add slivers as user pans.
208
- * Useful with 'Bottom' or 'Top' sliverPosition. */
209
- firstFrameFullFrame: boolean;
210
- /** Hard cap on hold duration (ms). 0 disables auto-stop. */
211
- maxRecordingMs: number;
212
- /** Frames per second of recording to sample for stitching. */
213
- framesPerSecond: number;
214
- /** Floor / ceiling on extracted frame count. */
215
- minFrames: number;
216
- maxFrames: number;
217
- /** JPEG quality (0-100) for output panorama. */
218
- quality: number;
219
- /**
220
- * 2026-05-14 (revised) — capture-source picker for the panorama
221
- * camera screen. Two options after the 2026-05-14 user-reported
222
- * Galaxy A35 crash + simplification request:
223
- *
224
- * 'ar' (DEFAULT) — Use the AR stack (ARKit on iOS, ARCore on
225
- * Android). Plane detection, pose-aware
226
- * capture, pose-driven gate. Falls back to
227
- * non-AR silently if the device doesn't
228
- * support AR.
229
- * 'non-ar' — Use vision-camera. Disables all AR-based
230
- * services (planeSource=Disabled, no plane
231
- * polling, no AR session, frameSelectionMode
232
- * flipped to flow-based). Lens-switcher chip
233
- * on the capture screen lets the operator
234
- * toggle 0.5× / 1× without re-opening Settings.
235
- * The chip is hidden if the device has only
236
- * one physical back lens.
237
- *
238
- * Cascade: switching from 'ar' → 'non-ar' triggers a useEffect
239
- * in `AuditCaptureScreen` that patches dependent settings
240
- * (planeSource, frameSelectionMode, useARPreview) to a coherent
241
- * non-AR state. Operators don't have to know which other
242
- * settings to flip.
243
- *
244
- * Earlier draft (replaced 2026-05-14) had 4 values:
245
- * 'auto' | 'ar' | 'wide' | 'ultrawide'. The pre-mount
246
- * physical-lens selection ('wide' / 'ultrawide') crashed the
247
- * Galaxy A35 vision-camera CameraCaptureSession with a Parcel
248
- * exception (physical_camera_id=null in AidlCamera3-Device
249
- * configureStreams) — Camera2 can't be reliably steered to a
250
- * specific physical lens via vision-camera's `physicalDevices`
251
- * filter on this hardware. The post-mount on-screen chip path
252
- * works because vision-camera selects the safe multi-lens
253
- * virtual device first, and the lens swap happens against an
254
- * already-open camera.
255
- */
256
- captureSource: 'ar' | 'non-ar';
257
- /**
258
- * 2026-05-16 (Issue 5) — diagnostic toast on every successful
259
- * finalize. When `true`, the host renders a transient toast
260
- * summarising the C+D progressive-confidence retry telemetry:
261
- *
262
- * "Stitch: 6/6 frames retained at thresh 1.00 (1 attempt)"
263
- *
264
- * Defaults to `false` so end-users don't see it. Toggle from the
265
- * Settings modal under "Debug". Independent from any log-level
266
- * controls — purely a UI affordance for field testing.
267
- */
268
- debug: boolean;
269
- /**
270
- * 2026-05-14 — `cv::Stitcher` pipeline mode for the batch stitch.
271
- *
272
- * 'auto' (DEFAULT)
273
- * The capture engine looks at the accumulated translation vs
274
- * rotation magnitudes between first and last accepted keyframe
275
- * poses (AR-mode) or the windowed IMU integration (non-AR
276
- * mode) and picks PANORAMA or SCANS at finalize time.
277
- *
278
- * 'panorama'
279
- * `cv::Stitcher::PANORAMA` — rotation-only pipeline. Best for
280
- * "rotate phone in place to capture a wide field of view"
281
- * captures. ORB feature matching + global BundleAdjusterRay +
282
- * SphericalWarper. Sharp seams, expensive memory. WARNING:
283
- * on translation-heavy input the rotation-only homography fit
284
- * diverges and the canvas can blow up to multi-GB on Android
285
- * (2026-05-14 lmkd kill observed). Pick this only for genuine
286
- * rotation panoramas.
287
- *
288
- * 'scans'
289
- * `cv::Stitcher::SCANS` — translational pipeline. Best for
290
- * "walk past a shelf and pan sideways" captures. Affine
291
- * matcher + AffineBasedEstimator + BundleAdjusterAffine +
292
- * PlaneWarper. Canvas size bounded by sum of frame areas.
293
- * Slight quality drop on pure rotations but works for them too.
294
- *
295
- * iOS NOTE: as of 2026-05-14 the iOS stitcher uses a hand-rolled
296
- * PANORAMA-style pipeline (OpenCVStitcher.mm:600+) regardless of
297
- * this setting. Setting is passed through to iOS but ignored.
298
- * Android honours it via image_stitcher_jni.cpp. Bridging iOS is
299
- * a follow-up.
300
- */
301
- stitchMode: 'auto' | 'panorama' | 'scans';
302
- }
303
- export declare const DEFAULT_PANORAMA_SETTINGS: PanoramaSettings;
55
+ import { type PanoramaSettings } from './PanoramaSettings';
304
56
  export interface PanoramaSettingsModalProps {
305
57
  visible: boolean;
306
58
  settings: PanoramaSettings;