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.
Files changed (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. 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