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.
- package/CHANGELOG.md +511 -1
- package/README.md +1 -1
- package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
- package/cpp/stitcher.cpp +101 -1
- package/cpp/stitcher.hpp +8 -0
- package/dist/camera/Camera.d.ts +9 -0
- package/dist/camera/Camera.js +165 -43
- package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
- package/dist/camera/CaptureDebugOverlay.js +146 -0
- package/dist/camera/CaptureKeyframePill.d.ts +28 -0
- package/dist/camera/CaptureKeyframePill.js +60 -0
- package/dist/camera/CaptureMemoryPill.d.ts +28 -0
- package/dist/camera/CaptureMemoryPill.js +109 -0
- package/dist/camera/CaptureOrientationPill.d.ts +22 -0
- package/dist/camera/CaptureOrientationPill.js +44 -0
- package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
- package/dist/camera/CaptureStitchStatsToast.js +133 -0
- package/dist/camera/PanoramaSettings.d.ts +478 -0
- package/dist/camera/PanoramaSettings.js +120 -0
- package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
- package/dist/camera/PanoramaSettingsBridge.js +208 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
- package/dist/camera/PanoramaSettingsModal.js +189 -354
- package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
- package/dist/camera/buildPanoramaInitialSettings.js +97 -0
- package/dist/camera/lowMemDevice.d.ts +24 -0
- package/dist/camera/lowMemDevice.js +69 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +37 -2
- package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
- package/dist/sensors/useIMUTranslationGate.js +83 -1
- package/dist/stitching/incremental.d.ts +25 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
- package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
- package/package.json +6 -2
- package/src/camera/Camera.tsx +220 -54
- package/src/camera/CaptureDebugOverlay.tsx +180 -0
- package/src/camera/CaptureKeyframePill.tsx +77 -0
- package/src/camera/CaptureMemoryPill.tsx +96 -0
- package/src/camera/CaptureOrientationPill.tsx +57 -0
- package/src/camera/CaptureStitchStatsToast.tsx +155 -0
- package/src/camera/PanoramaSettings.ts +605 -0
- package/src/camera/PanoramaSettingsBridge.ts +238 -0
- package/src/camera/PanoramaSettingsModal.tsx +296 -988
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
- package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
- package/src/camera/buildPanoramaInitialSettings.ts +139 -0
- package/src/camera/lowMemDevice.ts +71 -0
- package/src/index.ts +61 -3
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,515 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.4.0] — 2026-05-23
|
|
20
|
+
|
|
21
|
+
### v0.4 settings revamp (F10)
|
|
22
|
+
|
|
23
|
+
> [!WARNING]
|
|
24
|
+
> **Breaking type change.** The flat 45-field `PanoramaSettings`
|
|
25
|
+
> interface from v0.3 has been replaced with three engine-discriminated
|
|
26
|
+
> hierarchical types (`PanoramaSettings`, `SlitscanSettings`,
|
|
27
|
+
> `HybridSettings`). Consumers passing custom settings literals to
|
|
28
|
+
> `<Camera>` or to a Layer 2 modal must migrate to the new shape; the
|
|
29
|
+
> v0.3 type is deleted, not aliased. The C++ engine wire format is
|
|
30
|
+
> unchanged — only the JS-side type surface moved.
|
|
31
|
+
>
|
|
32
|
+
> **Migration guide:** [`docs/migrations/v0.3-to-v0.4-panorama-settings.md`](docs/migrations/v0.3-to-v0.4-panorama-settings.md)
|
|
33
|
+
> walks through every recipe (default-only hosts, custom-literal
|
|
34
|
+
> hosts, slit-scan / hybrid hosts, storage migration for persisted
|
|
35
|
+
> settings).
|
|
36
|
+
|
|
37
|
+
#### Why
|
|
38
|
+
|
|
39
|
+
The 2026-05-22 audit (entry below in v0.3.0) traced every
|
|
40
|
+
`PanoramaSettings` field's native consumer and proved the flat type
|
|
41
|
+
mixed three engines' (batch-keyframe, slit-scan, hybrid) settings into
|
|
42
|
+
one bag of disjoint subsets. Hosts had no way to know at the type
|
|
43
|
+
level which settings their chosen engine would even read; the modal
|
|
44
|
+
exposed knobs that were silently ignored on the active engine. The
|
|
45
|
+
revamp splits the type along engine boundaries so the types match what
|
|
46
|
+
each engine actually consumes.
|
|
47
|
+
|
|
48
|
+
#### What changed
|
|
49
|
+
|
|
50
|
+
- **New file:** `src/camera/PanoramaSettings.ts` — `CaptureBaseSettings`
|
|
51
|
+
+ three top-level types (`PanoramaSettings`, `SlitscanSettings`,
|
|
52
|
+
`HybridSettings`), each with co-located `DEFAULT_*_SETTINGS`. Sub-trees
|
|
53
|
+
group related knobs: `stitcher` / `frameSelection.flow` (panorama);
|
|
54
|
+
`painting` / `registration.ncc1d` / `registration.ncc2d.emaSmoothing` /
|
|
55
|
+
`registration.ncc2d.panAxisLock` / `plane` / `advanced` (slitscan).
|
|
56
|
+
- **New file:** `src/camera/PanoramaSettingsBridge.ts` — three pure
|
|
57
|
+
adapter functions (`panoramaSettingsToNativeConfig`,
|
|
58
|
+
`slitscanSettingsToNativeConfig`, `hybridSettingsToNativeConfig`)
|
|
59
|
+
that translate the typed JS tree → the flat
|
|
60
|
+
`Record<string, primitive>` the native bridges consume. Handles
|
|
61
|
+
presence-as-enable (`ncc1d` defined ⇒ `enable1dNcc: true` on the
|
|
62
|
+
wire) and source-conditional plane optionals.
|
|
63
|
+
- **New file:** `src/camera/buildPanoramaInitialSettings.ts` — pure
|
|
64
|
+
helper that translates `<Camera>`'s `default*` props into the
|
|
65
|
+
initial `PanoramaSettings` snapshot. Takes the device's low-mem
|
|
66
|
+
classification as an argument so the function stays pure and
|
|
67
|
+
testable.
|
|
68
|
+
- **Rewritten:** `src/camera/PanoramaSettingsModal.tsx` — now consumes
|
|
69
|
+
the new `PanoramaSettings` shape. UI sections mirror the type tree
|
|
70
|
+
(Capture source, Debug, Stitcher accordion, Frame Selection
|
|
71
|
+
accordion with nested Flow tunables). ~600 LOC smaller than v0.3
|
|
72
|
+
because dead slit-scan / hybrid / video-recording fields are gone.
|
|
73
|
+
- **Rewired:** `src/camera/Camera.tsx` — settings state uses the new
|
|
74
|
+
type; `incremental.start({ config })` now passes
|
|
75
|
+
`panoramaSettingsToNativeConfig(settings)` instead of an inline flat
|
|
76
|
+
dict. IMU translation gate reads
|
|
77
|
+
`settings.frameSelection.flow?.maxTranslationCm`. Debug overlay
|
|
78
|
+
reads `settings.frameSelection.mode` + `settings.stitcher.stitchMode`.
|
|
79
|
+
- **Updated:** `src/index.ts` — exports the new types + adapters; drops
|
|
80
|
+
the deleted v0.3 type.
|
|
81
|
+
- **Test infra:** added `jest` + `ts-jest` + `@types/jest` devDeps; new
|
|
82
|
+
`jest.config.js`, `tsconfig.test.json`, `tsconfig.build.json` (the
|
|
83
|
+
latter excludes `__tests__/` from the shipped `dist/`). 19 tests
|
|
84
|
+
across two suites cover the bridge round-trips, presence-as-enable
|
|
85
|
+
cases, plane-source variants, and prop→settings-tree translation.
|
|
86
|
+
|
|
87
|
+
#### Migration table — v0.3 flat → v0.4 hierarchical
|
|
88
|
+
|
|
89
|
+
For `<Camera>`-consuming hosts (the only public path that took
|
|
90
|
+
`PanoramaSettings` in v0.3):
|
|
91
|
+
|
|
92
|
+
| v0.3 field | v0.4 path |
|
|
93
|
+
|----------------------------------|-------------------------------------------------|
|
|
94
|
+
| `captureSource` | `captureSource` (unchanged) |
|
|
95
|
+
| `debug` | `debug` (unchanged) |
|
|
96
|
+
| `stitchMode` | `stitcher.stitchMode` |
|
|
97
|
+
| `warperType` | `stitcher.warperType` |
|
|
98
|
+
| `blenderType` | `stitcher.blenderType` |
|
|
99
|
+
| `seamFinderType` | `stitcher.seamFinderType` |
|
|
100
|
+
| `enableMaxInscribedRectCrop` | `stitcher.enableMaxInscribedRectCrop` |
|
|
101
|
+
| `frameSelectionMode` | `frameSelection.mode` |
|
|
102
|
+
| `keyframeMaxCount` | `frameSelection.maxKeyframes` |
|
|
103
|
+
| `keyframeOverlapThreshold` | `frameSelection.overlapThreshold` |
|
|
104
|
+
| `flowNoveltyPercentile` | `frameSelection.flow.noveltyPercentile` |
|
|
105
|
+
| `flowEvalEveryNFrames` | `frameSelection.flow.evalEveryNFrames` |
|
|
106
|
+
| `flowMaxTranslationCm` | `frameSelection.flow.maxTranslationCm` |
|
|
107
|
+
| `flowMaxCorners` | `frameSelection.flow.maxCorners` |
|
|
108
|
+
| `flowQualityLevel` | `frameSelection.flow.qualityLevel` |
|
|
109
|
+
| `flowMinDistance` | `frameSelection.flow.minDistance` |
|
|
110
|
+
|
|
111
|
+
#### Deleted from the public type surface
|
|
112
|
+
|
|
113
|
+
These fields were consumed only by slit-scan or hybrid engines (or
|
|
114
|
+
not consumed at all per the audit) and were dead surface on
|
|
115
|
+
`<Camera>`'s batch-keyframe path:
|
|
116
|
+
|
|
117
|
+
- `incrementalEngine` — `<Camera>` always uses `batch-keyframe`; the
|
|
118
|
+
knob never reached this component. Hosts that want slit-scan or
|
|
119
|
+
hybrid build their own capture flow on `incremental.start()` and
|
|
120
|
+
pass `SlitscanSettings` / `HybridSettings` instead.
|
|
121
|
+
- `useARPreview` — superseded by `captureSource` ('ar' / 'non-ar').
|
|
122
|
+
- `useDetectedPlane` — superseded by `SlitscanSettings.plane.source`.
|
|
123
|
+
- `planeSource`, `virtualPlaneDepthMeters`, `arkitPlaneAlignmentThreshold`,
|
|
124
|
+
`planeProjectionStyle` — slit-scan only; on `SlitscanSettings.plane.*`.
|
|
125
|
+
- `slitWidthFraction`, `sliverPosition`, `firstFrameFullFrame`,
|
|
126
|
+
`paintMode` — slit-scan only; on `SlitscanSettings.painting.*`.
|
|
127
|
+
- `acceptGate`, `enableTriangulation`, `enableTriAccumulator`,
|
|
128
|
+
`enable2dNcc`, `enableRansacHomography`, `nccSearchRadius1d`,
|
|
129
|
+
`nccSearchMargin2d`, `nccConfidenceThreshold2d`,
|
|
130
|
+
`enableNcc2dEmaSmoothing`, `ncc2dEmaAlpha`,
|
|
131
|
+
`enableNcc2dPanAxisLock`, `ncc2dCrossAxisLockPx` — slit-scan only;
|
|
132
|
+
on `SlitscanSettings.registration.*`.
|
|
133
|
+
- `hybridProjection` — hybrid only; on `HybridSettings.projection`.
|
|
134
|
+
- `maxRecordingMs`, `framesPerSecond`, `minFrames`, `maxFrames`,
|
|
135
|
+
`quality` — historical video-recording fallback fields with no
|
|
136
|
+
consumer on `<Camera>`'s batch-keyframe path.
|
|
137
|
+
|
|
138
|
+
#### Latent v0.3 bug fixed in passing
|
|
139
|
+
|
|
140
|
+
The v0.3 `<Camera>` accepted a `defaultCaptureSource` prop but the
|
|
141
|
+
internal `buildInitialSettings` function never copied it into
|
|
142
|
+
`settings.captureSource` — only into `arPreference` state. The
|
|
143
|
+
discrepancy meant the wire dict sent to native always reported
|
|
144
|
+
`captureSource: 'ar'` even when the operator's effective source was
|
|
145
|
+
`'non-ar'`, which silently disabled Android's `disableAngularFallback`
|
|
146
|
+
opt-out (audit fix F1). v0.4's `extractPanoramaOverrides` +
|
|
147
|
+
`buildPanoramaInitialSettings` route the prop through correctly.
|
|
148
|
+
Hosts using `defaultCaptureSource="non-ar"` will see native receive
|
|
149
|
+
the matching value for the first time.
|
|
150
|
+
|
|
151
|
+
#### Known limitation — modal Capture-source field vs. AR toggle
|
|
152
|
+
|
|
153
|
+
The on-screen AR toggle button at the bottom of `<Camera>` updates
|
|
154
|
+
`arPreference` state (and through it `effectiveCaptureSource`),
|
|
155
|
+
which decides which preview component mounts. The Capture-source
|
|
156
|
+
segmented control inside the settings modal updates
|
|
157
|
+
`settings.captureSource`, which only affects what's reported to the
|
|
158
|
+
native engine via `panoramaSettingsToNativeConfig` (gates Android's
|
|
159
|
+
angular-fallback opt-out per audit fix F1). These two values can
|
|
160
|
+
drift if the operator toggles the AR button without re-opening
|
|
161
|
+
settings, OR flips the modal field without touching the AR button.
|
|
162
|
+
The on-screen toggle is the canonical UI affordance for the live
|
|
163
|
+
preview path; the modal field is best thought of as a tester escape
|
|
164
|
+
hatch for the wire-format consequence. A future cleanup is to make
|
|
165
|
+
both update the same source of truth — out of scope for v0.4.
|
|
166
|
+
|
|
167
|
+
#### Migration example
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// Before (v0.3)
|
|
171
|
+
const settings: PanoramaSettings = {
|
|
172
|
+
captureSource: 'ar',
|
|
173
|
+
stitchMode: 'auto',
|
|
174
|
+
blenderType: 'multiband',
|
|
175
|
+
flowMaxTranslationCm: 50,
|
|
176
|
+
flowNoveltyPercentile: 0.85,
|
|
177
|
+
keyframeMaxCount: 6,
|
|
178
|
+
frameSelectionMode: 'flow-based',
|
|
179
|
+
// … 40+ more fields
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// After (v0.4)
|
|
183
|
+
const settings: PanoramaSettings = {
|
|
184
|
+
captureSource: 'ar',
|
|
185
|
+
debug: false,
|
|
186
|
+
stitcher: {
|
|
187
|
+
stitchMode: 'auto',
|
|
188
|
+
warperType: 'plane',
|
|
189
|
+
blenderType: 'multiband',
|
|
190
|
+
seamFinderType: 'graphcut',
|
|
191
|
+
enableMaxInscribedRectCrop: false,
|
|
192
|
+
},
|
|
193
|
+
frameSelection: {
|
|
194
|
+
mode: 'flow-based',
|
|
195
|
+
maxKeyframes: 6,
|
|
196
|
+
overlapThreshold: 0.20,
|
|
197
|
+
flow: {
|
|
198
|
+
noveltyPercentile: 0.85,
|
|
199
|
+
evalEveryNFrames: 5,
|
|
200
|
+
maxTranslationCm: 50,
|
|
201
|
+
maxCorners: 150,
|
|
202
|
+
qualityLevel: 0.01,
|
|
203
|
+
minDistance: 10,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Or just use the default:
|
|
209
|
+
import { DEFAULT_PANORAMA_SETTINGS } from 'react-native-image-stitcher';
|
|
210
|
+
const settings = { ...DEFAULT_PANORAMA_SETTINGS, captureSource: 'non-ar' };
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## [0.3.0] — 2026-05-23
|
|
215
|
+
|
|
216
|
+
> [!IMPORTANT]
|
|
217
|
+
> **v0.3.0 is the audit-follow-up release.** After v0.2.x we ran an
|
|
218
|
+
> exhaustive PanoramaSettings ground-truth audit and shipped the
|
|
219
|
+
> v0.3-pixel-data work alongside ~15 follow-up correctness fixes,
|
|
220
|
+
> two crash fixes, a stitcher mode-fallback retry, and the
|
|
221
|
+
> RetaiLens-parity debug UI port. Detailed entries below.
|
|
222
|
+
>
|
|
223
|
+
> **Behaviour changes**
|
|
224
|
+
> - Android AR mode + both platforms' non-AR mode now actually run
|
|
225
|
+
> the Flow strategy (sparse optical-flow novelty) end-to-end.
|
|
226
|
+
> Pre-0.3 they silently fell back to Pose strategy because no
|
|
227
|
+
> pixel data was supplied — hosts who tuned
|
|
228
|
+
> `keyframeOverlapThreshold` on those paths were tuning a
|
|
229
|
+
> different algorithm than is now active.
|
|
230
|
+
> - `stitchMode: 'auto'` now resolves correctly on iOS (was
|
|
231
|
+
> silently hardcoded to Panorama) and uses IMU-measured
|
|
232
|
+
> translation in non-AR mode.
|
|
233
|
+
> - `frameSelectionMode` is now honoured on both platforms;
|
|
234
|
+
> previously hardcoded to `'flow-based'`.
|
|
235
|
+
> - Mode-fallback retry: if the resolved cv::Stitcher mode fails
|
|
236
|
+
> with degenerate camera params, the stitcher automatically
|
|
237
|
+
> retries with the opposite mode before giving up.
|
|
238
|
+
|
|
239
|
+
### Added
|
|
240
|
+
|
|
241
|
+
- **Pixel-aware Flow strategy across all four capture paths** —
|
|
242
|
+
iOS AR, iOS non-AR, Android AR, Android non-AR. The C++
|
|
243
|
+
KeyframeGate's `evaluateWithFrame` overload is now reached from
|
|
244
|
+
every entry point with real grayscale pixel data (Y plane bytes
|
|
245
|
+
on AR paths, decoded JPEG luma on non-AR paths).
|
|
246
|
+
- **Debug UI suite** (gated by `settings.debug`):
|
|
247
|
+
`CaptureMemoryPill` (top-right), `CaptureKeyframePill` (top-center),
|
|
248
|
+
`CaptureOrientationPill` (top-left), `CaptureStitchStatsToast` +
|
|
249
|
+
`useStitchStatsToast` hook, plus a detailed metrics block
|
|
250
|
+
(`CaptureDebugOverlay`). All exported individually for Layer 2
|
|
251
|
+
hosts to compose their own debug surface.
|
|
252
|
+
- **`stitchModeResolved`** in `IncrementalFinalizeResult` +
|
|
253
|
+
`CameraCaptureResult.panorama` — surfaces which cv::Stitcher
|
|
254
|
+
pipeline actually ran (`panorama` / `scans`), useful for
|
|
255
|
+
displaying on the output preview.
|
|
256
|
+
|
|
257
|
+
### Fixed
|
|
258
|
+
|
|
259
|
+
- **F1 — Android `disableAngularFallback` was always false.**
|
|
260
|
+
The non-AR opt-out tested `captureSource ∈ {"wide", "ultrawide"}`
|
|
261
|
+
against a JS API that has been sending `"ar"` / `"non-ar"` since
|
|
262
|
+
v0.2. String mismatch silently nullified the opt-out → gyro
|
|
263
|
+
drift accepted near-identical frames → `STITCH_CAMERA_PARAMS_FAIL
|
|
264
|
+
— warpRoi too large (43039×55525)` on shelf-scan captures.
|
|
265
|
+
- **F1b — iOS `disableAngularFallback` wasn't wired at all.** The
|
|
266
|
+
C++ setter existed but the Swift facade had no property, the
|
|
267
|
+
Obj-C++ bridge had no method, and IncrementalStitcher never
|
|
268
|
+
called it. Same crash class as F1, just hidden until now.
|
|
269
|
+
- **F2 — iOS `stitchMode` was hardcoded to Panorama.** Now reads
|
|
270
|
+
the JS setting and resolves 'auto' via translation/rotation
|
|
271
|
+
magnitude-ratio (port of Android's resolveStitchModeAuto).
|
|
272
|
+
- **F2b — Auto-resolver uses IMU translation in non-AR mode.** The
|
|
273
|
+
JS-driver path doesn't carry pose tx/ty/tz, so the pose-only
|
|
274
|
+
resolver always picked 'panorama' even for shelf scans. Now
|
|
275
|
+
folds the IMU translation gate's measured displacement into the
|
|
276
|
+
resolver (`tMeters = max(tPose, tImu)`).
|
|
277
|
+
- **F2c — Cross-capture IMU drift bias.** Pre-fix the gravityX IIR
|
|
278
|
+
estimate was preserved across capture boundaries; if the phone
|
|
279
|
+
was at a different orientation between captures, the stale
|
|
280
|
+
estimate biased the linear-acceleration calculation for the
|
|
281
|
+
~200 ms IIR convergence window, integrating into posX and
|
|
282
|
+
compounding per-capture. Now reseed gravityX on every
|
|
283
|
+
subscription start (= every capture).
|
|
284
|
+
- **F2d — IMU gate auto-rearms on every budget interval.** Pre-fix
|
|
285
|
+
the gate latched after the first `markNextFrameAsLastKeyframe`
|
|
286
|
+
fire and never re-triggered. Now resets posX + velX + fired
|
|
287
|
+
internally so it fires every `flowMaxTranslationCm` of measured
|
|
288
|
+
translation.
|
|
289
|
+
- **F2e — Android batch-keyframe now emits overlap %.** Pre-fix
|
|
290
|
+
`overlapPercent` was hardcoded to -1 in the accept emit, and
|
|
291
|
+
reject events emitted nothing at all — debug overlay was frozen
|
|
292
|
+
between accepts. Now reflects the gate's actual newContent
|
|
293
|
+
fraction on both accepts and rejects.
|
|
294
|
+
- **F2f — IMU delta resets on ANY frame accept.** Pre-fix the
|
|
295
|
+
`imuΔ` debug indicator only reset when the IMU gate itself
|
|
296
|
+
fired; a flow-novelty accept left posX ticking up indefinitely.
|
|
297
|
+
Now Camera.tsx watches `acceptedCount` and resets the gate on
|
|
298
|
+
every increment. A separate `totalAbsMetres` accumulator banks
|
|
299
|
+
the magnitude across resets so the finalize-time auto-resolver
|
|
300
|
+
still sees full translation history.
|
|
301
|
+
- **F4 — Camera.tsx now passes the four flow-tunable fields and
|
|
302
|
+
`captureSource`.** Pre-fix `flowMaxCorners`, `flowQualityLevel`,
|
|
303
|
+
`flowMinDistance`, `enableMaxInscribedRectCrop`, and
|
|
304
|
+
`captureSource` were silently dropped between the modal and the
|
|
305
|
+
native bridge. Now all five reach the engine.
|
|
306
|
+
- **F5 — Android KeyframeGate gained the missing Flow-tunable
|
|
307
|
+
surface.** Added Kotlin facade properties + JNI thunks for
|
|
308
|
+
`setFlowMaxCorners`, `setFlowQualityLevel`, `setFlowMinDistance`,
|
|
309
|
+
`setStrategy`. Android now mirrors iOS for the gate's full
|
|
310
|
+
knob set. Added the eval-throttle (`flowEvalEveryNFrames`)
|
|
311
|
+
to the AR ingest path.
|
|
312
|
+
- **F6 — `frameSelectionMode` is no longer hardcoded to
|
|
313
|
+
'flow-based'.** Camera.tsx now passes the JS setting through;
|
|
314
|
+
both platforms honour `time-based` (gate disabled),
|
|
315
|
+
`pose-based` (Pose strategy), and `flow-based` (Flow strategy).
|
|
316
|
+
- **F7 — README documented `defaultFlowMaxTranslationCm` as 8
|
|
317
|
+
cm.** Actual default is 50 cm; 6× off.
|
|
318
|
+
- **ARCore Session.close() on AR-off** (Android-only crash fix).
|
|
319
|
+
Pre-fix `RNSARSession.stop()` and `stopForView()` called
|
|
320
|
+
`Session.pause()` then nulled the session reference. ARCore's
|
|
321
|
+
`pause()` only stops frame production — its native worker
|
|
322
|
+
threads stay alive. Orphaned, those threads kept running and
|
|
323
|
+
crashed under memory pressure with SIGSEGV in
|
|
324
|
+
`tango_pool_lp4`/`libarcore_c.so` (tombstone-confirmed). Now
|
|
325
|
+
calls `pause()` then `close()` (ARCore's documented full
|
|
326
|
+
teardown), and the camera-view drops its own stale reference.
|
|
327
|
+
- **Stitcher mode-fallback retry.** When the configured stitchMode
|
|
328
|
+
fails with degenerate camera params, the stitcher now
|
|
329
|
+
automatically retries with the opposite mode before giving up
|
|
330
|
+
(panorama → scans or scans → panorama). Result type carries
|
|
331
|
+
`stitchModeUsed` so callers can see which mode succeeded. The
|
|
332
|
+
warpRoi-too-large error message now includes the configured
|
|
333
|
+
mode + frame index for diagnostics.
|
|
334
|
+
- **Thumbnail strip first-frame race.** Pre-fix the `useEffect`
|
|
335
|
+
that cleared `batchKeyframeThumbnails` on statusPhase change
|
|
336
|
+
could race ahead of the JS subscriber: the AR camera's GL
|
|
337
|
+
thread could emit an ACCEPT during handleHoldStart's
|
|
338
|
+
`await incremental.start(...)` window, the subscriber would
|
|
339
|
+
add frame 0 to thumbnails, THEN React's queued statusPhase
|
|
340
|
+
effect would wipe the array — frame 0 was missing from the
|
|
341
|
+
strip. Fixed by moving the reset synchronously to the top of
|
|
342
|
+
handleHoldStart, before any await.
|
|
343
|
+
|
|
344
|
+
### Audit ground-truth findings (no code change, doc-only)
|
|
345
|
+
|
|
346
|
+
- **F1 — Android `disableAngularFallback` was always false.** The
|
|
347
|
+
Android JNI's non-AR opt-out for the angular-delta gate fallback
|
|
348
|
+
tested `captureSource ∈ {"wide", "ultrawide"}` against a JS API
|
|
349
|
+
that has been sending `"ar"` / `"non-ar"` since 2026-05-14. The
|
|
350
|
+
string mismatch silently nullified the opt-out for the entire
|
|
351
|
+
Android non-AR path, letting gyro drift accumulate into the
|
|
352
|
+
integrated yaw/pitch and produce near-identical "accepted"
|
|
353
|
+
frames — which is what blew up cv::Stitcher with the "warpRoi too
|
|
354
|
+
large (43039×55525) — estimator produced degenerate camera params"
|
|
355
|
+
error on shelf-scan captures. Fix: read `"non-ar"`.
|
|
356
|
+
- **F2 — iOS `stitchMode` setting is now honoured end-to-end.** Pre-
|
|
357
|
+
audit, `OpenCVStitcher.mm:436` hardcoded `cv::Stitcher::PANORAMA`
|
|
358
|
+
regardless of the JS setting, so operators picking `'scans'` or
|
|
359
|
+
`'auto'` from the modal saw no effect on iOS. iOS now reads
|
|
360
|
+
`configOverrides["stitchMode"]`, tracks first + last accepted
|
|
361
|
+
keyframe poses, and implements `resolveStitchModeAuto` (port of
|
|
362
|
+
Android's translation/rotation magnitude-ratio heuristic) at
|
|
363
|
+
finalize time. Both platforms now resolve `'auto'` identically.
|
|
364
|
+
- **F4 — Camera.tsx now passes settings the modal exposed but Camera
|
|
365
|
+
silently dropped.** Pre-audit, the `config` block passed to
|
|
366
|
+
`incremental.start()` omitted four fields that iOS native already
|
|
367
|
+
read: `flowMaxCorners`, `flowQualityLevel`, `flowMinDistance`,
|
|
368
|
+
`enableMaxInscribedRectCrop`. Modal sliders for these were
|
|
369
|
+
silent no-ops on every platform. Now wired. Also added
|
|
370
|
+
`captureSource` to the config so F1's Android opt-out has
|
|
371
|
+
something to read.
|
|
372
|
+
- **F5 — Android KeyframeGate now exposes the full Flow tunable
|
|
373
|
+
surface.** Pre-audit, the Android KeyframeGate facade lacked
|
|
374
|
+
Kotlin properties + JNI thunks for `setFlowMaxCorners` /
|
|
375
|
+
`setFlowQualityLevel` / `setFlowMinDistance` / `setStrategy`,
|
|
376
|
+
even though the underlying C++ gate has had them since 0.2.0.
|
|
377
|
+
Added the missing JNI bindings + Kotlin facade fields. Android
|
|
378
|
+
IncrementalStitcher now reads `flowMaxCorners`, `flowQualityLevel`,
|
|
379
|
+
`flowMinDistance`, `flowEvalEveryNFrames`, and `frameSelectionMode`
|
|
380
|
+
from configOverrides with clamp ranges matching iOS.
|
|
381
|
+
- **F6 — Camera.tsx no longer hardcodes `frameSelectionMode`.** Pre-
|
|
382
|
+
audit, line 835 hardcoded `'flow-based'`, so the modal's
|
|
383
|
+
`time-based` / `pose-based` / `flow-based` toggle had no runtime
|
|
384
|
+
effect. Now passes `settings.frameSelectionMode` through. Both
|
|
385
|
+
platforms honour the setting: `time-based` disables the gate
|
|
386
|
+
(passthrough), `pose-based` enables Pose strategy, `flow-based`
|
|
387
|
+
enables Flow strategy. Android additionally now applies the
|
|
388
|
+
eval-throttle (`flowEvalEveryNFrames`) to the AR ingest path,
|
|
389
|
+
matching iOS' `IncrementalStitcher.swift:2459-2471` behaviour.
|
|
390
|
+
- **F7 — README documented `defaultFlowMaxTranslationCm` default as
|
|
391
|
+
`8`.** Actual `DEFAULT_PANORAMA_SETTINGS.flowMaxTranslationCm` is
|
|
392
|
+
`50`; 6× off. Corrected.
|
|
393
|
+
|
|
394
|
+
### Audit ground-truth findings (doc-only)
|
|
395
|
+
|
|
396
|
+
The full audit traced every `PanoramaSettings` field through Camera.tsx,
|
|
397
|
+
the iOS bridge (`IncrementalStitcher.swift::applyConfigOverrides` and
|
|
398
|
+
the cv::Stitcher path), the Android bridge
|
|
399
|
+
(`IncrementalStitcher.kt::start`), the C++ gate (`cpp/keyframe_gate.cpp`),
|
|
400
|
+
and the live-engine config type (`RLISStitcherConfig`). Conclusions:
|
|
401
|
+
|
|
402
|
+
- Batch-keyframe and the live engines (hybrid + slit-scan) share
|
|
403
|
+
**zero settings**. All RLISStitcherConfig fields (NCC, plane
|
|
404
|
+
projection, paint mode, slit-scan painting) flow only through
|
|
405
|
+
Layer 2 entry points (`incremental.start({ engine: 'slitscan-…' })`),
|
|
406
|
+
never through `<Camera>` (which hardcodes `engine: 'batch-keyframe'`).
|
|
407
|
+
- ~10 fields in `PanoramaSettings` are confirmed dead (no native
|
|
408
|
+
consumer at all): `useARPreview`, `incrementalEngine`,
|
|
409
|
+
`slitWidthFraction`, `acceptGate`, `maxRecordingMs`,
|
|
410
|
+
`framesPerSecond`, `minFrames`, `maxFrames`, `quality`, and the
|
|
411
|
+
legacy `useDetectedPlane` alias. These are scheduled for removal
|
|
412
|
+
in v0.4.0 as part of the engine-discriminated typed-settings
|
|
413
|
+
rewrite.
|
|
414
|
+
|
|
415
|
+
## [0.3.0-pre-audit] — 2026-05-21
|
|
416
|
+
|
|
417
|
+
> [!IMPORTANT]
|
|
418
|
+
> **Behaviour change on Android AR mode and on both platforms' non-AR
|
|
419
|
+
> mode.** Keyframe selection now actually runs the **Flow strategy**
|
|
420
|
+
> (sparse optical-flow novelty) on these paths, where pre-0.3 the
|
|
421
|
+
> C++ KeyframeGate silently fell back to the Pose strategy
|
|
422
|
+
> (angular-delta) because no pixel data was supplied. Hosts that
|
|
423
|
+
> tuned `keyframeOverlapThreshold` on these paths were tuning a
|
|
424
|
+
> different algorithm than is now active — see the migration note
|
|
425
|
+
> below before re-validating capture quality. iOS AR mode is
|
|
426
|
+
> unchanged (already ran Flow with pixel data via the AR delegate).
|
|
427
|
+
|
|
428
|
+
### Fixed
|
|
429
|
+
|
|
430
|
+
- **[#9](https://github.com/bhargavkanda/react-native-image-stitcher/issues/9): Android AR mode — first keyframe thumbnail no longer delayed
|
|
431
|
+
several hundred milliseconds.** Pre-0.3 the AR ingest pipeline
|
|
432
|
+
encoded every ARCore frame to JPEG and wrote it to disk on the
|
|
433
|
+
GL render thread (~25 ms per frame at ~60 Hz) regardless of
|
|
434
|
+
whether the gate would accept it. Then the gate ran a pose-only
|
|
435
|
+
evaluation (no pixel data) which silently fell back to the
|
|
436
|
+
stricter Pose strategy, masking the result by force-accepting via
|
|
437
|
+
the IMU translation gate. Net effect: noticeable lag before
|
|
438
|
+
frame 1 thumbnail rendered, and frame 1 / frame 2 spacing
|
|
439
|
+
visually too large.
|
|
440
|
+
- v0.3 rewires the AR ingest path to extract just the **Y plane
|
|
441
|
+
bytes** from the ARCore camera image (zero-copy via
|
|
442
|
+
DirectByteBuffer → JVM byte[] + JNI `GetPrimitiveArrayCritical`)
|
|
443
|
+
and feeds them directly to the C++ gate's existing
|
|
444
|
+
`evaluateWithFrame` overload. Per-frame cost on the GL render
|
|
445
|
+
thread drops from ~25-40 ms to ~2-5 ms for rejected frames.
|
|
446
|
+
- JPEG encode + disk write is **deferred to only accepted frames**
|
|
447
|
+
(typically 3-6 per capture) via an `onAccept` lambda the gate
|
|
448
|
+
invokes if-and-only-if it keeps the frame. Single disk write
|
|
449
|
+
per accepted keyframe (pre-0.3 was: encode-then-copy = two
|
|
450
|
+
writes).
|
|
451
|
+
- Gate now runs Flow strategy with real pixel content — feature-
|
|
452
|
+
tracking-based novelty, not the strict angular-delta proxy.
|
|
453
|
+
- **iOS non-AR + Android non-AR Flow strategy regression** —
|
|
454
|
+
related to #9 but not user-reported. Both non-AR paths previously
|
|
455
|
+
called `evaluate(pose, plane: nil)` with no pixel data, which
|
|
456
|
+
silently fell back to Pose strategy on both platforms. v0.3
|
|
457
|
+
decodes the JPEG snapshot to grayscale before the gate call so
|
|
458
|
+
Flow strategy runs:
|
|
459
|
+
- iOS: `CGImageSource → CGContext` into a single-channel
|
|
460
|
+
`CVPixelBuffer` (`kCVPixelFormatType_OneComponent8`). The
|
|
461
|
+
`KeyframeGateBridge.mm` got OneComponent8 case-handling
|
|
462
|
+
(parallel to the existing NV12 / BGRA cases). ~10-20 ms per
|
|
463
|
+
snapshot on iPhone 13/16 Pro.
|
|
464
|
+
- Android: `Imgcodecs.imread(path, IMREAD_GRAYSCALE)` decodes
|
|
465
|
+
the JPEG straight to a CV_8UC1 Mat which we marshal into a
|
|
466
|
+
ByteArray for the new `nativeEvaluateWithFrame` JNI thunk.
|
|
467
|
+
~10-20 ms per snapshot on Galaxy A35.
|
|
468
|
+
|
|
469
|
+
### Added
|
|
470
|
+
|
|
471
|
+
- **`KeyframeGate.evaluateWithFrame(pose, plane, grayData, w, h, stride)`**
|
|
472
|
+
(Kotlin) — pixel-aware Flow-strategy gate-evaluate entry point,
|
|
473
|
+
parity with the existing iOS `KeyframeGateBridge.evaluatePixelBuffer:…`.
|
|
474
|
+
- **`nativeEvaluateWithFrame`** JNI thunk in `keyframe_gate_jni.cpp`.
|
|
475
|
+
Uses `GetPrimitiveArrayCritical` for zero-copy access to the
|
|
476
|
+
JVM-side byte[] during the gate evaluate.
|
|
477
|
+
- **`kCVPixelFormatType_OneComponent8` handling** in iOS
|
|
478
|
+
`KeyframeGateBridge.mm` — base address is read directly as the
|
|
479
|
+
Y plane with no conversion cost.
|
|
480
|
+
|
|
481
|
+
### Changed
|
|
482
|
+
|
|
483
|
+
- **`IncrementalStitcher.ingestFromARCameraView` signature** (Android,
|
|
484
|
+
internal):
|
|
485
|
+
- **Removed**: `path: String` parameter. AR camera view no longer
|
|
486
|
+
encodes a JPEG to feed this method — it hands over Y-plane bytes
|
|
487
|
+
instead.
|
|
488
|
+
- **Added**: `grayData: ByteArray, grayWidth: Int, grayHeight: Int,
|
|
489
|
+
grayStride: Int, onAccept: (targetPath: String) -> Boolean`.
|
|
490
|
+
The lambda is invoked only on gate-accept and is expected to
|
|
491
|
+
write a JPEG of the current camera image to the supplied target
|
|
492
|
+
path. Returns true on success.
|
|
493
|
+
- `RNSARCameraView.forwardToIncremental` updated accordingly.
|
|
494
|
+
- **`RNSARCameraView.postFrameToEngine` removed.** The thin wrapper
|
|
495
|
+
was only used to wrap the old positional call to
|
|
496
|
+
`ingestFromARCameraView`; the new lambda-based call shape is
|
|
497
|
+
inline in `forwardToIncremental`.
|
|
498
|
+
|
|
499
|
+
### Migration from 0.2.x
|
|
500
|
+
|
|
501
|
+
**Most consumers**: no code change required. The public JS API
|
|
502
|
+
(`<Camera>`, `useCapture`, `useIMUTranslationGate`,
|
|
503
|
+
`useDeviceOrientation`, everything) is byte-identical to 0.2.1.
|
|
504
|
+
|
|
505
|
+
**Hosts that tuned `keyframeOverlapThreshold` against Android AR or
|
|
506
|
+
either non-AR path**: the threshold now controls **Flow novelty
|
|
507
|
+
percentile** instead of **Pose angular delta**. Same setting, very
|
|
508
|
+
different metric — re-tune against your typical captures. The
|
|
509
|
+
default (`0.20`) was chosen to roughly match the pre-0.3 visible
|
|
510
|
+
behaviour; most hosts shouldn't need to change anything, but
|
|
511
|
+
quality-sensitive hosts should re-validate before shipping.
|
|
512
|
+
|
|
513
|
+
**Hosts that observed the Android-AR first-frame delay**: the bug
|
|
514
|
+
is fixed — first thumbnail should render within ~50 ms of shutter
|
|
515
|
+
hold (was ~200+ ms).
|
|
516
|
+
|
|
517
|
+
### Deferred to v0.4 ([#11](https://github.com/bhargavkanda/react-native-image-stitcher/issues/11))
|
|
518
|
+
|
|
519
|
+
Non-AR capture currently still goes through vision-camera's
|
|
520
|
+
`takeSnapshot()` API at ~4 FPS with a per-snapshot JPEG-encode +
|
|
521
|
+
disk-write + decode-to-grayscale round-trip. v0.4 will migrate
|
|
522
|
+
non-AR to vision-camera's Frame Processor API: raw pixel data
|
|
523
|
+
direct from the camera, no JPEG, no disk, full camera frame rate.
|
|
524
|
+
At that point the JPEG-decode-to-grayscale workaround added in
|
|
525
|
+
v0.3's iOS/Android non-AR paths becomes redundant and will be
|
|
526
|
+
removed. See issue #11 for the full scope.
|
|
527
|
+
|
|
19
528
|
## [0.2.1] — 2026-05-21
|
|
20
529
|
|
|
21
530
|
### Changed
|
|
@@ -357,7 +866,8 @@ Native module names also changed:
|
|
|
357
866
|
- iOS pod: `RetaiLensCaptureSDK` → `RNImageStitcher`
|
|
358
867
|
- iOS xcframework: shipped as `opencv2.xcframework` (linked from `RNImageStitcher.podspec`)
|
|
359
868
|
|
|
360
|
-
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.
|
|
869
|
+
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.3.0...HEAD
|
|
870
|
+
[0.3.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.1...v0.3.0
|
|
361
871
|
[0.2.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.0...v0.2.1
|
|
362
872
|
[0.2.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.3...v0.2.0
|
|
363
873
|
[0.1.3]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.2...v0.1.3
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ See `src/camera/Camera.tsx` for the full TSDoc. Highlights:
|
|
|
118
118
|
| `defaultWarper` | `'plane'` | `'plane'`, `'cylindrical'`, `'spherical'` |
|
|
119
119
|
| `defaultFlowNoveltyPercentile` | `0.85` | Range 0.50 – 0.99 |
|
|
120
120
|
| `defaultFlowEvalEveryNFrames` | `5` | Range 1 – 10 |
|
|
121
|
-
| `defaultFlowMaxTranslationCm` | `
|
|
121
|
+
| `defaultFlowMaxTranslationCm` | `50` | 0 = disabled |
|
|
122
122
|
| `defaultKeyframeMaxCount` | `6` | Range 3 – 10 |
|
|
123
123
|
| `defaultKeyframeOverlapThreshold` | `0.20` | Range 0.20 – 0.60 |
|
|
124
124
|
|