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,252 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// QualityChecker.swift
|
|
3
|
+
//
|
|
4
|
+
// Pure-Swift implementations of the on-device quality scores the SDK
|
|
5
|
+
// surfaces to the JS layer. No React Native dependency in this file —
|
|
6
|
+
// the bridge wrapper lives in `QualityCheckerBridge.swift` and only
|
|
7
|
+
// translates promise resolves into the same numeric scores this file
|
|
8
|
+
// produces. Keeping the bridge thin means XCTest hits the algorithms
|
|
9
|
+
// directly (via SwiftPM `swift test`) instead of needing an iOS
|
|
10
|
+
// simulator + RN runtime to validate a Laplacian variance.
|
|
11
|
+
//
|
|
12
|
+
// Why this layer at all (instead of OpenCV)?
|
|
13
|
+
// Phase-1 quality checks (blur + brightness) are a tiny slice of
|
|
14
|
+
// what OpenCV does and Apple already ships the primitives in the OS
|
|
15
|
+
// (Accelerate's vImage convolutions, CoreImage's CIAreaAverage).
|
|
16
|
+
// Pulling in opencv-mobile only for two filters would add ~10 MB to
|
|
17
|
+
// the IPA for no functional gain — opencv-mobile gets paid for in
|
|
18
|
+
// Phase 2 when stitching arrives, where Apple has no equivalent.
|
|
19
|
+
//
|
|
20
|
+
// Algorithm references:
|
|
21
|
+
// * Blur: variance-of-Laplacian. Pech-Pacheco et al. 2000,
|
|
22
|
+
// "Diatom autofocusing in brightfield microscopy: a comparative
|
|
23
|
+
// study." Threshold ~50–100 separates "soft" from "blurry" for
|
|
24
|
+
// mobile shelf imagery in our pilot data.
|
|
25
|
+
// * Brightness: mean luminance. Underexposed = mean < 60,
|
|
26
|
+
// overexposed = mean > 200, both unusable for downstream OCR.
|
|
27
|
+
|
|
28
|
+
import Accelerate
|
|
29
|
+
import CoreGraphics
|
|
30
|
+
import CoreImage
|
|
31
|
+
import Foundation
|
|
32
|
+
|
|
33
|
+
/// Errors the quality check can surface to the bridge. Each maps to a
|
|
34
|
+
/// dedicated reject-code on the JS side so the host app can branch on
|
|
35
|
+
/// "missing file" vs. "couldn't decode" vs. "internal".
|
|
36
|
+
public enum QualityCheckError: Error {
|
|
37
|
+
case fileNotFound(path: String)
|
|
38
|
+
case imageDecodeFailed(path: String)
|
|
39
|
+
case bufferAllocationFailed
|
|
40
|
+
case convolutionFailed(vImageError: vImage_Error)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Numeric quality measurements; mirrors the QualityReport TS shape.
|
|
44
|
+
public struct QualityScores: Equatable {
|
|
45
|
+
/// Variance of the Laplacian. Higher = sharper. Implementation
|
|
46
|
+
/// returns a non-negative Double; +∞ is reserved for the JS shim
|
|
47
|
+
/// fallback so production code can distinguish "we measured this
|
|
48
|
+
/// and it's astonishingly sharp" from "we never measured it."
|
|
49
|
+
public let blurScore: Double
|
|
50
|
+
|
|
51
|
+
/// Mean luminance in [0, 255]. 255 = pure white, 0 = pure black.
|
|
52
|
+
/// We use the standard ITU-R BT.601 luma weights (0.299 R + 0.587 G +
|
|
53
|
+
/// 0.114 B) — vImage's RGB-to-Y conversion uses the same weights so
|
|
54
|
+
/// keeping them here means the mean we compute matches the mean
|
|
55
|
+
/// vImage would produce had we asked it directly.
|
|
56
|
+
public let brightnessScore: Double
|
|
57
|
+
|
|
58
|
+
public init(blurScore: Double, brightnessScore: Double) {
|
|
59
|
+
self.blurScore = blurScore
|
|
60
|
+
self.brightnessScore = brightnessScore
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public enum QualityChecker {
|
|
65
|
+
|
|
66
|
+
/// Measure both blur and brightness in one decode pass.
|
|
67
|
+
///
|
|
68
|
+
/// Call sites that only need one score (rare — both are cheap once
|
|
69
|
+
/// the image is decoded) can ignore the unused field. Bundling the
|
|
70
|
+
/// API means we decode + convert-to-grayscale exactly once per call,
|
|
71
|
+
/// which is the bulk of the work; reading the grayscale buffer twice
|
|
72
|
+
/// to extract the two stats is essentially free.
|
|
73
|
+
public static func measure(imagePath: String) throws -> QualityScores {
|
|
74
|
+
let cgImage = try decodeImage(at: imagePath)
|
|
75
|
+
let grayBuffer = try makeGrayscaleBuffer(from: cgImage)
|
|
76
|
+
defer { grayBuffer.free() }
|
|
77
|
+
|
|
78
|
+
let brightness = grayBuffer.mean()
|
|
79
|
+
let blur = try grayBuffer.varianceOfLaplacian()
|
|
80
|
+
return QualityScores(blurScore: blur, brightnessScore: brightness)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Convenience: measure blur only. Provided so unit tests can call
|
|
84
|
+
/// the algorithm in isolation without invoking the brightness path.
|
|
85
|
+
public static func measureBlurScore(imagePath: String) throws -> Double {
|
|
86
|
+
return try measure(imagePath: imagePath).blurScore
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Convenience: measure brightness only.
|
|
90
|
+
public static func measureBrightness(imagePath: String) throws -> Double {
|
|
91
|
+
return try measure(imagePath: imagePath).brightnessScore
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MARK: - Internal helpers
|
|
95
|
+
|
|
96
|
+
/// Decode the file at `imagePath` into a CGImage. Strips any
|
|
97
|
+
/// `file://` prefix the bridge may have passed through unchanged so
|
|
98
|
+
/// callers don't have to remember whether the SDK wants a URL or a
|
|
99
|
+
/// path.
|
|
100
|
+
private static func decodeImage(at imagePath: String) throws -> CGImage {
|
|
101
|
+
let cleaned = imagePath.hasPrefix("file://")
|
|
102
|
+
? String(imagePath.dropFirst("file://".count))
|
|
103
|
+
: imagePath
|
|
104
|
+
guard FileManager.default.fileExists(atPath: cleaned) else {
|
|
105
|
+
throw QualityCheckError.fileNotFound(path: imagePath)
|
|
106
|
+
}
|
|
107
|
+
let url = URL(fileURLWithPath: cleaned)
|
|
108
|
+
guard
|
|
109
|
+
let source = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
110
|
+
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil)
|
|
111
|
+
else {
|
|
112
|
+
throw QualityCheckError.imageDecodeFailed(path: imagePath)
|
|
113
|
+
}
|
|
114
|
+
return cgImage
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Convert a CGImage to an 8-bit single-channel grayscale buffer.
|
|
118
|
+
///
|
|
119
|
+
/// Why vImage instead of CoreImage's CIPhotoEffectMono filter?
|
|
120
|
+
/// CIFilter chains are deferred and lazy — we'd have to render
|
|
121
|
+
/// anyway to read the pixels back out. vImage gives us an actual
|
|
122
|
+
/// contiguous Y' buffer in one shot, which is exactly what the
|
|
123
|
+
/// blur/brightness stats want.
|
|
124
|
+
private static func makeGrayscaleBuffer(from cgImage: CGImage) throws -> GrayscaleBuffer {
|
|
125
|
+
var format = vImage_CGImageFormat(
|
|
126
|
+
bitsPerComponent: 8,
|
|
127
|
+
bitsPerPixel: 8,
|
|
128
|
+
colorSpace: Unmanaged.passRetained(CGColorSpaceCreateDeviceGray()),
|
|
129
|
+
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
|
|
130
|
+
version: 0,
|
|
131
|
+
decode: nil,
|
|
132
|
+
renderingIntent: .defaultIntent
|
|
133
|
+
)
|
|
134
|
+
var buffer = vImage_Buffer()
|
|
135
|
+
let createErr = vImageBuffer_InitWithCGImage(
|
|
136
|
+
&buffer, &format, nil, cgImage, vImage_Flags(kvImageNoFlags)
|
|
137
|
+
)
|
|
138
|
+
guard createErr == kvImageNoError else {
|
|
139
|
+
throw QualityCheckError.bufferAllocationFailed
|
|
140
|
+
}
|
|
141
|
+
return GrayscaleBuffer(buffer: buffer)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// RAII wrapper around vImage_Buffer + the algorithms that read it.
|
|
146
|
+
/// Owns the underlying pixel memory so callers `defer { buf.free() }`
|
|
147
|
+
/// after construction.
|
|
148
|
+
struct GrayscaleBuffer {
|
|
149
|
+
var buffer: vImage_Buffer
|
|
150
|
+
|
|
151
|
+
func free() {
|
|
152
|
+
if buffer.data != nil { Foundation.free(buffer.data) }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Mean pixel value across the buffer, [0, 255]. vImage doesn't
|
|
156
|
+
/// expose a direct mean function for Planar8 so we walk the rows
|
|
157
|
+
/// manually with vDSP_meanv on each row's bytes-as-floats.
|
|
158
|
+
func mean() -> Double {
|
|
159
|
+
let width = Int(buffer.width)
|
|
160
|
+
let height = Int(buffer.height)
|
|
161
|
+
let rowBytes = buffer.rowBytes
|
|
162
|
+
let basePtr = buffer.data.assumingMemoryBound(to: UInt8.self)
|
|
163
|
+
|
|
164
|
+
var total: Double = 0
|
|
165
|
+
var count: Double = 0
|
|
166
|
+
var rowAccumulator = [Float](repeating: 0, count: width)
|
|
167
|
+
for y in 0..<height {
|
|
168
|
+
let rowPtr = basePtr.advanced(by: y * rowBytes)
|
|
169
|
+
// vImageConvert_Planar8toPlanarF would do the type conversion in
|
|
170
|
+
// bulk; for the per-row mean the manual loop is faster than
|
|
171
|
+
// setting up a vImage call per row.
|
|
172
|
+
for x in 0..<width {
|
|
173
|
+
rowAccumulator[x] = Float(rowPtr[x])
|
|
174
|
+
}
|
|
175
|
+
var rowMean: Float = 0
|
|
176
|
+
vDSP_meanv(rowAccumulator, 1, &rowMean, vDSP_Length(width))
|
|
177
|
+
total += Double(rowMean)
|
|
178
|
+
count += 1
|
|
179
|
+
}
|
|
180
|
+
return count > 0 ? total / count : 0
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Variance-of-Laplacian — the canonical "how blurry is this" score.
|
|
184
|
+
///
|
|
185
|
+
/// Steps:
|
|
186
|
+
/// 1. Convolve the grayscale buffer with the discrete Laplacian
|
|
187
|
+
/// kernel. Output = high-frequency / edge response.
|
|
188
|
+
/// 2. Compute the variance of the convolved buffer. Sharp images
|
|
189
|
+
/// have lots of edges with widely varying magnitudes; blurry
|
|
190
|
+
/// images have weak, similar values throughout.
|
|
191
|
+
func varianceOfLaplacian() throws -> Double {
|
|
192
|
+
let width = buffer.width
|
|
193
|
+
let height = buffer.height
|
|
194
|
+
|
|
195
|
+
// Allocate the output buffer. vImageConvolve_Planar8 writes the
|
|
196
|
+
// convolved bytes into a new buffer of the same dimensions.
|
|
197
|
+
var output = vImage_Buffer()
|
|
198
|
+
let outputBytes = Int(height) * Int(width)
|
|
199
|
+
output.data = malloc(outputBytes)
|
|
200
|
+
output.width = width
|
|
201
|
+
output.height = height
|
|
202
|
+
output.rowBytes = Int(width)
|
|
203
|
+
guard output.data != nil else {
|
|
204
|
+
throw QualityCheckError.bufferAllocationFailed
|
|
205
|
+
}
|
|
206
|
+
defer { Foundation.free(output.data) }
|
|
207
|
+
|
|
208
|
+
// Discrete Laplacian. Sums to zero so the convolved buffer's
|
|
209
|
+
// mean is ~0 except where edges live; variance then quantifies
|
|
210
|
+
// edge response — exactly the "is this image sharp" question.
|
|
211
|
+
// 0 -1 0
|
|
212
|
+
// -1 4 -1
|
|
213
|
+
// 0 -1 0
|
|
214
|
+
let kernel: [Int16] = [0, -1, 0, -1, 4, -1, 0, -1, 0]
|
|
215
|
+
let divisor: Int32 = 1
|
|
216
|
+
|
|
217
|
+
// vImageConvolve_Planar8 takes mutable buffer pointers; copy into
|
|
218
|
+
// local vars so we can hand it inout refs without violating Swift's
|
|
219
|
+
// exclusive-access rules on `self.buffer`.
|
|
220
|
+
var input = buffer
|
|
221
|
+
let convErr = kernel.withUnsafeBufferPointer { kernelPtr -> vImage_Error in
|
|
222
|
+
// backgroundColor is unused with kvImageEdgeExtend (we extend
|
|
223
|
+
// edge pixels rather than padding) — pass 0 as a placeholder.
|
|
224
|
+
vImageConvolve_Planar8(
|
|
225
|
+
&input, &output, nil, 0, 0,
|
|
226
|
+
kernelPtr.baseAddress, 3, 3,
|
|
227
|
+
divisor, 0,
|
|
228
|
+
vImage_Flags(kvImageEdgeExtend)
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
guard convErr == kvImageNoError else {
|
|
232
|
+
throw QualityCheckError.convolutionFailed(vImageError: convErr)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Compute variance over the output pixels. vImage gives us the
|
|
236
|
+
// bytes; vDSP gives us the stats. Convert U8 → F32 in one bulk op
|
|
237
|
+
// so vDSP_normalize / vDSP_measqv can run native.
|
|
238
|
+
let count = Int(width) * Int(height)
|
|
239
|
+
var floatBuffer = [Float](repeating: 0, count: count)
|
|
240
|
+
let outBytes = output.data.assumingMemoryBound(to: UInt8.self)
|
|
241
|
+
for i in 0..<count {
|
|
242
|
+
floatBuffer[i] = Float(outBytes[i])
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
var mean: Float = 0
|
|
246
|
+
var meanSquare: Float = 0
|
|
247
|
+
vDSP_meanv(floatBuffer, 1, &mean, vDSP_Length(count))
|
|
248
|
+
vDSP_measqv(floatBuffer, 1, &meanSquare, vDSP_Length(count))
|
|
249
|
+
let variance = Double(meanSquare) - Double(mean) * Double(mean)
|
|
250
|
+
return max(0, variance)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// QualityCheckerBridge.m
|
|
4
|
+
//
|
|
5
|
+
// Obj-C glue that registers the Swift `QualityCheckerBridge`
|
|
6
|
+
// class with the React Native module map. React Native's
|
|
7
|
+
// `RCT_EXTERN_MODULE` and `RCT_EXTERN_METHOD` are C macros — they
|
|
8
|
+
// can't be invoked from Swift directly — so a `.m` shim is required
|
|
9
|
+
// even though the actual implementation is Swift.
|
|
10
|
+
//
|
|
11
|
+
// The first arg to RCT_EXTERN_MODULE is the Obj-C-visible class name.
|
|
12
|
+
// Marking the Swift class `@objc(RNImageStitcherQualityChecker)` aliases it
|
|
13
|
+
// to the same name on the Obj-C side, which means the JS layer sees
|
|
14
|
+
// `NativeModules.RNImageStitcherQualityChecker` regardless of the Swift
|
|
15
|
+
// class's actual Swift-side name.
|
|
16
|
+
//
|
|
17
|
+
|
|
18
|
+
#import <React/RCTBridgeModule.h>
|
|
19
|
+
|
|
20
|
+
@interface RCT_EXTERN_MODULE(RNImageStitcherQualityChecker, NSObject)
|
|
21
|
+
|
|
22
|
+
RCT_EXTERN_METHOD(measure:(NSString *)imagePath
|
|
23
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
24
|
+
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
25
|
+
|
|
26
|
+
@end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// QualityCheckerBridge.swift
|
|
3
|
+
//
|
|
4
|
+
// React Native bridge for `QualityChecker`. This file does NOT contain
|
|
5
|
+
// algorithm code — it converts JS-side promise calls into the pure
|
|
6
|
+
// Swift API in `QualityChecker.swift` and converts the resulting
|
|
7
|
+
// scores or errors back into a shape the JS layer can consume.
|
|
8
|
+
//
|
|
9
|
+
// Pairing pattern:
|
|
10
|
+
// * QualityChecker.swift — pure Swift, XCTest-able, no RN.
|
|
11
|
+
// * QualityCheckerBridge.swift (this file) — RN-aware, registered
|
|
12
|
+
// via `@objc(RNImageStitcherQualityChecker)` so the JS shim can find it
|
|
13
|
+
// at NativeModules.RNImageStitcherQualityChecker.
|
|
14
|
+
//
|
|
15
|
+
// Why two files instead of one?
|
|
16
|
+
// The bridge depends on React (RCTPromiseResolveBlock,
|
|
17
|
+
// RCTBridgeModule). XCTest in SwiftPM mode can't resolve those
|
|
18
|
+
// without an Xcode workspace. Splitting means the algorithm tests
|
|
19
|
+
// build clean from the command line, and only the bridge requires
|
|
20
|
+
// a host-app context to compile (which it already has, via the
|
|
21
|
+
// mobile app's Pods workspace).
|
|
22
|
+
|
|
23
|
+
#if canImport(React)
|
|
24
|
+
import Foundation
|
|
25
|
+
import React
|
|
26
|
+
|
|
27
|
+
@objc(RNImageStitcherQualityChecker)
|
|
28
|
+
public class QualityCheckerBridge: NSObject {
|
|
29
|
+
|
|
30
|
+
// RCT_EXPORT_MODULE — the Obj-C bridge file picks up this name and
|
|
31
|
+
// registers it with the JS module map. Returning false here means
|
|
32
|
+
// the module's queue methods aren't required to run on the main
|
|
33
|
+
// thread, which we want — image decode is CPU work.
|
|
34
|
+
@objc public static func requiresMainQueueSetup() -> Bool { return false }
|
|
35
|
+
|
|
36
|
+
/// Bridged entry: measure both blur + brightness and resolve with
|
|
37
|
+
/// `{ blurScore, brightnessScore }`. Reject with a stable code so
|
|
38
|
+
/// the JS layer can branch (e.g. "missing-file" can fall back to a
|
|
39
|
+
/// retry, "decode-failed" usually can't).
|
|
40
|
+
@objc(measure:resolver:rejecter:)
|
|
41
|
+
public func measure(
|
|
42
|
+
imagePath: NSString,
|
|
43
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
44
|
+
rejecter: @escaping RCTPromiseRejectBlock
|
|
45
|
+
) {
|
|
46
|
+
do {
|
|
47
|
+
let scores = try QualityChecker.measure(imagePath: imagePath as String)
|
|
48
|
+
resolver([
|
|
49
|
+
"blurScore": scores.blurScore,
|
|
50
|
+
"brightnessScore": scores.brightnessScore,
|
|
51
|
+
])
|
|
52
|
+
} catch let err as QualityCheckError {
|
|
53
|
+
switch err {
|
|
54
|
+
case .fileNotFound(let path):
|
|
55
|
+
rejecter("file-not-found", "File not found at path: \(path)", err)
|
|
56
|
+
case .imageDecodeFailed(let path):
|
|
57
|
+
rejecter("decode-failed", "Could not decode image at \(path)", err)
|
|
58
|
+
case .bufferAllocationFailed:
|
|
59
|
+
rejecter("buffer-alloc-failed", "Failed to allocate pixel buffer", err)
|
|
60
|
+
case .convolutionFailed(let vImageError):
|
|
61
|
+
rejecter(
|
|
62
|
+
"convolution-failed",
|
|
63
|
+
"vImage convolution failed (error \(vImageError))",
|
|
64
|
+
err
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
rejecter("unknown", "Unexpected error: \(error)", error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
#endif
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// RNSARCameraView — native UIView that renders the AR camera
|
|
4
|
+
// feed for the SDK's pose-aware capture surface.
|
|
5
|
+
//
|
|
6
|
+
// Phase 4.4 of the AR measurement plan. This is the camera-access
|
|
7
|
+
// handoff: vision-camera owns the camera in non-AR audits, ARKit
|
|
8
|
+
// owns it in AR audits. React Native picks which CameraView the
|
|
9
|
+
// host mounts; the host never sees both at once.
|
|
10
|
+
//
|
|
11
|
+
// Why ARSCNView vs custom Metal:
|
|
12
|
+
// We just need to render `ARFrame.capturedImage` to screen — no
|
|
13
|
+
// 3D content overlays, no SceneKit nodes. Custom Metal would be
|
|
14
|
+
// ~150 lines of MTKView setup + a textured-quad shader. ARSCNView
|
|
15
|
+
// does the same thing in 2 lines: it's a UIKit view that auto-
|
|
16
|
+
// renders the camera feed as the SceneKit background whenever its
|
|
17
|
+
// `session` property points at a running ARSession. Phase 5
|
|
18
|
+
// stitching consumes pose data, NOT pixels-from-Metal, so we
|
|
19
|
+
// never need the lower-level rendering control.
|
|
20
|
+
//
|
|
21
|
+
// Why a wrapper UIView (vs. exposing ARSCNView directly):
|
|
22
|
+
// RCTViewManager expects to vend a UIView subclass it owns.
|
|
23
|
+
// Wrapping ARSCNView lets us:
|
|
24
|
+
// - resize its frame to match the React Native layout in
|
|
25
|
+
// `layoutSubviews` (auto-resizing masks alone don't always
|
|
26
|
+
// fire when RN's flexbox engine assigns a new bounds rect),
|
|
27
|
+
// - add lifecycle hooks (start/stop the singleton AR session
|
|
28
|
+
// when the view enters / leaves the window hierarchy), and
|
|
29
|
+
// - keep room for future overlays (tracking-state HUD, focus
|
|
30
|
+
// indicator, etc.) without touching ARSCNView internals.
|
|
31
|
+
|
|
32
|
+
import Foundation
|
|
33
|
+
import ARKit
|
|
34
|
+
import UIKit
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@objc(RNSARCameraView)
|
|
38
|
+
public final class RNSARCameraView: UIView {
|
|
39
|
+
|
|
40
|
+
/// The ARSCNView that does the actual rendering. Bound to the
|
|
41
|
+
/// singleton's ARSession so all preview surfaces share the same
|
|
42
|
+
/// session (and the same pose log that the stitcher consumes).
|
|
43
|
+
private var arSCNView: ARSCNView!
|
|
44
|
+
|
|
45
|
+
public override init(frame: CGRect) {
|
|
46
|
+
super.init(frame: frame)
|
|
47
|
+
setupView()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public required init?(coder: NSCoder) {
|
|
51
|
+
super.init(coder: coder)
|
|
52
|
+
setupView()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func setupView() {
|
|
56
|
+
arSCNView = ARSCNView(frame: bounds)
|
|
57
|
+
arSCNView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
58
|
+
|
|
59
|
+
// Bind to the singleton's session. This is the critical
|
|
60
|
+
// line — without it, ARSCNView would try to create its own
|
|
61
|
+
// session and we'd lose the pose log. Sharing means:
|
|
62
|
+
// - The host's `useARSession` hook still drives lifecycle.
|
|
63
|
+
// - Pose data captured via `RNSARSession.shared`'s
|
|
64
|
+
// delegate callbacks remains intact; this view is purely
|
|
65
|
+
// a renderer.
|
|
66
|
+
arSCNView.session = RNSARSession.shared.arSession
|
|
67
|
+
|
|
68
|
+
// We don't draw any 3D content in Phase 4.4. Disable
|
|
69
|
+
// SceneKit's automatic statistics overlay and lighting model
|
|
70
|
+
// — we just want the camera feed.
|
|
71
|
+
arSCNView.showsStatistics = false
|
|
72
|
+
arSCNView.automaticallyUpdatesLighting = false
|
|
73
|
+
|
|
74
|
+
// Black background while ARKit is initialising so the user
|
|
75
|
+
// sees a clean frame instead of whatever was there before.
|
|
76
|
+
backgroundColor = .black
|
|
77
|
+
addSubview(arSCNView)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public override func layoutSubviews() {
|
|
81
|
+
super.layoutSubviews()
|
|
82
|
+
// RN's flexbox can re-bound this view at any time; keep the
|
|
83
|
+
// ARSCNView locked to our bounds. autoresizingMask handles
|
|
84
|
+
// most cases but isn't always enough on rotation transitions.
|
|
85
|
+
arSCNView.frame = bounds
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public override func didMoveToWindow() {
|
|
89
|
+
super.didMoveToWindow()
|
|
90
|
+
// When this view enters the hierarchy, ensure the AR session
|
|
91
|
+
// is running. When it leaves, stop the session so the
|
|
92
|
+
// hardware camera is freed for vision-camera or other uses.
|
|
93
|
+
//
|
|
94
|
+
// The singleton's start/stop are idempotent, so multiple
|
|
95
|
+
// ARCameraView instances mounting/unmounting won't fight
|
|
96
|
+
// each other (last-mount-wins semantics). In practice the
|
|
97
|
+
// host only mounts one at a time.
|
|
98
|
+
if window != nil {
|
|
99
|
+
if !RNSARSession.shared.isRunning {
|
|
100
|
+
RNSARSession.shared.start()
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Removed from window — stop the session. Don't clear
|
|
104
|
+
// the pose log here; the host explicitly clears between
|
|
105
|
+
// captures via `RNSARSession.shared.clearPoseLog()`
|
|
106
|
+
// so the JS layer controls when poses get discarded.
|
|
107
|
+
if RNSARSession.shared.isRunning {
|
|
108
|
+
RNSARSession.shared.stop()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|