react-native-image-stitcher 0.14.2 → 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.
- package/CHANGELOG.md +131 -0
- package/README.md +35 -0
- package/RNImageStitcher.podspec +8 -7
- package/android/build.gradle +0 -16
- package/android/src/main/cpp/CMakeLists.txt +2 -63
- package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +13 -64
- package/cpp/keyframe_gate.cpp +82 -23
- package/cpp/keyframe_gate.hpp +31 -2
- package/cpp/stitcher.cpp +208 -28
- package/cpp/tests/CMakeLists.txt +18 -12
- package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
- package/cpp/tests/warp_guard_test.cpp +48 -0
- package/cpp/warp_guard.hpp +41 -0
- package/dist/camera/Camera.d.ts +31 -16
- package/dist/camera/Camera.js +10 -2
- package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
- package/dist/camera/CaptureStitchStatsToast.js +27 -7
- package/dist/camera/PanoramaSettings.d.ts +10 -223
- package/dist/camera/PanoramaSettings.js +6 -28
- package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
- package/dist/camera/PanoramaSettingsBridge.js +3 -102
- package/dist/camera/PanoramaSettingsModal.js +7 -1
- package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
- package/dist/camera/buildPanoramaInitialSettings.js +4 -0
- package/dist/camera/cameraErrorMessages.d.ts +32 -0
- package/dist/camera/cameraErrorMessages.js +53 -0
- package/dist/camera/selectCaptureDevice.d.ts +5 -1
- package/dist/camera/selectCaptureDevice.js +22 -2
- package/dist/camera/useCapture.js +38 -0
- package/dist/index.d.ts +5 -8
- package/dist/index.js +11 -34
- package/dist/stitching/incremental.d.ts +1 -117
- package/dist/stitching/stitchVideo.d.ts +0 -35
- package/dist/types.d.ts +0 -87
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
- package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
- package/package.json +3 -2
- package/src/camera/Camera.tsx +43 -22
- package/src/camera/CaptureStitchStatsToast.tsx +58 -14
- package/src/camera/PanoramaSettings.ts +16 -289
- package/src/camera/PanoramaSettingsBridge.ts +3 -114
- package/src/camera/PanoramaSettingsModal.tsx +14 -1
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
- package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
- package/src/camera/buildPanoramaInitialSettings.ts +17 -0
- package/src/camera/cameraErrorMessages.ts +84 -0
- package/src/camera/selectCaptureDevice.ts +28 -3
- package/src/camera/useCapture.ts +44 -1
- package/src/index.ts +11 -40
- package/src/stitching/incremental.ts +3 -140
- package/src/stitching/stitchVideo.ts +0 -26
- package/src/types.ts +0 -95
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
- package/cpp/stitcher_frame_jsi.cpp +0 -214
- package/cpp/stitcher_frame_jsi.hpp +0 -108
- package/cpp/stitcher_proxy_jsi.cpp +0 -109
- package/cpp/stitcher_proxy_jsi.hpp +0 -46
- package/cpp/stitcher_worklet_dispatch.cpp +0 -103
- package/cpp/stitcher_worklet_dispatch.hpp +0 -71
- package/cpp/stitcher_worklet_registry.cpp +0 -91
- package/cpp/stitcher_worklet_registry.hpp +0 -146
- package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
- package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
- package/dist/stitching/IncrementalStitcherView.js +0 -157
- package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
- package/dist/stitching/StitcherWorkletRegistry.js +0 -78
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
- package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
- package/dist/stitching/useFrameProcessor.d.ts +0 -119
- package/dist/stitching/useFrameProcessor.js +0 -196
- package/dist/stitching/useFrameStream.d.ts +0 -34
- package/dist/stitching/useFrameStream.js +0 -234
- package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
- package/dist/stitching/useThrottledFrameProcessor.js +0 -132
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
- package/src/stitching/IncrementalStitcherView.tsx +0 -198
- package/src/stitching/StitcherWorkletRegistry.ts +0 -156
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
- package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
- package/src/stitching/useFrameProcessor.ts +0 -226
- package/src/stitching/useFrameStream.ts +0 -271
- 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
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
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
|
-
//
|
|
160
|
-
//
|
|
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
|
-
// `
|
|
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 `
|
|
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.
|
|
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
|
-
//
|
|
240
|
-
//
|
|
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 /
|
|
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
|