react-native-image-stitcher 0.15.2 → 0.16.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 +124 -1
- package/README.md +116 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/stitcher.cpp +651 -55
- package/cpp/stitcher.hpp +10 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +196 -12
- package/dist/camera/Camera.js +629 -35
- package/dist/camera/CameraView.js +35 -16
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +142 -0
- package/dist/camera/CaptureMemoryPill.d.ts +9 -1
- package/dist/camera/CaptureMemoryPill.js +3 -3
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +26 -5
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +161 -0
- package/dist/camera/RectCropPreview.js +480 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +45 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +191 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +994 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
- package/src/camera/CaptureMemoryPill.tsx +17 -3
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaSettings.ts +34 -11
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +820 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +45 -0
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
// The header lives in the SDK's `cpp/` dir and is on the pod's
|
|
59
59
|
// HEADER_SEARCH_PATHS (see RNImageStitcher.podspec).
|
|
60
60
|
#import "stitcher.hpp"
|
|
61
|
+
// item-7 cropToQuad: the OpenCV-free quad geometry (quadDstRect /
|
|
62
|
+
// isQuadAcceptable) + the shared canvas OOM guard. Same `cpp/`
|
|
63
|
+
// HEADER_SEARCH_PATHS as stitcher.hpp.
|
|
64
|
+
#import "crop_quad.hpp"
|
|
65
|
+
#import "warp_guard.hpp"
|
|
61
66
|
#import <UIKit/UIKit.h>
|
|
62
67
|
#import <AVFoundation/AVFoundation.h>
|
|
63
68
|
#import <os/log.h>
|
|
@@ -262,6 +267,12 @@ NSString *const RNImageStitcherErrorDomain = @"RNImageStitcherErrorDomain";
|
|
|
262
267
|
// RNStitchResult
|
|
263
268
|
// ─────────────────────────────────────────────────────────────────────
|
|
264
269
|
|
|
270
|
+
// Redeclare debugSummary as readwrite internally so it can be set after the
|
|
271
|
+
// designated initializer (keeps the init signature unchanged).
|
|
272
|
+
@interface RNStitchResult ()
|
|
273
|
+
@property (nonatomic, copy, readwrite) NSString *debugSummary;
|
|
274
|
+
@end
|
|
275
|
+
|
|
265
276
|
@implementation RNStitchResult
|
|
266
277
|
|
|
267
278
|
- (instancetype)initWithOutputPath:(NSString *)outputPath
|
|
@@ -280,6 +291,7 @@ NSString *const RNImageStitcherErrorDomain = @"RNImageStitcherErrorDomain";
|
|
|
280
291
|
_framesRequested = framesRequested;
|
|
281
292
|
_framesIncluded = framesIncluded;
|
|
282
293
|
_finalConfidenceThresh = finalConfidenceThresh;
|
|
294
|
+
_debugSummary = @"";
|
|
283
295
|
}
|
|
284
296
|
return self;
|
|
285
297
|
}
|
|
@@ -411,6 +423,7 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
411
423
|
captureOrientation:(NSString *)captureOrientation
|
|
412
424
|
useInscribedRectCrop:(BOOL)useInscribedRectCrop
|
|
413
425
|
stitchMode:(NSString *)stitchMode
|
|
426
|
+
useManualPipeline:(BOOL)useManualPipeline
|
|
414
427
|
error:(NSError **)error {
|
|
415
428
|
// ── Phase 2 (2026-05-16): delegated to shared C++ ───────────────────
|
|
416
429
|
//
|
|
@@ -438,7 +451,7 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
438
451
|
// working until they are updated. The shared C++ has its own
|
|
439
452
|
// defaults but we want the wrapper to be tolerant of nil inputs
|
|
440
453
|
// from Swift / Obj-C callers that grew up against the legacy API.
|
|
441
|
-
if (warperType == nil || warperType.length == 0) warperType = @"
|
|
454
|
+
if (warperType == nil || warperType.length == 0) warperType = @"spherical";
|
|
442
455
|
if (blenderType == nil || blenderType.length == 0) blenderType = @"multiband";
|
|
443
456
|
if (seamFinderType == nil || seamFinderType.length == 0) seamFinderType = @"graphcut";
|
|
444
457
|
if (captureOrientation == nil || captureOrientation.length == 0) captureOrientation = @"portrait";
|
|
@@ -479,12 +492,22 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
479
492
|
cfg.availableRamMB =
|
|
480
493
|
(double)NSProcessInfo.processInfo.physicalMemory
|
|
481
494
|
/ (1024.0 * 1024.0);
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
495
|
+
// 2026-06-15 — DEFAULT to the MANUAL cv::detail pipeline. ALL the memory/OOM
|
|
496
|
+
// hardening lives on the manual path (PreStitchMemoryAbort, RAM-aware
|
|
497
|
+
// canvas-budget downscale, STREAM/BATCH held-set routing, the black-canvas
|
|
498
|
+
// utilization guard); the high-level cv::Stitcher path calls NONE of it. So
|
|
499
|
+
// manual is both the user's preferred output AND the memory-safe one.
|
|
500
|
+
//
|
|
501
|
+
// WARPER: NOT hardcoded — cfg.warperType carries the caller's choice (set from
|
|
502
|
+
// the JS `warperType`, which defaults to "spherical" and is settable via the
|
|
503
|
+
// ⚙️ panel / the host's `defaultWarper` prop). The JS default is the single
|
|
504
|
+
// source of truth now. Choosing "plane" re-arms the dynamic plane→spherical
|
|
505
|
+
// fallback + divergence switch in the manual pipeline (they only fire when
|
|
506
|
+
// warperType != "spherical").
|
|
507
|
+
//
|
|
508
|
+
// The pipeline is caller-driven: batch capture passes YES (manual, default
|
|
509
|
+
// output); the on-demand high-level tab re-stitches with NO.
|
|
510
|
+
cfg.useManualPipeline = useManualPipeline;
|
|
488
511
|
|
|
489
512
|
// Marshal NSArray<NSString*> → std::vector<std::string>. Strip the
|
|
490
513
|
// `file://` scheme that some callers attach so the shared C++ can
|
|
@@ -566,6 +589,18 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
566
589
|
framesRequested:framesRequested
|
|
567
590
|
framesIncluded:(NSInteger)r.framesIncluded
|
|
568
591
|
finalConfidenceThresh:r.finalConfidenceThresh];
|
|
592
|
+
if (!r.debugSummary.empty()) {
|
|
593
|
+
result.debugSummary =
|
|
594
|
+
[NSString stringWithUTF8String:r.debugSummary.c_str()];
|
|
595
|
+
}
|
|
596
|
+
// 2026-06-15 — the eager A/B harness that ALSO stitched the high-level
|
|
597
|
+
// alt on EVERY capture has been REMOVED. Manual is now the default (this
|
|
598
|
+
// method), so computing high-level eagerly was pure wasted work —
|
|
599
|
+
// especially while profiling memory/perf — when the user isn't viewing
|
|
600
|
+
// it. The keyframe JPEGs are retained on disk so high-level can be
|
|
601
|
+
// produced ON DEMAND (follow-up: a `useManualPipeline` param on this
|
|
602
|
+
// method lets `refinePanorama` re-stitch them via the high-level path
|
|
603
|
+
// when the user switches to the high-level tab).
|
|
569
604
|
} else {
|
|
570
605
|
// Map StitchErrorCode → NSError.code. Preserves the existing
|
|
571
606
|
// 9001/9002/9003/1001/9007 sentinels the JS UX layer already
|
|
@@ -863,6 +898,7 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
863
898
|
captureOrientation:nil
|
|
864
899
|
useInscribedRectCrop:NO
|
|
865
900
|
stitchMode:nil
|
|
901
|
+
useManualPipeline:NO // legacy video path keeps high-level cv::Stitcher
|
|
866
902
|
error:&stitchErr];
|
|
867
903
|
|
|
868
904
|
// Always tear down the tmp dir, success or fail — leaving
|
|
@@ -1063,6 +1099,154 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
1063
1099
|
};
|
|
1064
1100
|
}
|
|
1065
1101
|
|
|
1102
|
+
// item-7 — free-quad perspective crop. Mirrors cropToRectAtPath, but
|
|
1103
|
+
// instead of an axis-aligned sub-rectangle it takes 4 user-dragged
|
|
1104
|
+
// corners in IMAGE-PIXEL space (ordered TL, TR, BR, BL by the JS editor's
|
|
1105
|
+
// orderQuadCorners) and rectifies them to an upright rectangle via
|
|
1106
|
+
// cv::getPerspectiveTransform + cv::warpPerspective. The destination
|
|
1107
|
+
// size + the convex/min-area/in-bounds gate come from the shared OpenCV-
|
|
1108
|
+
// free cpp/crop_quad.hpp so iOS / Android / JS agree bit-for-bit; the
|
|
1109
|
+
// output canvas is GUARDED with the same canvasExceedsGuard the stitch
|
|
1110
|
+
// pipeline uses so a near-collinear quad can't OOM a multi-MP panorama.
|
|
1111
|
+
+ (NSDictionary<NSString *, NSNumber *> *)cropToQuadAtPath:(NSString *)imagePath
|
|
1112
|
+
tlX:(double)tlX
|
|
1113
|
+
tlY:(double)tlY
|
|
1114
|
+
trX:(double)trX
|
|
1115
|
+
trY:(double)trY
|
|
1116
|
+
brX:(double)brX
|
|
1117
|
+
brY:(double)brY
|
|
1118
|
+
blX:(double)blX
|
|
1119
|
+
blY:(double)blY
|
|
1120
|
+
quality:(NSInteger)quality
|
|
1121
|
+
error:(NSError **)error {
|
|
1122
|
+
NSString *cleaned = normalizeImagePath(imagePath);
|
|
1123
|
+
if (![[NSFileManager defaultManager] fileExistsAtPath:cleaned]) {
|
|
1124
|
+
if (error) {
|
|
1125
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1126
|
+
code:1020
|
|
1127
|
+
userInfo:@{
|
|
1128
|
+
NSLocalizedDescriptionKey:
|
|
1129
|
+
[NSString stringWithFormat:@"Image not found: %@", imagePath],
|
|
1130
|
+
}];
|
|
1131
|
+
}
|
|
1132
|
+
return nil;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
std::string nativePath(cleaned.UTF8String);
|
|
1136
|
+
cv::Mat img = cv::imread(nativePath, cv::IMREAD_COLOR);
|
|
1137
|
+
if (img.empty()) {
|
|
1138
|
+
if (error) {
|
|
1139
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1140
|
+
code:1021
|
|
1141
|
+
userInfo:@{
|
|
1142
|
+
NSLocalizedDescriptionKey:
|
|
1143
|
+
[NSString stringWithFormat:@"Could not decode image at %@", imagePath],
|
|
1144
|
+
}];
|
|
1145
|
+
}
|
|
1146
|
+
return nil;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
retailens::CropQuad quad;
|
|
1150
|
+
quad.tl = {tlX, tlY};
|
|
1151
|
+
quad.tr = {trX, trY};
|
|
1152
|
+
quad.br = {brX, brY};
|
|
1153
|
+
quad.bl = {blX, blY};
|
|
1154
|
+
|
|
1155
|
+
// Geometry gate — convex, non-degenerate, inside the decoded image.
|
|
1156
|
+
if (!retailens::isQuadAcceptable(quad, (double)img.cols, (double)img.rows)) {
|
|
1157
|
+
if (error) {
|
|
1158
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1159
|
+
code:1023
|
|
1160
|
+
userInfo:@{
|
|
1161
|
+
NSLocalizedDescriptionKey:
|
|
1162
|
+
@"Crop quad is degenerate (non-convex, zero-area, or out of bounds)",
|
|
1163
|
+
}];
|
|
1164
|
+
}
|
|
1165
|
+
return nil;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const retailens::QuadDstSize dst = retailens::quadDstRect(quad);
|
|
1169
|
+
// Output-canvas OOM net — the same guard the stitch pipeline uses.
|
|
1170
|
+
if (dst.width <= 0 || dst.height <= 0 ||
|
|
1171
|
+
retailens::canvasExceedsGuard(dst.width, dst.height)) {
|
|
1172
|
+
if (error) {
|
|
1173
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1174
|
+
code:1024
|
|
1175
|
+
userInfo:@{
|
|
1176
|
+
NSLocalizedDescriptionKey:
|
|
1177
|
+
[NSString stringWithFormat:
|
|
1178
|
+
@"Crop quad output canvas is degenerate or exceeds the size guard (%dx%d)",
|
|
1179
|
+
dst.width, dst.height],
|
|
1180
|
+
}];
|
|
1181
|
+
}
|
|
1182
|
+
return nil;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const cv::Point2f src[4] = {
|
|
1186
|
+
cv::Point2f((float)tlX, (float)tlY),
|
|
1187
|
+
cv::Point2f((float)trX, (float)trY),
|
|
1188
|
+
cv::Point2f((float)brX, (float)brY),
|
|
1189
|
+
cv::Point2f((float)blX, (float)blY),
|
|
1190
|
+
};
|
|
1191
|
+
const cv::Point2f dstPts[4] = {
|
|
1192
|
+
cv::Point2f(0.0f, 0.0f),
|
|
1193
|
+
cv::Point2f((float)dst.width, 0.0f),
|
|
1194
|
+
cv::Point2f((float)dst.width, (float)dst.height),
|
|
1195
|
+
cv::Point2f(0.0f, (float)dst.height),
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
cv::Mat warped;
|
|
1199
|
+
// OpenCV throws cv::Exception (a C++ exception) — catch with a C++
|
|
1200
|
+
// try/catch, NOT @try/@catch (which only traps NSException).
|
|
1201
|
+
try {
|
|
1202
|
+
cv::Mat transform = cv::getPerspectiveTransform(src, dstPts);
|
|
1203
|
+
cv::warpPerspective(img, warped, transform,
|
|
1204
|
+
cv::Size(dst.width, dst.height), cv::INTER_LINEAR);
|
|
1205
|
+
} catch (const cv::Exception &e) {
|
|
1206
|
+
if (error) {
|
|
1207
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1208
|
+
code:1025
|
|
1209
|
+
userInfo:@{
|
|
1210
|
+
NSLocalizedDescriptionKey:
|
|
1211
|
+
[NSString stringWithFormat:@"Perspective warp failed: %s", e.what()],
|
|
1212
|
+
}];
|
|
1213
|
+
}
|
|
1214
|
+
return nil;
|
|
1215
|
+
}
|
|
1216
|
+
if (warped.empty()) {
|
|
1217
|
+
if (error) {
|
|
1218
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1219
|
+
code:1025
|
|
1220
|
+
userInfo:@{
|
|
1221
|
+
NSLocalizedDescriptionKey: @"Perspective warp produced an empty image",
|
|
1222
|
+
}];
|
|
1223
|
+
}
|
|
1224
|
+
return nil;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
int q = (int)quality;
|
|
1228
|
+
if (q < 1) { q = 1; }
|
|
1229
|
+
if (q > 100) { q = 100; }
|
|
1230
|
+
std::vector<int> writeParams = { cv::IMWRITE_JPEG_QUALITY, q };
|
|
1231
|
+
bool ok = cv::imwrite(nativePath, warped, writeParams);
|
|
1232
|
+
if (!ok) {
|
|
1233
|
+
if (error) {
|
|
1234
|
+
*error = [NSError errorWithDomain:RNImageStitcherErrorDomain
|
|
1235
|
+
code:1022
|
|
1236
|
+
userInfo:@{
|
|
1237
|
+
NSLocalizedDescriptionKey:
|
|
1238
|
+
[NSString stringWithFormat:@"Could not rewrite image at %@", imagePath],
|
|
1239
|
+
}];
|
|
1240
|
+
}
|
|
1241
|
+
return nil;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return @{
|
|
1245
|
+
@"width": @((NSInteger)warped.cols),
|
|
1246
|
+
@"height": @((NSInteger)warped.rows),
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1066
1250
|
+ (NSDictionary *)debugMaskOverlayAtPath:(NSString *)imagePath
|
|
1067
1251
|
threshold:(NSInteger)threshold
|
|
1068
1252
|
error:(NSError **)error {
|
|
@@ -154,6 +154,10 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
154
154
|
attributes: .concurrent
|
|
155
155
|
)
|
|
156
156
|
private static let MAX_POSE_LOG = 600 // ~10 s @ 60Hz
|
|
157
|
+
/// AR keyframe long-edge budget (px) — downscale every device's frame to
|
|
158
|
+
/// this before encoding so stitch memory is consistent cross-device.
|
|
159
|
+
/// Mirrors Android's AR_KEYFRAME_MAX_LONG_EDGE.
|
|
160
|
+
private static let arKeyframeMaxLongEdge: CGFloat = 640
|
|
157
161
|
|
|
158
162
|
/// Latest tracking state. Read by JS for UI feedback.
|
|
159
163
|
@objc public private(set) var currentTrackingState: RNSARTrackingState = .notAvailable
|
|
@@ -472,6 +476,18 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
472
476
|
// small text and packaging detail.
|
|
473
477
|
config.isAutoFocusEnabled = true
|
|
474
478
|
|
|
479
|
+
// Option B — prefer the 4:3 videoFormat for full sensor FOV; the
|
|
480
|
+
// keyframe is downscaled to arKeyframeMaxLongEdge below so memory
|
|
481
|
+
// stays consistent across devices regardless of the format res.
|
|
482
|
+
if let fmt = ARWorldTrackingConfiguration.supportedVideoFormats.min(by: { a, b in
|
|
483
|
+
let da = abs(a.imageResolution.width / a.imageResolution.height - 4.0 / 3.0)
|
|
484
|
+
let db = abs(b.imageResolution.width / b.imageResolution.height - 4.0 / 3.0)
|
|
485
|
+
if abs(da - db) > 0.001 { return da < db }
|
|
486
|
+
return a.imageResolution.width * a.imageResolution.height
|
|
487
|
+
< b.imageResolution.width * b.imageResolution.height
|
|
488
|
+
}) {
|
|
489
|
+
config.videoFormat = fmt
|
|
490
|
+
}
|
|
475
491
|
arSession.run(config, options: [.resetTracking, .removeExistingAnchors])
|
|
476
492
|
// V16-diag — log the chosen video format so we can correlate
|
|
477
493
|
// batch-keyframe memory with ARFrame resolution. iPhone Pro
|
|
@@ -855,8 +871,16 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
855
871
|
case "portrait-upside-down": exifOrientation = .left
|
|
856
872
|
default: exifOrientation = .right // portrait + unknown
|
|
857
873
|
}
|
|
858
|
-
|
|
874
|
+
var ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
|
859
875
|
.oriented(exifOrientation)
|
|
876
|
+
// AR keyframe downscale guard — normalise long edge to the budget so
|
|
877
|
+
// every device produces a ~0.3 MP keyframe (cross-device-consistent
|
|
878
|
+
// stitch memory). Mirrors Android's downscale in YuvImageConverter.
|
|
879
|
+
let kfLongEdge = max(ciImage.extent.width, ciImage.extent.height)
|
|
880
|
+
if kfLongEdge > Self.arKeyframeMaxLongEdge {
|
|
881
|
+
let kfScale = Self.arKeyframeMaxLongEdge / kfLongEdge
|
|
882
|
+
ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: kfScale, y: kfScale))
|
|
883
|
+
}
|
|
860
884
|
let context = CIContext(options: nil)
|
|
861
885
|
guard let cgImage = context.createCGImage(
|
|
862
886
|
ciImage,
|
|
@@ -161,7 +161,11 @@ public enum Stitcher {
|
|
|
161
161
|
// expose stitchMode in its options dict yet. nil falls
|
|
162
162
|
// through to Panorama in OpenCVStitcher.mm (preserves
|
|
163
163
|
// historical behaviour).
|
|
164
|
-
stitchMode: nil
|
|
164
|
+
stitchMode: nil,
|
|
165
|
+
// Generic one-shot API keeps the high-level cv::Stitcher pipeline
|
|
166
|
+
// (its historical behaviour); the batch capture is what defaults to
|
|
167
|
+
// manual. warperType "plane" above only matters on the manual path.
|
|
168
|
+
useManualPipeline: false
|
|
165
169
|
)
|
|
166
170
|
return StitchResult(
|
|
167
171
|
outputPath: result.outputPath,
|
|
@@ -239,6 +243,35 @@ public enum Stitcher {
|
|
|
239
243
|
}
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
/// item-7 — perspective-rectify the quadrilateral with corners
|
|
247
|
+
/// `(tlX,tlY) (trX,trY) (brX,brY) (blX,blY)` (IMAGE-PIXEL space, ordered
|
|
248
|
+
/// TL→TR→BR→BL) out of `imagePath` into an upright rectangle, overwrite
|
|
249
|
+
/// in place, re-encode at `quality`. Pairs with the axis-aligned
|
|
250
|
+
/// `cropToRect`; the JS editor picks this when the dragged quad isn't
|
|
251
|
+
/// ~rectangular. Returns the rectified `{ width, height }`.
|
|
252
|
+
public static func cropToQuad(
|
|
253
|
+
imagePath: String,
|
|
254
|
+
tlX: Double, tlY: Double,
|
|
255
|
+
trX: Double, trY: Double,
|
|
256
|
+
brX: Double, brY: Double,
|
|
257
|
+
blX: Double, blY: Double,
|
|
258
|
+
quality: Int
|
|
259
|
+
) throws -> (width: Int, height: Int) {
|
|
260
|
+
do {
|
|
261
|
+
let d = try OpenCVStitcher.cropToQuad(
|
|
262
|
+
atPath: imagePath,
|
|
263
|
+
tlX: tlX, tlY: tlY,
|
|
264
|
+
trX: trX, trY: trY,
|
|
265
|
+
brX: brX, brY: brY,
|
|
266
|
+
blX: blX, blY: blY,
|
|
267
|
+
quality: quality
|
|
268
|
+
)
|
|
269
|
+
return (width: d["width"]?.intValue ?? 0, height: d["height"]?.intValue ?? 0)
|
|
270
|
+
} catch let nsError as NSError {
|
|
271
|
+
throw StitcherError.fromNSError(nsError)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
242
275
|
/// v0.15 debug — write a red-tinted mask overlay (excluded pixels =
|
|
243
276
|
/// red) next to the image and report what fraction the brightness mask
|
|
244
277
|
/// drops. Pairs with the inscribed-rect debug harness.
|
|
@@ -34,6 +34,11 @@ RCT_EXTERN_METHOD(cropToRect:(NSDictionary *)options
|
|
|
34
34
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
35
35
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
36
36
|
|
|
37
|
+
// item-7 perspective crop (free-quad rectify; pairs with cropToRect).
|
|
38
|
+
RCT_EXTERN_METHOD(cropToQuad:(NSDictionary *)options
|
|
39
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
40
|
+
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
41
|
+
|
|
37
42
|
RCT_EXTERN_METHOD(debugMaskOverlay:(NSDictionary *)options
|
|
38
43
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
39
44
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
@@ -265,6 +265,62 @@ public class StitcherBridge: NSObject {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/// item-7 — perspective-rectify a user-dragged quad. `options` carries
|
|
269
|
+
/// `imagePath` + the 4 IMAGE-PIXEL corners as a flat `quad` array of 8
|
|
270
|
+
/// numbers `[tlX, tlY, trX, trY, brX, brY, blX, blY]` (ordered
|
|
271
|
+
/// TL→TR→BR→BL by the JS editor) + optional `quality` (default 90).
|
|
272
|
+
/// Resolves the rectified `{ width, height }`.
|
|
273
|
+
@objc(cropToQuad:resolver:rejecter:)
|
|
274
|
+
public func cropToQuad(
|
|
275
|
+
options: NSDictionary,
|
|
276
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
277
|
+
rejecter: @escaping RCTPromiseRejectBlock
|
|
278
|
+
) {
|
|
279
|
+
guard let imagePath = options["imagePath"] as? String else {
|
|
280
|
+
rejecter("invalid-options", "imagePath must be a string", nil)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
guard let quad = options["quad"] as? [NSNumber], quad.count == 8 else {
|
|
284
|
+
rejecter(
|
|
285
|
+
"invalid-options",
|
|
286
|
+
"quad must be an array of 8 numbers [tlX,tlY,trX,trY,brX,brY,blX,blY]",
|
|
287
|
+
nil
|
|
288
|
+
)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
let p = quad.map { $0.doubleValue }
|
|
292
|
+
let quality = (options["quality"] as? NSNumber)?.intValue ?? 90
|
|
293
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
294
|
+
do {
|
|
295
|
+
let dims = try Stitcher.cropToQuad(
|
|
296
|
+
imagePath: imagePath,
|
|
297
|
+
tlX: p[0], tlY: p[1],
|
|
298
|
+
trX: p[2], trY: p[3],
|
|
299
|
+
brX: p[4], brY: p[5],
|
|
300
|
+
blX: p[6], blY: p[7],
|
|
301
|
+
quality: quality
|
|
302
|
+
)
|
|
303
|
+
resolver([
|
|
304
|
+
"width": dims.width,
|
|
305
|
+
"height": dims.height,
|
|
306
|
+
])
|
|
307
|
+
} catch let err as StitcherError {
|
|
308
|
+
switch err {
|
|
309
|
+
case .insufficientFrames(let count):
|
|
310
|
+
rejecter("insufficient-frames", "(unexpected for cropToQuad) frames=\(count)", err)
|
|
311
|
+
case .readFailed(let path):
|
|
312
|
+
rejecter("read-failed", "Could not read image: \(path)", err)
|
|
313
|
+
case .writeFailed(let path):
|
|
314
|
+
rejecter("write-failed", "Could not write image: \(path)", err)
|
|
315
|
+
case .opencvFailed(let code, let message):
|
|
316
|
+
rejecter("opencv-failed-\(code)", message, err)
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
rejecter("unknown", "Unexpected cropToQuad failure: \(error)", error)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
268
324
|
/// v0.15 debug — write a red-tinted mask overlay for `imagePath`
|
|
269
325
|
/// (excluded pixels red). `threshold` optional (default 1, matching the
|
|
270
326
|
/// inscribed-rect mask). Resolves
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"dist/**/*.js",
|
|
9
9
|
"dist/**/*.d.ts",
|
|
10
10
|
"src",
|
|
11
|
+
"!src/**/__tests__",
|
|
12
|
+
"!src/**/__tests__/**",
|
|
11
13
|
"ios/Sources",
|
|
12
14
|
"ios/Package.swift",
|
|
13
15
|
"android/build.gradle",
|
|
@@ -15,6 +17,8 @@
|
|
|
15
17
|
"android/src/main/cpp",
|
|
16
18
|
"android/src/main/AndroidManifest.xml",
|
|
17
19
|
"cpp",
|
|
20
|
+
"!cpp/tests",
|
|
21
|
+
"!cpp/tests/**",
|
|
18
22
|
"scripts/postinstall-fetch-binaries.js",
|
|
19
23
|
"scripts/opencv-version.txt",
|
|
20
24
|
"RNImageStitcher.podspec",
|