react-native-image-stitcher 0.14.1 → 0.15.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 (119) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -1
  4. package/android/build.gradle +0 -16
  5. package/android/src/main/cpp/CMakeLists.txt +2 -63
  6. package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
  7. package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
  8. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
  9. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
  10. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
  11. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
  12. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +13 -64
  13. package/cpp/keyframe_gate.cpp +82 -23
  14. package/cpp/keyframe_gate.hpp +31 -2
  15. package/cpp/stitcher.cpp +208 -28
  16. package/cpp/tests/CMakeLists.txt +18 -12
  17. package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
  18. package/cpp/tests/warp_guard_test.cpp +48 -0
  19. package/cpp/warp_guard.hpp +41 -0
  20. package/dist/ar/useARSession.d.ts +9 -0
  21. package/dist/ar/useARSession.js +24 -2
  22. package/dist/camera/Camera.d.ts +31 -16
  23. package/dist/camera/Camera.js +27 -4
  24. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  25. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  26. package/dist/camera/PanoramaSettings.d.ts +10 -223
  27. package/dist/camera/PanoramaSettings.js +6 -28
  28. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  29. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  30. package/dist/camera/PanoramaSettingsModal.js +7 -1
  31. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  32. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  33. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  34. package/dist/camera/cameraErrorMessages.js +53 -0
  35. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  36. package/dist/camera/selectCaptureDevice.js +22 -2
  37. package/dist/camera/useCapture.js +38 -0
  38. package/dist/index.d.ts +5 -8
  39. package/dist/index.js +11 -34
  40. package/dist/stitching/incremental.d.ts +1 -117
  41. package/dist/stitching/stitchVideo.d.ts +0 -35
  42. package/dist/types.d.ts +0 -87
  43. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  44. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  45. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  46. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  47. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  48. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  49. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  50. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  51. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  52. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  53. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  54. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  55. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  56. package/package.json +3 -2
  57. package/src/ar/useARSession.ts +35 -5
  58. package/src/camera/Camera.tsx +63 -24
  59. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  60. package/src/camera/PanoramaSettings.ts +16 -289
  61. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  62. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  63. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  64. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  65. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  66. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  67. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  68. package/src/camera/cameraErrorMessages.ts +84 -0
  69. package/src/camera/selectCaptureDevice.ts +28 -3
  70. package/src/camera/useCapture.ts +44 -1
  71. package/src/index.ts +11 -40
  72. package/src/stitching/incremental.ts +3 -140
  73. package/src/stitching/stitchVideo.ts +0 -26
  74. package/src/types.ts +0 -95
  75. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  76. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  77. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  78. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  79. package/cpp/stitcher_frame_jsi.cpp +0 -214
  80. package/cpp/stitcher_frame_jsi.hpp +0 -108
  81. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  82. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  83. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  84. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  85. package/cpp/stitcher_worklet_registry.cpp +0 -91
  86. package/cpp/stitcher_worklet_registry.hpp +0 -146
  87. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  88. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  89. package/dist/stitching/IncrementalStitcherView.js +0 -157
  90. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  91. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  92. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  93. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  94. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  95. package/dist/stitching/useFrameProcessor.js +0 -196
  96. package/dist/stitching/useFrameStream.d.ts +0 -34
  97. package/dist/stitching/useFrameStream.js +0 -234
  98. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  99. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  101. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  102. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  104. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  105. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  106. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  107. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  108. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  109. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  110. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  111. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  112. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  113. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  114. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  115. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  116. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  117. package/src/stitching/useFrameProcessor.ts +0 -226
  118. package/src/stitching/useFrameStream.ts +0 -271
  119. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
@@ -145,19 +145,16 @@ public final class IncrementalStitcherBridge: RCTEventEmitter {
145
145
  captureOrientation,
146
146
  Int32(rotation),
147
147
  String(describing: options["captureOrientation"]))
148
- // V15 — engine selection. Three modes:
149
- // 'hybrid' — planar projection + feature matching
150
- // 'slitscan-rotate' — V13.0a + 1D NCC for rotation wobble
151
- // 'slitscan-both' DEFAULT V13.0a + no gate + feather
152
- // blend; iterate via per-stage toggles
153
- // in the config dict.
154
- // Backward compat: 'firstwins-rectilinear' 'slitscan-rotate'.
155
- // Legacy 'firstwins' / 'firstwins-zoomed' / 'slitscan' fall
156
- // back to 'slitscan-both' with a deprecation warning.
157
- let engineMode = (options["engine"] as? String) ?? "slitscan-both"
148
+ // Engine selection. The live incremental engines (hybrid,
149
+ // slitscan-*, and the legacy firstwins* aliases) were archived
150
+ // in the 2026-06 batch-keyframe cleanup the SDK now ships
151
+ // only 'batch-keyframe'. Any other value is still accepted for
152
+ // backward compatibility but falls back to batch-keyframe with
153
+ // a deprecation log inside IncrementalStitcher.start().
154
+ let engineMode = (options["engine"] as? String) ?? "batch-keyframe"
158
155
 
159
- // V15 — per-stage config overrides. All optional; missing
160
- // fields use mode defaults from +[RLISStitcherConfig configForMode:].
156
+ // Per-stage config overrides. All optional; keys not consumed
157
+ // by the batch-keyframe pipeline are ignored.
161
158
  let configOverrides = options["config"] as? [String: Any] ?? [:]
162
159
 
163
160
  IncrementalStitcher.shared.start(
@@ -145,6 +145,20 @@ final class KeyframeGate {
145
145
  }
146
146
  }
147
147
 
148
+ /// Wall-clock keyframe-interval budget, in MILLISECONDS. When > 0,
149
+ /// the gate force-accepts a frame once the elapsed time since the
150
+ /// last accepted keyframe exceeds this value — even when novelty <
151
+ /// overlapThreshold. Unlike `flowMaxTranslationCm` this applies to
152
+ /// BOTH the Pose and Flow strategies, and is passed STRAIGHT
153
+ /// THROUGH to the bridge (the unit is already what C++ expects — no
154
+ /// cm→m style conversion). Default 2000 ms; 0 = disabled. The C++
155
+ /// setter clamps to ≥ 0.
156
+ var maxKeyframeIntervalMs: Double = 2000.0 {
157
+ didSet {
158
+ bridge.setMaxKeyframeIntervalMs(maxKeyframeIntervalMs)
159
+ }
160
+ }
161
+
148
162
  /// V16 — percentile used to aggregate the tracked features'
149
163
  /// absolute displacements into a per-axis novelty estimate.
150
164
  /// Default 0.85. Pre-V16 used median (0.50); the higher
@@ -72,6 +72,13 @@ NS_SWIFT_NAME(KeyframeGateBridge)
72
72
  /// translation overflow even when novelty < threshold; 0 disables.
73
73
  /// See KeyframeGate.swift for the operator-facing description.
74
74
  - (void)setFlowMaxTranslationM:(double)metres;
75
+ /// Wall-clock keyframe-interval budget (milliseconds). Set > 0 to
76
+ /// force-accept a frame when the elapsed time since the last accepted
77
+ /// keyframe exceeds this value (applies to BOTH Pose and Flow
78
+ /// strategies); 0 disables. Passed straight through (no unit
79
+ /// conversion). See KeyframeGate.swift for the operator-facing
80
+ /// description.
81
+ - (void)setMaxKeyframeIntervalMs:(double)ms;
75
82
  /// V16 — novelty aggregation percentile [0.5, 0.99]. Default 0.85.
76
83
  /// See KeyframeGate.swift for the operator-facing description.
77
84
  - (void)setFlowNoveltyPercentile:(double)percentile;
@@ -41,6 +41,8 @@ static NSString *kReasonStringFor(retailens::KeyframeGateDecisionReason r) {
41
41
  case R::RejectOverlapTooHighFlow: return @"overlap-too-high (flow)";
42
42
  // V16 — translation-budget force-accept
43
43
  case R::AcceptFlowTranslation: return @"ok-flow-translation";
44
+ // Wall-clock keyframe-interval force-accept (Pose + Flow)
45
+ case R::AcceptTimeInterval: return @"ok-time-interval";
44
46
  }
45
47
  return @"unknown";
46
48
  }
@@ -122,6 +124,10 @@ static NSString *kReasonStringFor(retailens::KeyframeGateDecisionReason r) {
122
124
  _gate.setFlowMaxTranslationM(metres);
123
125
  }
124
126
 
127
+ - (void)setMaxKeyframeIntervalMs:(double)ms {
128
+ _gate.setMaxKeyframeIntervalMs(ms);
129
+ }
130
+
125
131
  - (void)setFlowNoveltyPercentile:(double)percentile {
126
132
  _gate.setFlowNoveltyPercentile(percentile);
127
133
  }
@@ -3,7 +3,7 @@
3
3
  // OpenCVKeyframeCollector — V16 Phase 1 helper that accumulates the
4
4
  // AR-keyframe-gate's accepted CVPixelBuffers as on-disk JPEGs while
5
5
  // the user pans, then hands the path list off to OpenCVStitcher's
6
- // `stitchKeyframePaths:withPoses:` on shutter release.
6
+ // `stitchFramePaths:` on shutter release.
7
7
  //
8
8
  // Why a separate class:
9
9
  // - CVPixelBuffer → cv::Mat → cv::imwrite has to live in ObjC++ /
@@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
25
25
 
26
26
  /// Each saved keyframe ends up with a JPEG path + the index it was
27
27
  /// saved at + the on-disk size. Returned from `saveKeyframe:…` so
28
- /// the host can build the path/pose list for `stitchKeyframePaths:`.
28
+ /// the host can build the path/pose list for `stitchFramePaths:`.
29
29
  @interface OpenCVKeyframeRecord : NSObject
30
30
  @property (nonatomic, copy, readonly) NSString *path;
31
31
  @property (nonatomic, assign, readonly) NSInteger index;
@@ -174,7 +174,7 @@ static BOOL WriteJPEGWithEXIF(const cv::Mat &bgr,
174
174
 
175
175
  // Rotate to caller-requested orientation. The JPEGs are saved in
176
176
  // the orientation the stitcher expects (user-pan orientation), so
177
- // OpenCVStitcher.stitchKeyframePaths can read them with no further
177
+ // OpenCVStitcher.stitchFramePaths can read them with no further
178
178
  // rotation work.
179
179
  cv::Mat rotated;
180
180
  if (rotationDegrees == 90) {
@@ -236,8 +236,8 @@ static BOOL WriteJPEGWithEXIF(const cv::Mat &bgr,
236
236
 
237
237
  // ── CVPixelBuffer → cv::Mat (BGR) ──────────────────────────────────
238
238
  //
239
- // Mirrors `OpenCVIncrementalStitcher.convertPixelBuffer:toMat:` but
240
- // kept inline here so this file is self-contained. Supports the two
239
+ // Self-contained CVPixelBuffer → cv::Mat conversion (the
240
+ // OpenCVIncrementalStitcher it once mirrored is now archived). Supports the two
241
241
  // pixel formats ARFrame.capturedImage uses on iOS (NV12 by default;
242
242
  // BGRA when the AR session is configured for it). Lock-once, copy
243
243
  // out, unlock — buffer lifetime ends with the caller's accept frame.
@@ -52,7 +52,7 @@ extern NSString *const RNImageStitcherErrorDomain;
52
52
  framesIncluded:(NSInteger)framesIncluded
53
53
  finalConfidenceThresh:(double)finalConfidenceThresh NS_DESIGNATED_INITIALIZER;
54
54
  /// Convenience initializer for paths that don't carry C+D retry
55
- /// telemetry (e.g. stitchVideoAtPath / stitchKeyframePaths). Sets
55
+ /// telemetry (e.g. stitchVideoAtPath / stitchFramePaths). Sets
56
56
  /// the telemetry fields to sentinel values (-1) so JS callers can
57
57
  /// detect "no retry data available" cleanly.
58
58
  - (instancetype)initWithOutputPath:(NSString *)outputPath
@@ -152,65 +152,6 @@ extern NSString *const RNImageStitcherErrorDomain;
152
152
  seamFinderType:(nullable NSString *)seamFinderType
153
153
  error:(NSError **)error;
154
154
 
155
- /// Phase 5: pose-driven stitch. Same end-to-end shape as
156
- /// `stitchVideoAtPath` but consumes pre-computed camera poses
157
- /// (from ARKit/ARCore via RNSARSession) and skips the
158
- /// brittle features → matching → BundleAdjuster steps. Internally:
159
- ///
160
- /// 1. Extract maxFrames evenly-spaced frames from the video.
161
- /// 2. Compute each frame's timestamp (fraction × totalSeconds).
162
- /// 3. Match each frame to the nearest pose in `poses` (within
163
- /// a 100 ms tolerance).
164
- /// 4. Build cv::detail::CameraParams directly from the pose's
165
- /// quaternion + intrinsics — flips coordinate conventions
166
- /// between ARKit (Y-up, -Z forward) and OpenCV (Y-down,
167
- /// +Z forward).
168
- /// 5. Hand cameras to the existing warp + seam + blend pipeline.
169
- ///
170
- /// `poses` is an NSArray of NSDictionary; each entry has the keys
171
- /// matching `RNSARFramePose.asDictionary()`:
172
- /// tx, ty, tz, qx, qy, qz, qw, fx, fy, cx, cy,
173
- /// imageWidth, imageHeight, timestampMs, trackingState
174
- /// Frames whose closest pose is missing or beyond tolerance fall
175
- /// back to the feature-matched path frame-by-frame (degraded but
176
- /// functional). When ALL poses are missing the method returns
177
- /// the same NSError code (1030) so the host can opt to retry via
178
- /// the non-pose path.
179
- + (nullable RNStitchResult *)stitchVideoAtPath:(NSString *)videoPath
180
- outputPath:(NSString *)outputPath
181
- maxFrames:(NSInteger)maxFrames
182
- jpegQuality:(NSInteger)quality
183
- warperType:(nullable NSString *)warperType
184
- blenderType:(nullable NSString *)blenderType
185
- seamFinderType:(nullable NSString *)seamFinderType
186
- poses:(NSArray<NSDictionary *> *)poses
187
- error:(NSError **)error;
188
-
189
- /// V16 Phase 1: pose-driven stitch over an explicit list of frame
190
- /// paths. Sibling of `stitchVideoAtPath:withPoses:` — same compose
191
- /// stage, but the caller supplies frames as already-on-disk JPEGs
192
- /// + a 1:1 pose array, so the video extraction + timestamp matching
193
- /// steps are skipped entirely.
194
- ///
195
- /// This is the hot path for the "batch-on-AR-keyframes" flow: the
196
- /// Swift `KeyframeGate` accepts ≤6 frames per capture, each saved
197
- /// to disk with a known pose; on shutter release we feed those
198
- /// straight into the same `BundleAdjuster + GraphCutSeamFinder +
199
- /// MultiBandBlender` pipeline that the video-driven path uses.
200
- ///
201
- /// `framePaths.count` MUST equal `poses.count` (1:1 mapping; any
202
- /// downstream filtering happens inside this method). `framePaths`
203
- /// must be at least 2 entries. Pose dictionaries follow the same
204
- /// shape as `RNSARFramePose.asDictionary()`.
205
- + (nullable RNStitchResult *)stitchKeyframePaths:(NSArray<NSString *> *)framePaths
206
- outputPath:(NSString *)outputPath
207
- jpegQuality:(NSInteger)quality
208
- warperType:(nullable NSString *)warperType
209
- blenderType:(nullable NSString *)blenderType
210
- seamFinderType:(nullable NSString *)seamFinderType
211
- poses:(NSArray<NSDictionary *> *)poses
212
- error:(NSError **)error;
213
-
214
155
  /// Normalise the EXIF orientation of `imagePath` in place.
215
156
  ///
216
157
  /// vision-camera writes photos with the camera-sensor's native
@@ -234,6 +175,33 @@ extern NSString *const RNImageStitcherErrorDomain;
234
175
  + (nullable NSDictionary<NSString *, NSNumber *> *)normaliseImageAtPath:(NSString *)imagePath
235
176
  error:(NSError **)error;
236
177
 
178
+ /// v0.15 debug — compute the max-inscribed rectangle of the non-black
179
+ /// region of the JPEG at `imagePath` WITHOUT modifying the file.
180
+ /// Returns `{ x, y, width, height, imageWidth, imageHeight }` so the JS
181
+ /// debug harness can overlay the rect on the full image. Reuses the
182
+ /// same `MaxInscribedRectFromMask` the production crop uses.
183
+ + (nullable NSDictionary<NSString *, NSNumber *> *)computeInscribedRectAtPath:(NSString *)imagePath
184
+ error:(NSError **)error;
185
+
186
+ /// v0.15 debug — crop the JPEG at `imagePath` to the given rectangle
187
+ /// (clamped to image bounds), re-encode at `quality`, overwrite in
188
+ /// place. Returns the final `{ width, height }`.
189
+ + (nullable NSDictionary<NSString *, NSNumber *> *)cropToRectAtPath:(NSString *)imagePath
190
+ x:(NSInteger)x
191
+ y:(NSInteger)y
192
+ width:(NSInteger)width
193
+ height:(NSInteger)height
194
+ quality:(NSInteger)quality
195
+ error:(NSError **)error;
196
+
197
+ /// v0.15 debug — write a red-tinted overlay JPEG (excluded / sub-threshold
198
+ /// pixels rendered red) next to `imagePath` (suffix ".mask.jpg") so the
199
+ /// harness can show WHY the inscribed rect lands where it does. Returns
200
+ /// `{ maskPath, width, height, excludedPercent }`.
201
+ + (nullable NSDictionary *)debugMaskOverlayAtPath:(NSString *)imagePath
202
+ threshold:(NSInteger)threshold
203
+ error:(NSError **)error;
204
+
237
205
  @end
238
206
 
239
207
  NS_ASSUME_NONNULL_END