react-native-image-stitcher 0.5.1 → 0.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/CHANGELOG.md +199 -1
- package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +2 -2
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +2 -30
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +90 -368
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +6 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +17 -30
- package/dist/camera/Camera.d.ts +11 -27
- package/dist/camera/Camera.js +46 -78
- package/dist/index.d.ts +2 -3
- package/dist/index.js +10 -6
- package/dist/stitching/incremental.d.ts +79 -11
- package/dist/stitching/useFrameProcessorDriver.d.ts +7 -6
- package/dist/stitching/useFrameProcessorDriver.js +12 -11
- package/dist/stitching/useKeyframeStream.d.ts +69 -0
- package/dist/stitching/useKeyframeStream.js +120 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +48 -208
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +0 -8
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +6 -126
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +6 -6
- package/package.json +1 -1
- package/src/camera/Camera.tsx +57 -106
- package/src/index.ts +9 -9
- package/src/stitching/incremental.ts +84 -11
- package/src/stitching/useFrameProcessorDriver.ts +12 -11
- package/src/stitching/useKeyframeStream.ts +127 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +0 -74
- package/dist/stitching/useIncrementalJSDriver.js +0 -220
- package/src/stitching/useIncrementalJSDriver.ts +0 -297
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,198 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.7.0] — 2026-05-26
|
|
20
|
+
|
|
21
|
+
### Added — Tier 1: `useKeyframeStream`
|
|
22
|
+
|
|
23
|
+
JS-thread subscription hook for **accepted-keyframe events** — the
|
|
24
|
+
small subset of camera frames the stitching engine actually chose to
|
|
25
|
+
include in the panorama. Foundation for plugin-pattern host features:
|
|
26
|
+
OCR on each saved keyframe, packet detection, server-side analysis,
|
|
27
|
+
analytics, etc.
|
|
28
|
+
|
|
29
|
+
Fires 4-6 times per panorama (once per accepted keyframe), NOT per
|
|
30
|
+
camera frame — the lowest-frequency, highest-value frame stream.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { useKeyframeStream, type AcceptedKeyframe } from 'react-native-image-stitcher';
|
|
34
|
+
|
|
35
|
+
function OcrPlugin() {
|
|
36
|
+
useKeyframeStream(useCallback(async (kf: AcceptedKeyframe) => {
|
|
37
|
+
const text = await runOCR(kf.jpegPath);
|
|
38
|
+
console.log(`Keyframe ${kf.index} pose=${kf.pose.rotation}:`, text);
|
|
39
|
+
}, []));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **`useKeyframeStream(handler)`** exported from
|
|
45
|
+
`react-native-image-stitcher`. Subscribes to the existing
|
|
46
|
+
`IncrementalStateUpdate` event channel; surfaces accepted-keyframe
|
|
47
|
+
events through a typed callback. Re-subscribes on handler-identity
|
|
48
|
+
changes; async handler rejections are surfaced via `console.error`
|
|
49
|
+
rather than swallowed.
|
|
50
|
+
- **`AcceptedKeyframe` type** exported. Fields: `jpegPath` (absolute
|
|
51
|
+
path, no `file://` prefix); `pose` (rotation quaternion + optional
|
|
52
|
+
translation vector); `timestamp` (ms since epoch); `index`
|
|
53
|
+
(zero-based position in current panorama).
|
|
54
|
+
- **`IncrementalState.batchKeyframePose?`** + **`batchKeyframeAcceptedAtMs?`**
|
|
55
|
+
new optional fields. Populated by the native emit alongside the
|
|
56
|
+
existing `batchKeyframeThumbnailPath` + `batchKeyframeIndex` on
|
|
57
|
+
accept events. Direct readers of `IncrementalState` can consume
|
|
58
|
+
these without going through the new hook.
|
|
59
|
+
|
|
60
|
+
### Changed (internal — externally invisible)
|
|
61
|
+
|
|
62
|
+
- **Native `emitBatchKeyframeAcceptedState` populates pose + timestamp.**
|
|
63
|
+
Both `IncrementalStitcher.swift::emitBatchKeyframeAcceptedState` and
|
|
64
|
+
`IncrementalStitcher.kt::emitBatchKeyframeAcceptedState` grew
|
|
65
|
+
parameters for the pose snapshot (quaternion + translation) and
|
|
66
|
+
accept-time wall-clock millis. The existing call sites in the
|
|
67
|
+
batch-keyframe accept path thread the pose they already have in
|
|
68
|
+
scope.
|
|
69
|
+
|
|
70
|
+
### Engine-mode caveat
|
|
71
|
+
|
|
72
|
+
`useKeyframeStream` only fires under the `batch-keyframe` engine (the
|
|
73
|
+
`<Camera>` component's default). Live engines (`firstwins-rectilinear`,
|
|
74
|
+
`hybrid`, `slitscan-*`) paint into a live canvas instead of saving
|
|
75
|
+
per-accept JPEGs and do not surface accept events through this channel
|
|
76
|
+
— the hook silently does not fire in those modes. Live-engine accept
|
|
77
|
+
emit may land as a v0.7.1 follow-up if a real consumer needs it.
|
|
78
|
+
|
|
79
|
+
### Translation semantics
|
|
80
|
+
|
|
81
|
+
`AcceptedKeyframe.pose.translation` is always populated by the native
|
|
82
|
+
emit. In AR mode it carries the real ARKit / ARCore camera transform
|
|
83
|
+
in metres (world coords). In non-AR (Frame Processor) mode the
|
|
84
|
+
translation reads as `[0, 0, 0]` because gyroscope provides only
|
|
85
|
+
rotation (no spatial anchor). Hosts that need to distinguish can
|
|
86
|
+
either check the active `frameSourceMode` or threshold the translation
|
|
87
|
+
magnitude.
|
|
88
|
+
|
|
89
|
+
### Compatibility
|
|
90
|
+
|
|
91
|
+
Strict additive over v0.6.0. No host changes required. Existing
|
|
92
|
+
`subscribeIncrementalState` consumers see new optional fields but
|
|
93
|
+
their existing reads are unaffected.
|
|
94
|
+
|
|
95
|
+
### Verification
|
|
96
|
+
|
|
97
|
+
- iPhone 17 Pro (real device, iOS 26.5): hold-and-release AR-mode
|
|
98
|
+
panorama produced four accepted-keyframe events with real pose
|
|
99
|
+
data (unit quaternion + non-zero translation in metres matching
|
|
100
|
+
the physical pan).
|
|
101
|
+
- Android (Galaxy A35): `compileDebugKotlin` BUILD SUCCESSFUL;
|
|
102
|
+
on-device runtime verification deferred for this release (the
|
|
103
|
+
Kotlin emit mirrors the iOS emit at the byte-for-byte payload
|
|
104
|
+
level — same field names, same types, same call-site pattern).
|
|
105
|
+
|
|
106
|
+
## [0.6.0] — 2026-05-25
|
|
107
|
+
|
|
108
|
+
> [!WARNING]
|
|
109
|
+
> **Breaking changes.** v0.6.0 retires the deprecated JS-driver
|
|
110
|
+
> non-AR path that was marked for removal in v0.5.0's *Deprecated*
|
|
111
|
+
> section. Hosts using the default `<Camera>` flow (`legacyDriver`
|
|
112
|
+
> unset) are not affected — they were already on
|
|
113
|
+
> `useFrameProcessorDriver`. Hosts that opted into the legacy
|
|
114
|
+
> driver (`legacyDriver={true}` on `<Camera>`, or a direct
|
|
115
|
+
> `useIncrementalJSDriver()` consumer) MUST migrate to the Frame
|
|
116
|
+
> Processor driver — see *Migration from 0.5.x* below.
|
|
117
|
+
|
|
118
|
+
### Removed (breaking)
|
|
119
|
+
|
|
120
|
+
- **`useIncrementalJSDriver` hook** + its `UseIncrementalJSDriverOptions`
|
|
121
|
+
/ `IncrementalJSDriverHandle` types. Deprecated in v0.5.0; the
|
|
122
|
+
v0.5 deprecation warning has now been replaced by deletion.
|
|
123
|
+
- **`legacyDriver?: boolean` prop on `<Camera>`**. The escape hatch
|
|
124
|
+
back to the JS driver is gone. Hosts that set this prop will
|
|
125
|
+
get a TS-level error; at runtime the prop is silently ignored.
|
|
126
|
+
- **`frameSourceMode: 'jsDriver'`** enum value in
|
|
127
|
+
`IncrementalStartOptions`. The TS type is now narrowed to
|
|
128
|
+
`'arSession' | 'frameProcessor'`. Passing `'jsDriver'` is a
|
|
129
|
+
compile error; at the native bridge layer the value falls through
|
|
130
|
+
to the default (now `'arSession'`).
|
|
131
|
+
- **`IncrementalStitcher.processFrameAtPath` native method** on both
|
|
132
|
+
iOS and Android. The only JS caller was `useIncrementalJSDriver`,
|
|
133
|
+
also deleted. Hosts calling
|
|
134
|
+
`NativeModules.IncrementalStitcher.processFrameAtPath(...)` via
|
|
135
|
+
raw `NativeModules` access will get a runtime "method does not
|
|
136
|
+
exist" error. Use the Frame Processor driver instead.
|
|
137
|
+
|
|
138
|
+
### Changed (breaking)
|
|
139
|
+
|
|
140
|
+
- **Android `frameSourceMode` default switched from `"jsDriver"` to
|
|
141
|
+
`"arSession"`** for parity with iOS. Raw `NativeModules` callers
|
|
142
|
+
that omitted `frameSourceMode` were previously getting an inert
|
|
143
|
+
capture (the "jsDriver" branch dropped all engine input on
|
|
144
|
+
Android since v0.5.0); they now get AR-mode behaviour, matching
|
|
145
|
+
iOS. The production `<Camera>` is unaffected — it always passes
|
|
146
|
+
`frameSourceMode: 'arSession'` explicitly for AR captures.
|
|
147
|
+
|
|
148
|
+
### Changed (non-breaking)
|
|
149
|
+
|
|
150
|
+
- **`RNSARCameraView` (AR mode) no longer eager-encodes a JPEG per
|
|
151
|
+
ARCore frame.** Migrated to the pixel-data path introduced for
|
|
152
|
+
the Frame Processor in v0.5.1's F8.6 work. AR-mode captures now
|
|
153
|
+
pass `nv21PixelData` / `nv21PixelWidth` / `nv21PixelHeight`
|
|
154
|
+
through `ingestFromARCameraView`; `legacyJpegPath` is always null
|
|
155
|
+
on this path. Expected gain on Galaxy A35: ~30-50 ms per
|
|
156
|
+
accepted frame, with the dominant savings on rejected frames
|
|
157
|
+
(no JPEG encode → no imread round-trip). Closes the v0.5.0
|
|
158
|
+
follow-up.
|
|
159
|
+
|
|
160
|
+
### Removed (internal cleanup; no external API impact)
|
|
161
|
+
|
|
162
|
+
- **F8.6 perf-diagnostic logs** (`F8.6-route`, `F8.6-perf`)
|
|
163
|
+
introduced in v0.5.1 stripped from `IncrementalStitcher` +
|
|
164
|
+
`IncrementalFirstwinsEngine` — F8.6 is now baked in for
|
|
165
|
+
production and the diagnostic spam is no longer informative.
|
|
166
|
+
- **Orphaned native helpers** dropped after `processFrameAtPath`
|
|
167
|
+
removal:
|
|
168
|
+
- iOS: `addBatchKeyframePath(path:pose:)`, `isBatchKeyframeMode`
|
|
169
|
+
getter, `decodeJpegToGrayscalePixelBuffer` (only callers were
|
|
170
|
+
`processFrameAtPath`).
|
|
171
|
+
- Android: `decodeJpegToGrayscale` + `GrayscaleFrame` data class,
|
|
172
|
+
`isBatchKeyframeMode` getter (only callers were
|
|
173
|
+
`processFrameAtPath` and the AR-mode eager-encode branch).
|
|
174
|
+
- **Stale comments** referencing removed code paths swept across
|
|
175
|
+
Kotlin/Swift/Obj-C/TS. Historical "removed in v0.6" markers
|
|
176
|
+
retained; comments that described live code in terms of the
|
|
177
|
+
removed names rewritten to describe current behaviour.
|
|
178
|
+
|
|
179
|
+
### Migration from 0.5.x
|
|
180
|
+
|
|
181
|
+
**Default `<Camera>` hosts (no `legacyDriver` prop set):** no
|
|
182
|
+
action required. `<Camera>` already used `useFrameProcessorDriver`
|
|
183
|
+
in non-AR mode and `RNSARSession` in AR mode since v0.5.0.
|
|
184
|
+
|
|
185
|
+
**Hosts with `legacyDriver={true}` on `<Camera>`:** remove the
|
|
186
|
+
prop. `<Camera>` will use the Frame Processor driver, which has
|
|
187
|
+
been the default since v0.5.0 and the only path since this release.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// Before (v0.5.x)
|
|
191
|
+
<Camera legacyDriver={true} ... />
|
|
192
|
+
|
|
193
|
+
// After (v0.6.0)
|
|
194
|
+
<Camera ... />
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Hosts directly using `useIncrementalJSDriver`:** migrate to
|
|
198
|
+
`useFrameProcessorDriver`. The handle shape (`{ start, stop,
|
|
199
|
+
frameProcessor, isRunning }`) is preserved, but the new hook is a
|
|
200
|
+
Frame Processor + gyro driver instead of a `takeSnapshot` + JS
|
|
201
|
+
interval driver. See
|
|
202
|
+
[`src/stitching/useFrameProcessorDriver.ts`](src/stitching/useFrameProcessorDriver.ts)
|
|
203
|
+
for the migration mapping; the gyro pose synthesis convention
|
|
204
|
+
(`q = q_yaw * q_pitch * q_roll`) is identical, so existing pose
|
|
205
|
+
math at call sites continues to work.
|
|
206
|
+
|
|
207
|
+
**Hosts passing `frameSourceMode: 'jsDriver'` to
|
|
208
|
+
`incremental.start(...)`:** change to `'frameProcessor'`. The
|
|
209
|
+
TypeScript type now rejects `'jsDriver'` at compile time.
|
|
210
|
+
|
|
19
211
|
## [0.5.1] — 2026-05-25
|
|
20
212
|
|
|
21
213
|
### Added — F8.6 Android pixel-buffer engine parity
|
|
@@ -1056,7 +1248,13 @@ Native module names also changed:
|
|
|
1056
1248
|
- iOS pod: `RetaiLensCaptureSDK` → `RNImageStitcher`
|
|
1057
1249
|
- iOS xcframework: shipped as `opencv2.xcframework` (linked from `RNImageStitcher.podspec`)
|
|
1058
1250
|
|
|
1059
|
-
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.
|
|
1251
|
+
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.7.0...HEAD
|
|
1252
|
+
[0.7.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.6.0...v0.7.0
|
|
1253
|
+
[0.6.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.1...v0.6.0
|
|
1254
|
+
[0.5.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.5.0...v0.5.1
|
|
1255
|
+
[0.5.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.4.1...v0.5.0
|
|
1256
|
+
[0.4.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.4.0...v0.4.1
|
|
1257
|
+
[0.4.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.3.0...v0.4.0
|
|
1060
1258
|
[0.3.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.1...v0.3.0
|
|
1061
1259
|
[0.2.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.0...v0.2.1
|
|
1062
1260
|
[0.2.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.3...v0.2.0
|
|
@@ -29,8 +29,8 @@ import com.mrousavy.camera.frameprocessors.VisionCameraProxy
|
|
|
29
29
|
* 3. Call `IncrementalStitcher.consumeFrameFromPlugin(image, …)`
|
|
30
30
|
* which:
|
|
31
31
|
* - Drops the call if `frameSourceMode != "frameProcessor"`
|
|
32
|
-
* (prevents double-feeding the engine alongside the
|
|
33
|
-
* `
|
|
32
|
+
* (prevents double-feeding the engine alongside the
|
|
33
|
+
* AR-mode `ingestFromARCameraView` path).
|
|
34
34
|
* - Otherwise: extracts the Y plane, evaluates the keyframe
|
|
35
35
|
* gate via `KeyframeGate.evaluateWithFrame`, encodes the
|
|
36
36
|
* accepted frame to JPEG synchronously, and hands the path
|
|
@@ -197,7 +197,7 @@ internal class IncrementalFirstwinsEngine(
|
|
|
197
197
|
}
|
|
198
198
|
val frameBGR = downsampleToCompose(srcRaw)
|
|
199
199
|
if (frameBGR !== srcRaw) srcRaw.release()
|
|
200
|
-
|
|
200
|
+
return addFrameMat(
|
|
201
201
|
frameBGR,
|
|
202
202
|
qx, qy, qz, qw,
|
|
203
203
|
fx, fy, cx, cy,
|
|
@@ -206,8 +206,6 @@ internal class IncrementalFirstwinsEngine(
|
|
|
206
206
|
fovHorizDegrees, fovVertDegrees,
|
|
207
207
|
t0,
|
|
208
208
|
)
|
|
209
|
-
f8_6_logPerf("firstwins/jpeg", t0, tele.outcome)
|
|
210
|
-
return tele
|
|
211
209
|
}
|
|
212
210
|
|
|
213
211
|
/**
|
|
@@ -283,7 +281,7 @@ internal class IncrementalFirstwinsEngine(
|
|
|
283
281
|
}
|
|
284
282
|
val frameBGR = downsampleToCompose(srcRaw)
|
|
285
283
|
if (frameBGR !== srcRaw) srcRaw.release()
|
|
286
|
-
|
|
284
|
+
return addFrameMat(
|
|
287
285
|
frameBGR,
|
|
288
286
|
qx, qy, qz, qw,
|
|
289
287
|
fx, fy, cx, cy,
|
|
@@ -292,32 +290,6 @@ internal class IncrementalFirstwinsEngine(
|
|
|
292
290
|
fovHorizDegrees, fovVertDegrees,
|
|
293
291
|
t0,
|
|
294
292
|
)
|
|
295
|
-
f8_6_logPerf("firstwins/pixel", t0, tele.outcome)
|
|
296
|
-
return tele
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* F8.6 perf-diagnostic counter. Logs ingest timing every Nth
|
|
301
|
-
* call so a single capture session yields enough samples to
|
|
302
|
-
* eyeball the JPEG-path vs pixel-data-path delta. Remove this
|
|
303
|
-
* once F8.6 is verified in production.
|
|
304
|
-
*/
|
|
305
|
-
@Volatile private var f8_6_perfCallCounter: Long = 0L
|
|
306
|
-
private fun f8_6_logPerf(
|
|
307
|
-
path: String,
|
|
308
|
-
t0Nanos: Long,
|
|
309
|
-
outcome: FrameOutcome,
|
|
310
|
-
) {
|
|
311
|
-
val n = ++f8_6_perfCallCounter
|
|
312
|
-
// Every 5th call ≈ ~1 line/sec at 5 Hz live-engine rate;
|
|
313
|
-
// first call always logs so we see something on capture
|
|
314
|
-
// start without waiting.
|
|
315
|
-
if (n == 1L || n % 5L == 0L) {
|
|
316
|
-
Log.i(
|
|
317
|
-
"F8.6-perf",
|
|
318
|
-
"$path took ${msSince(t0Nanos)}ms outcome=$outcome (call #$n)",
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
293
|
}
|
|
322
294
|
|
|
323
295
|
/**
|