react-native-image-stitcher 0.1.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 +96 -0
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +189 -0
- package/RNImageStitcher.podspec +76 -0
- package/android/build.gradle +224 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +124 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
- package/cpp/ar_frame_pose.h +63 -0
- package/cpp/keyframe_gate.cpp +927 -0
- package/cpp/keyframe_gate.hpp +240 -0
- package/cpp/stitcher.cpp +2207 -0
- package/cpp/stitcher.hpp +275 -0
- package/dist/ar/useARSession.d.ts +102 -0
- package/dist/ar/useARSession.js +133 -0
- package/dist/camera/ARCameraView.d.ts +93 -0
- package/dist/camera/ARCameraView.js +170 -0
- package/dist/camera/Camera.d.ts +134 -0
- package/dist/camera/Camera.js +688 -0
- package/dist/camera/CameraShutter.d.ts +80 -0
- package/dist/camera/CameraShutter.js +237 -0
- package/dist/camera/CameraView.d.ts +65 -0
- package/dist/camera/CameraView.js +117 -0
- package/dist/camera/CaptureControlsBar.d.ts +87 -0
- package/dist/camera/CaptureControlsBar.js +82 -0
- package/dist/camera/CaptureHeader.d.ts +62 -0
- package/dist/camera/CaptureHeader.js +81 -0
- package/dist/camera/CapturePreview.d.ts +70 -0
- package/dist/camera/CapturePreview.js +188 -0
- package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
- package/dist/camera/CaptureStatusOverlay.js +326 -0
- package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
- package/dist/camera/CaptureThumbnailStrip.js +177 -0
- package/dist/camera/IncrementalPanGuide.d.ts +83 -0
- package/dist/camera/IncrementalPanGuide.js +267 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
- package/dist/camera/PanoramaBandOverlay.js +399 -0
- package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
- package/dist/camera/PanoramaConfirmModal.js +128 -0
- package/dist/camera/PanoramaGuidance.d.ts +79 -0
- package/dist/camera/PanoramaGuidance.js +246 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
- package/dist/camera/PanoramaSettingsModal.js +611 -0
- package/dist/camera/ViewportCropOverlay.d.ts +46 -0
- package/dist/camera/ViewportCropOverlay.js +67 -0
- package/dist/camera/useCapture.d.ts +111 -0
- package/dist/camera/useCapture.js +160 -0
- package/dist/camera/useDeviceOrientation.d.ts +48 -0
- package/dist/camera/useDeviceOrientation.js +131 -0
- package/dist/camera/useVideoCapture.d.ts +79 -0
- package/dist/camera/useVideoCapture.js +151 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +39 -0
- package/dist/quality/normaliseOrientation.d.ts +36 -0
- package/dist/quality/normaliseOrientation.js +62 -0
- package/dist/quality/runQualityCheck.d.ts +41 -0
- package/dist/quality/runQualityCheck.js +98 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
- package/dist/sensors/useIMUTranslationGate.js +235 -0
- package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
- package/dist/stitching/IncrementalStitcherView.js +157 -0
- package/dist/stitching/incremental.d.ts +930 -0
- package/dist/stitching/incremental.js +133 -0
- package/dist/stitching/stitchFrames.d.ts +55 -0
- package/dist/stitching/stitchFrames.js +56 -0
- package/dist/stitching/stitchVideo.d.ts +119 -0
- package/dist/stitching/stitchVideo.js +57 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
- package/dist/stitching/useIncrementalJSDriver.js +199 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
- package/dist/stitching/useIncrementalStitcher.js +172 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +15 -0
- package/ios/Package.swift +72 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
- package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
- package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
- package/package.json +73 -0
- package/react-native.config.js +34 -0
- package/scripts/opencv-version.txt +1 -0
- package/scripts/postinstall-fetch-binaries.js +286 -0
- package/src/ar/useARSession.ts +210 -0
- package/src/camera/.gitkeep +0 -0
- package/src/camera/ARCameraView.tsx +256 -0
- package/src/camera/Camera.tsx +1053 -0
- package/src/camera/CameraShutter.tsx +292 -0
- package/src/camera/CameraView.tsx +157 -0
- package/src/camera/CaptureControlsBar.tsx +204 -0
- package/src/camera/CaptureHeader.tsx +184 -0
- package/src/camera/CapturePreview.tsx +318 -0
- package/src/camera/CaptureStatusOverlay.tsx +391 -0
- package/src/camera/CaptureThumbnailStrip.tsx +277 -0
- package/src/camera/IncrementalPanGuide.tsx +328 -0
- package/src/camera/PanoramaBandOverlay.tsx +498 -0
- package/src/camera/PanoramaConfirmModal.tsx +206 -0
- package/src/camera/PanoramaGuidance.tsx +327 -0
- package/src/camera/PanoramaSettingsModal.tsx +1357 -0
- package/src/camera/ViewportCropOverlay.tsx +81 -0
- package/src/camera/useCapture.ts +279 -0
- package/src/camera/useDeviceOrientation.ts +140 -0
- package/src/camera/useVideoCapture.ts +236 -0
- package/src/index.ts +53 -0
- package/src/quality/.gitkeep +0 -0
- package/src/quality/normaliseOrientation.ts +79 -0
- package/src/quality/runQualityCheck.ts +131 -0
- package/src/sensors/useIMUTranslationGate.ts +347 -0
- package/src/stitching/.gitkeep +0 -0
- package/src/stitching/IncrementalStitcherView.tsx +198 -0
- package/src/stitching/incremental.ts +1021 -0
- package/src/stitching/stitchFrames.ts +88 -0
- package/src/stitching/stitchVideo.ts +153 -0
- package/src/stitching/useIncrementalJSDriver.ts +273 -0
- package/src/stitching/useIncrementalStitcher.ts +252 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// OpenCVKeyframeCollector — V16 Phase 1 helper that accumulates the
|
|
4
|
+
// AR-keyframe-gate's accepted CVPixelBuffers as on-disk JPEGs while
|
|
5
|
+
// the user pans, then hands the path list off to OpenCVStitcher's
|
|
6
|
+
// `stitchKeyframePaths:withPoses:` on shutter release.
|
|
7
|
+
//
|
|
8
|
+
// Why a separate class:
|
|
9
|
+
// - CVPixelBuffer → cv::Mat → cv::imwrite has to live in ObjC++ /
|
|
10
|
+
// OpenCV-aware code. IncrementalStitcher.swift can't
|
|
11
|
+
// call it directly.
|
|
12
|
+
// - The frame collection state (session dir, accepted-frame
|
|
13
|
+
// counter) is small and capture-scoped; isolating it from the
|
|
14
|
+
// much larger OpenCVStitcher class file keeps the surface
|
|
15
|
+
// small.
|
|
16
|
+
// - When KLT/multi-band incremental work lands later (Phase 3 LHF
|
|
17
|
+
// #2), this collector becomes the natural seam between the
|
|
18
|
+
// "frames are arriving live" path and the "stitch them now"
|
|
19
|
+
// path; centralising it now pays back later.
|
|
20
|
+
|
|
21
|
+
#import <Foundation/Foundation.h>
|
|
22
|
+
#import <CoreVideo/CoreVideo.h>
|
|
23
|
+
|
|
24
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
25
|
+
|
|
26
|
+
/// Each saved keyframe ends up with a JPEG path + the index it was
|
|
27
|
+
/// saved at + the on-disk size. Returned from `saveKeyframe:…` so
|
|
28
|
+
/// the host can build the path/pose list for `stitchKeyframePaths:`.
|
|
29
|
+
@interface OpenCVKeyframeRecord : NSObject
|
|
30
|
+
@property (nonatomic, copy, readonly) NSString *path;
|
|
31
|
+
@property (nonatomic, assign, readonly) NSInteger index;
|
|
32
|
+
@property (nonatomic, assign, readonly) NSInteger width;
|
|
33
|
+
@property (nonatomic, assign, readonly) NSInteger height;
|
|
34
|
+
- (instancetype)initWithPath:(NSString *)path
|
|
35
|
+
index:(NSInteger)index
|
|
36
|
+
width:(NSInteger)width
|
|
37
|
+
height:(NSInteger)height NS_DESIGNATED_INITIALIZER;
|
|
38
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
39
|
+
@end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@interface OpenCVKeyframeCollector : NSObject
|
|
43
|
+
|
|
44
|
+
/// Active session directory under
|
|
45
|
+
/// `Library/AppSupport/Captures/{capture-uuid}/`. Persisted (NOT
|
|
46
|
+
/// NSTemporaryDirectory) so the operator / debug menu can re-process
|
|
47
|
+
/// captures later. Caller is responsible for `cleanup` when no
|
|
48
|
+
/// longer needed.
|
|
49
|
+
@property (nonatomic, copy, readonly) NSString *sessionDir;
|
|
50
|
+
|
|
51
|
+
/// Total keyframes saved so far.
|
|
52
|
+
@property (nonatomic, assign, readonly) NSInteger acceptedCount;
|
|
53
|
+
|
|
54
|
+
/// Create a new collector with a freshly-minted session directory
|
|
55
|
+
/// under `Library/AppSupport/Captures/{NSUUID}/`. Returns nil if
|
|
56
|
+
/// the directory couldn't be created (out-of-space etc.) and
|
|
57
|
+
/// populates `error`. Imported into Swift as
|
|
58
|
+
/// `try OpenCVKeyframeCollector()`.
|
|
59
|
+
- (nullable instancetype)initWithError:(NSError **)error NS_DESIGNATED_INITIALIZER;
|
|
60
|
+
/// Plain `init` is forwarded to `initWithError:` with a discarded
|
|
61
|
+
/// error so Swift's `try Type()` translation works without colliding
|
|
62
|
+
/// with NS_UNAVAILABLE machinery. Don't call from ObjC — use the
|
|
63
|
+
/// throwing initializer.
|
|
64
|
+
- (nullable instancetype)init;
|
|
65
|
+
|
|
66
|
+
/// Save one accepted ARFrame's pixel buffer as a JPEG inside the
|
|
67
|
+
/// session directory. Filename is `keyframe-{index zero-padded}.jpg`.
|
|
68
|
+
/// Pixel buffer format must be NV12 (the ARFrame default) or BGRA;
|
|
69
|
+
/// other formats fail with NSError code 1200.
|
|
70
|
+
///
|
|
71
|
+
/// `rotationDegrees`: 0/90/180/270. The buffer is PHYSICALLY
|
|
72
|
+
/// rotated by this amount before encoding. Use 0 for batch-keyframe
|
|
73
|
+
/// (the stitcher's intrinsics describe the unrotated landscape
|
|
74
|
+
/// sensor; rotating breaks the camera-K-matrix contract).
|
|
75
|
+
///
|
|
76
|
+
/// `exifOrientation`: standard EXIF Orientation tag value (1..8).
|
|
77
|
+
/// 1 = no rotation; 6 = 90° CW for display; 3 = 180°; 8 = 90° CCW.
|
|
78
|
+
/// Saved as JPEG metadata via ImageIO. iOS Image renderers (RN's
|
|
79
|
+
/// `<Image>`, Files.app, Photos) honour this and display the photo
|
|
80
|
+
/// rotated for natural viewing. cv::imread (when called with
|
|
81
|
+
/// IMREAD_IGNORE_ORIENTATION) returns raw landscape pixels — match
|
|
82
|
+
/// for the stitcher's intrinsics.
|
|
83
|
+
- (nullable OpenCVKeyframeRecord *)saveKeyframe:(CVPixelBufferRef)pixelBuffer
|
|
84
|
+
rotationDegrees:(NSInteger)rotationDegrees
|
|
85
|
+
exifOrientation:(NSInteger)exifOrientation
|
|
86
|
+
jpegQuality:(NSInteger)jpegQuality
|
|
87
|
+
error:(NSError **)error;
|
|
88
|
+
|
|
89
|
+
/// Remove the session directory and any saved keyframes. Idempotent.
|
|
90
|
+
/// Called from IncrementalStitcher's `cancel` / on
|
|
91
|
+
/// successful finalize when the operator hasn't opted into
|
|
92
|
+
/// "keep-for-reprocess" mode.
|
|
93
|
+
- (void)cleanup;
|
|
94
|
+
|
|
95
|
+
@end
|
|
96
|
+
|
|
97
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
#import "OpenCVKeyframeCollector.h"
|
|
4
|
+
#import <ImageIO/ImageIO.h>
|
|
5
|
+
#import <CoreServices/CoreServices.h>
|
|
6
|
+
|
|
7
|
+
// Same #pragma dance the other ObjC++ stitcher files use to suppress
|
|
8
|
+
// noisy header warnings before importing opencv2.
|
|
9
|
+
#pragma push_macro("NO")
|
|
10
|
+
#undef NO
|
|
11
|
+
#include <opencv2/opencv.hpp>
|
|
12
|
+
#include <opencv2/imgcodecs.hpp>
|
|
13
|
+
#pragma pop_macro("NO")
|
|
14
|
+
|
|
15
|
+
// V16 Phase 1.fix2 — write a JPEG with an EXIF Orientation tag so
|
|
16
|
+
// iOS image renderers display the saved frame correctly while
|
|
17
|
+
// cv::imread (with IMREAD_IGNORE_ORIENTATION) gets raw landscape
|
|
18
|
+
// pixels for the stitcher. Returns YES on success.
|
|
19
|
+
static BOOL WriteJPEGWithEXIF(const cv::Mat &bgr,
|
|
20
|
+
NSString *path,
|
|
21
|
+
NSInteger exifOrientation,
|
|
22
|
+
NSInteger quality) {
|
|
23
|
+
if (bgr.empty()) return NO;
|
|
24
|
+
|
|
25
|
+
// Convert BGR (OpenCV native) → RGBA (CoreGraphics expects).
|
|
26
|
+
cv::Mat rgba;
|
|
27
|
+
cv::cvtColor(bgr, rgba, cv::COLOR_BGR2RGBA);
|
|
28
|
+
|
|
29
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
30
|
+
CGBitmapInfo bitmapInfo =
|
|
31
|
+
kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
|
|
32
|
+
CGContextRef ctx = CGBitmapContextCreate(
|
|
33
|
+
rgba.data,
|
|
34
|
+
(size_t)rgba.cols,
|
|
35
|
+
(size_t)rgba.rows,
|
|
36
|
+
8,
|
|
37
|
+
(size_t)rgba.step,
|
|
38
|
+
colorSpace,
|
|
39
|
+
bitmapInfo);
|
|
40
|
+
if (!ctx) {
|
|
41
|
+
CGColorSpaceRelease(colorSpace);
|
|
42
|
+
return NO;
|
|
43
|
+
}
|
|
44
|
+
CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
|
|
45
|
+
CGContextRelease(ctx);
|
|
46
|
+
CGColorSpaceRelease(colorSpace);
|
|
47
|
+
if (!cgImage) return NO;
|
|
48
|
+
|
|
49
|
+
NSURL *url = [NSURL fileURLWithPath:path];
|
|
50
|
+
CGImageDestinationRef dst = CGImageDestinationCreateWithURL(
|
|
51
|
+
(__bridge CFURLRef)url,
|
|
52
|
+
// public.jpeg is the stable UTI for JPEG. Avoiding kUTTypeJPEG
|
|
53
|
+
// (deprecated) and the iOS-15+ UTType class so this compiles
|
|
54
|
+
// against older deployment targets too.
|
|
55
|
+
CFSTR("public.jpeg"),
|
|
56
|
+
1,
|
|
57
|
+
NULL);
|
|
58
|
+
if (!dst) {
|
|
59
|
+
CGImageRelease(cgImage);
|
|
60
|
+
return NO;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
NSInteger q = MAX(0, MIN(100, quality));
|
|
64
|
+
// Clamp EXIF orientation to the valid range (1..8). Default to
|
|
65
|
+
// 1 (no rotation) for unrecognised values.
|
|
66
|
+
NSInteger exif = (exifOrientation >= 1 && exifOrientation <= 8)
|
|
67
|
+
? exifOrientation : 1;
|
|
68
|
+
NSDictionary *props = @{
|
|
69
|
+
(id)kCGImageDestinationLossyCompressionQuality: @((double)q / 100.0),
|
|
70
|
+
(id)kCGImagePropertyOrientation: @(exif),
|
|
71
|
+
};
|
|
72
|
+
CGImageDestinationAddImage(
|
|
73
|
+
dst, cgImage, (__bridge CFDictionaryRef)props);
|
|
74
|
+
BOOL ok = CGImageDestinationFinalize(dst);
|
|
75
|
+
CFRelease(dst);
|
|
76
|
+
CGImageRelease(cgImage);
|
|
77
|
+
return ok;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
82
|
+
// Record
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
@implementation OpenCVKeyframeRecord
|
|
86
|
+
- (instancetype)initWithPath:(NSString *)path
|
|
87
|
+
index:(NSInteger)index
|
|
88
|
+
width:(NSInteger)width
|
|
89
|
+
height:(NSInteger)height {
|
|
90
|
+
if ((self = [super init])) {
|
|
91
|
+
_path = [path copy];
|
|
92
|
+
_index = index;
|
|
93
|
+
_width = width;
|
|
94
|
+
_height = height;
|
|
95
|
+
}
|
|
96
|
+
return self;
|
|
97
|
+
}
|
|
98
|
+
@end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
102
|
+
// Collector
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
@interface OpenCVKeyframeCollector ()
|
|
106
|
+
@property (nonatomic, copy, readwrite) NSString *sessionDir;
|
|
107
|
+
@property (nonatomic, assign, readwrite) NSInteger acceptedCount;
|
|
108
|
+
@end
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@implementation OpenCVKeyframeCollector
|
|
112
|
+
|
|
113
|
+
- (nullable instancetype)init {
|
|
114
|
+
// Forward to the throwing init with a discarded error. Used only
|
|
115
|
+
// when Swift's `try Type()` form chooses the no-error path; calls
|
|
116
|
+
// from ObjC should always use `initWithError:` directly.
|
|
117
|
+
return [self initWithError:nil];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
- (nullable instancetype)initWithError:(NSError **)error {
|
|
121
|
+
if ((self = [super init])) {
|
|
122
|
+
NSURL *appSupport = [[NSFileManager defaultManager]
|
|
123
|
+
URLForDirectory:NSApplicationSupportDirectory
|
|
124
|
+
inDomain:NSUserDomainMask
|
|
125
|
+
appropriateForURL:nil
|
|
126
|
+
create:YES
|
|
127
|
+
error:error];
|
|
128
|
+
if (!appSupport) return nil;
|
|
129
|
+
NSString *captureUUID = [[NSUUID UUID] UUIDString];
|
|
130
|
+
NSString *sessionPath =
|
|
131
|
+
[[appSupport.path stringByAppendingPathComponent:@"Captures"]
|
|
132
|
+
stringByAppendingPathComponent:captureUUID];
|
|
133
|
+
BOOL ok = [[NSFileManager defaultManager]
|
|
134
|
+
createDirectoryAtPath:sessionPath
|
|
135
|
+
withIntermediateDirectories:YES
|
|
136
|
+
attributes:nil
|
|
137
|
+
error:error];
|
|
138
|
+
if (!ok) return nil;
|
|
139
|
+
_sessionDir = [sessionPath copy];
|
|
140
|
+
_acceptedCount = 0;
|
|
141
|
+
NSLog(@"[KeyframeCollector] session dir: %@", _sessionDir);
|
|
142
|
+
}
|
|
143
|
+
return self;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
- (nullable OpenCVKeyframeRecord *)saveKeyframe:(CVPixelBufferRef)pixelBuffer
|
|
147
|
+
rotationDegrees:(NSInteger)rotationDegrees
|
|
148
|
+
exifOrientation:(NSInteger)exifOrientation
|
|
149
|
+
jpegQuality:(NSInteger)jpegQuality
|
|
150
|
+
error:(NSError **)error {
|
|
151
|
+
if (!pixelBuffer) {
|
|
152
|
+
if (error) {
|
|
153
|
+
*error = [NSError errorWithDomain:@"OpenCVKeyframeCollector"
|
|
154
|
+
code:1200
|
|
155
|
+
userInfo:@{
|
|
156
|
+
NSLocalizedDescriptionKey: @"Nil pixelBuffer.",
|
|
157
|
+
}];
|
|
158
|
+
}
|
|
159
|
+
return nil;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cv::Mat bgr;
|
|
163
|
+
if (![self convertPixelBuffer:pixelBuffer toMat:bgr]) {
|
|
164
|
+
if (error) {
|
|
165
|
+
*error = [NSError errorWithDomain:@"OpenCVKeyframeCollector"
|
|
166
|
+
code:1201
|
|
167
|
+
userInfo:@{
|
|
168
|
+
NSLocalizedDescriptionKey:
|
|
169
|
+
@"Unsupported pixel-buffer format (need NV12 or BGRA).",
|
|
170
|
+
}];
|
|
171
|
+
}
|
|
172
|
+
return nil;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Rotate to caller-requested orientation. The JPEGs are saved in
|
|
176
|
+
// the orientation the stitcher expects (user-pan orientation), so
|
|
177
|
+
// OpenCVStitcher.stitchKeyframePaths can read them with no further
|
|
178
|
+
// rotation work.
|
|
179
|
+
cv::Mat rotated;
|
|
180
|
+
if (rotationDegrees == 90) {
|
|
181
|
+
cv::rotate(bgr, rotated, cv::ROTATE_90_CLOCKWISE);
|
|
182
|
+
bgr.release();
|
|
183
|
+
} else if (rotationDegrees == 180) {
|
|
184
|
+
cv::rotate(bgr, rotated, cv::ROTATE_180);
|
|
185
|
+
bgr.release();
|
|
186
|
+
} else if (rotationDegrees == 270) {
|
|
187
|
+
cv::rotate(bgr, rotated, cv::ROTATE_90_COUNTERCLOCKWISE);
|
|
188
|
+
bgr.release();
|
|
189
|
+
} else {
|
|
190
|
+
rotated = bgr;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
NSInteger idx = self.acceptedCount;
|
|
194
|
+
NSString *filename =
|
|
195
|
+
[NSString stringWithFormat:@"keyframe-%03ld.jpg", (long)idx];
|
|
196
|
+
NSString *fullPath =
|
|
197
|
+
[self.sessionDir stringByAppendingPathComponent:filename];
|
|
198
|
+
|
|
199
|
+
// V16 Phase 1.fix2 — write JPEG via ImageIO so we can set the
|
|
200
|
+
// EXIF Orientation tag. cv::imwrite doesn't support EXIF; iOS
|
|
201
|
+
// image renderers (RN's <Image>, Files.app) need the tag to display
|
|
202
|
+
// the saved landscape-sensor JPEG correctly when the user is
|
|
203
|
+
// holding the phone in portrait (which puts the sensor's natural
|
|
204
|
+
// long axis vertical to user — making un-tagged thumbnails appear
|
|
205
|
+
// sideways).
|
|
206
|
+
BOOL wrote = WriteJPEGWithEXIF(rotated, fullPath, exifOrientation,
|
|
207
|
+
jpegQuality);
|
|
208
|
+
if (!wrote) {
|
|
209
|
+
if (error) {
|
|
210
|
+
*error = [NSError errorWithDomain:@"OpenCVKeyframeCollector"
|
|
211
|
+
code:1202
|
|
212
|
+
userInfo:@{
|
|
213
|
+
NSLocalizedDescriptionKey:
|
|
214
|
+
[NSString stringWithFormat:
|
|
215
|
+
@"WriteJPEGWithEXIF failed for %@ (orient=%ld q=%ld)",
|
|
216
|
+
fullPath, (long)exifOrientation, (long)jpegQuality],
|
|
217
|
+
}];
|
|
218
|
+
}
|
|
219
|
+
return nil;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
self.acceptedCount = idx + 1;
|
|
223
|
+
NSInteger w = rotated.cols, h = rotated.rows;
|
|
224
|
+
rotated.release();
|
|
225
|
+
return [[OpenCVKeyframeRecord alloc] initWithPath:fullPath
|
|
226
|
+
index:idx
|
|
227
|
+
width:w
|
|
228
|
+
height:h];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
- (void)cleanup {
|
|
232
|
+
if (self.sessionDir.length == 0) return;
|
|
233
|
+
[[NSFileManager defaultManager] removeItemAtPath:self.sessionDir
|
|
234
|
+
error:nil];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── CVPixelBuffer → cv::Mat (BGR) ──────────────────────────────────
|
|
238
|
+
//
|
|
239
|
+
// Mirrors `OpenCVIncrementalStitcher.convertPixelBuffer:toMat:` but
|
|
240
|
+
// kept inline here so this file is self-contained. Supports the two
|
|
241
|
+
// pixel formats ARFrame.capturedImage uses on iOS (NV12 by default;
|
|
242
|
+
// BGRA when the AR session is configured for it). Lock-once, copy
|
|
243
|
+
// out, unlock — buffer lifetime ends with the caller's accept frame.
|
|
244
|
+
- (BOOL)convertPixelBuffer:(CVPixelBufferRef)pixelBuffer
|
|
245
|
+
toMat:(cv::Mat &)outBGR {
|
|
246
|
+
OSType pf = CVPixelBufferGetPixelFormatType(pixelBuffer);
|
|
247
|
+
CVReturn lockResult =
|
|
248
|
+
CVPixelBufferLockBaseAddress(pixelBuffer,
|
|
249
|
+
kCVPixelBufferLock_ReadOnly);
|
|
250
|
+
if (lockResult != kCVReturnSuccess) return NO;
|
|
251
|
+
|
|
252
|
+
BOOL ok = NO;
|
|
253
|
+
@try {
|
|
254
|
+
if (pf == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ||
|
|
255
|
+
pf == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
|
|
256
|
+
// NV12 — Y plane (full res) + interleaved CbCr (half res).
|
|
257
|
+
size_t w = CVPixelBufferGetWidth(pixelBuffer);
|
|
258
|
+
size_t h = CVPixelBufferGetHeight(pixelBuffer);
|
|
259
|
+
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
|
260
|
+
size_t cStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
|
261
|
+
uint8_t *yPlane =
|
|
262
|
+
(uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
|
263
|
+
uint8_t *cPlane =
|
|
264
|
+
(uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
|
265
|
+
|
|
266
|
+
cv::Mat nv12((int)(h * 3 / 2), (int)w, CV_8UC1);
|
|
267
|
+
// Copy Y plane row-by-row (strides may differ from width).
|
|
268
|
+
for (size_t r = 0; r < h; r++) {
|
|
269
|
+
memcpy(nv12.ptr((int)r), yPlane + r * yStride, w);
|
|
270
|
+
}
|
|
271
|
+
// Copy CbCr (interleaved) — height is h/2.
|
|
272
|
+
for (size_t r = 0; r < h / 2; r++) {
|
|
273
|
+
memcpy(nv12.ptr((int)(h + r)), cPlane + r * cStride, w);
|
|
274
|
+
}
|
|
275
|
+
cv::cvtColor(nv12, outBGR, cv::COLOR_YUV2BGR_NV12);
|
|
276
|
+
ok = YES;
|
|
277
|
+
} else if (pf == kCVPixelFormatType_32BGRA) {
|
|
278
|
+
size_t w = CVPixelBufferGetWidth(pixelBuffer);
|
|
279
|
+
size_t h = CVPixelBufferGetHeight(pixelBuffer);
|
|
280
|
+
size_t stride = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
281
|
+
uint8_t *base = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
|
|
282
|
+
// Wrap (no copy), then convert into outBGR (which IS owned).
|
|
283
|
+
cv::Mat bgra((int)h, (int)w, CV_8UC4, base, stride);
|
|
284
|
+
cv::cvtColor(bgra, outBGR, cv::COLOR_BGRA2BGR);
|
|
285
|
+
ok = YES;
|
|
286
|
+
}
|
|
287
|
+
} @catch (NSException *e) {
|
|
288
|
+
NSLog(@"[KeyframeCollector] convertPixelBuffer exception: %@", e);
|
|
289
|
+
ok = NO;
|
|
290
|
+
}
|
|
291
|
+
CVPixelBufferUnlockBaseAddress(pixelBuffer,
|
|
292
|
+
kCVPixelBufferLock_ReadOnly);
|
|
293
|
+
return ok;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// OpenCVFirstWinsCylindricalStitcher.h
|
|
4
|
+
//
|
|
5
|
+
// Apple-style slit-scan panorama engine. Alternative to
|
|
6
|
+
// OpenCVIncrementalStitcher (which is the Samsung-style hybrid
|
|
7
|
+
// frame-based approach). Both engines expose the same JS-facing
|
|
8
|
+
// API contract; the host picks one via the `engine` flag in start
|
|
9
|
+
// options.
|
|
10
|
+
//
|
|
11
|
+
// What slit-scan does differently:
|
|
12
|
+
//
|
|
13
|
+
// Hybrid (v9): accepts WHOLE frames at intervals, warps each via
|
|
14
|
+
// cylindrical projection, blends with feather over a substantial
|
|
15
|
+
// overlap region (~30-50% of frame width).
|
|
16
|
+
//
|
|
17
|
+
// Slit-scan (v10): continuously samples the camera buffer. For
|
|
18
|
+
// each AR frame, takes a NARROW VERTICAL STRIP (typically 30-60
|
|
19
|
+
// pixels) whose width tracks the gyro angular delta since the
|
|
20
|
+
// last strip. Strips are painted onto the cylindrical canvas at
|
|
21
|
+
// their exact angular positions, so the per-strip overlap is just
|
|
22
|
+
// 1-3 pixels. The "stitching" problem mostly disappears because
|
|
23
|
+
// the overlap region is too narrow to show parallax.
|
|
24
|
+
//
|
|
25
|
+
// Why both engines:
|
|
26
|
+
//
|
|
27
|
+
// Slit-scan produces near-perfect output for clean rotational
|
|
28
|
+
// pans (Apple Camera-app quality) but is sensitive to gyro drift
|
|
29
|
+
// on long pans and to non-rotational motion (operator translates
|
|
30
|
+
// their body slightly). Hybrid is more forgiving but has visible
|
|
31
|
+
// seams where alignment is imperfect. Field captures decide which
|
|
32
|
+
// wins for the actual gesture our reps use.
|
|
33
|
+
//
|
|
34
|
+
|
|
35
|
+
#import <Foundation/Foundation.h>
|
|
36
|
+
#import <CoreVideo/CoreVideo.h>
|
|
37
|
+
#import "OpenCVIncrementalStitcher.h" // RLISFrameOutcome, RLISFrameTelemetry, RLISSnapshot
|
|
38
|
+
|
|
39
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
40
|
+
|
|
41
|
+
@interface OpenCVFirstWinsCylindricalStitcher : NSObject
|
|
42
|
+
|
|
43
|
+
- (instancetype)initWithComposeWidth:(NSInteger)composeWidth
|
|
44
|
+
composeHeight:(NSInteger)composeHeight
|
|
45
|
+
canvasWidth:(NSInteger)canvasWidth
|
|
46
|
+
canvasHeight:(NSInteger)canvasHeight
|
|
47
|
+
featherPx:(NSInteger)featherPx
|
|
48
|
+
frameRotationDegrees:(NSInteger)frameRotationDegrees
|
|
49
|
+
useRectilinear:(BOOL)useRectilinear NS_DESIGNATED_INITIALIZER;
|
|
50
|
+
|
|
51
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
52
|
+
|
|
53
|
+
/// V15 — set the per-stage correction config. Should be called once
|
|
54
|
+
/// after init, before any `ingestPixelBuffer:` call. Subsequent calls
|
|
55
|
+
/// replace the config but mid-capture changes can produce inconsistent
|
|
56
|
+
/// state (prev features tracked under different settings). Best
|
|
57
|
+
/// practice: reset the engine when config changes. If never called,
|
|
58
|
+
/// the engine uses a default equivalent to
|
|
59
|
+
/// `+[RLISStitcherConfig configForMode:@"slitscan-both"]`.
|
|
60
|
+
- (void)setConfig:(RLISStitcherConfig *)config;
|
|
61
|
+
|
|
62
|
+
/// V15.0b — set the world-frame plane transform (4×4, column-major,
|
|
63
|
+
/// 16 floats). Should be called once a vertical plane has been
|
|
64
|
+
/// detected by ARKit and before any subsequent ingest. If never
|
|
65
|
+
/// called, the engine ignores `_config.useDetectedPlane` and falls
|
|
66
|
+
/// back to pose-driven projection.
|
|
67
|
+
- (void)setPlaneTransformFlat:(NSArray<NSNumber *> *)transform16;
|
|
68
|
+
|
|
69
|
+
- (RLISFrameTelemetry *)ingestPixelBuffer:(CVPixelBufferRef)pixelBuffer
|
|
70
|
+
qx:(double)qx
|
|
71
|
+
qy:(double)qy
|
|
72
|
+
qz:(double)qz
|
|
73
|
+
qw:(double)qw
|
|
74
|
+
tx:(double)tx
|
|
75
|
+
ty:(double)ty
|
|
76
|
+
tz:(double)tz
|
|
77
|
+
fx:(double)fx
|
|
78
|
+
fy:(double)fy
|
|
79
|
+
cx:(double)cx
|
|
80
|
+
cy:(double)cy
|
|
81
|
+
imageWidth:(NSInteger)imageWidth
|
|
82
|
+
imageHeight:(NSInteger)imageHeight
|
|
83
|
+
yaw:(double)yaw
|
|
84
|
+
pitch:(double)pitch
|
|
85
|
+
fovHorizDegrees:(double)fovHorizDegrees
|
|
86
|
+
fovVertDegrees:(double)fovVertDegrees
|
|
87
|
+
trackingPoor:(BOOL)trackingPoor
|
|
88
|
+
NS_SWIFT_NAME(ingest(pixelBuffer:qx:qy:qz:qw:tx:ty:tz:fx:fy:cx:cy:imageWidth:imageHeight:yaw:pitch:fovHorizDegrees:fovVertDegrees:trackingPoor:));
|
|
89
|
+
|
|
90
|
+
- (nullable RLISSnapshot *)snapshotWithJpegQuality:(NSInteger)quality
|
|
91
|
+
error:(NSError **)error;
|
|
92
|
+
|
|
93
|
+
- (nullable RLISSnapshot *)finalizeAtPath:(NSString *)outputPath
|
|
94
|
+
jpegQuality:(NSInteger)quality
|
|
95
|
+
error:(NSError **)error;
|
|
96
|
+
|
|
97
|
+
- (void)reset;
|
|
98
|
+
|
|
99
|
+
@property (nonatomic, readonly) NSInteger acceptedCount;
|
|
100
|
+
|
|
101
|
+
@end
|
|
102
|
+
|
|
103
|
+
NS_ASSUME_NONNULL_END
|