react-native-yolo 0.0.5 → 0.0.8
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/Yolo.podspec +3 -0
- package/android/src/main/java/com/yolo/HybridYolo.kt +12 -235
- package/android/src/main/java/com/yolo/HybridYoloModel.kt +259 -0
- package/android/src/main/java/com/yolo/utils/BitmapOrientationFixer.kt +8 -1
- package/ios/HybridYolo.swift +52 -10
- package/ios/HybridYoloModel.swift +250 -0
- package/ios/Loader/YoloModelLoader.swift +139 -0
- package/ios/Utils/BitmapOrientationFixer.swift +64 -0
- package/ios/Utils/ContextProvider.swift +45 -0
- package/ios/Utils/FrameJpegConverter.swift +41 -0
- package/ios/Utils/FrameValidator.swift +40 -0
- package/ios/Utils/Nv12JpegEncoder.swift +58 -0
- package/ios/Utils/Yuv420ToNv12Converter.swift +71 -0
- package/lib/commonjs/index.js +4 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +4 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +12 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/yolo.nitro.d.ts +10 -6
- package/lib/typescript/src/specs/yolo.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/Yolo+autolinking.cmake +2 -0
- package/nitrogen/generated/android/YoloOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JHybridYoloModelSpec.cpp +78 -0
- package/nitrogen/generated/android/c++/JHybridYoloModelSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JHybridYoloSpec.cpp +9 -32
- package/nitrogen/generated/android/c++/JHybridYoloSpec.hpp +1 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/HybridYoloModelSpec.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/HybridYoloSpec.kt +1 -9
- package/nitrogen/generated/ios/Yolo-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/Yolo-Swift-Cxx-Bridge.hpp +48 -31
- package/nitrogen/generated/ios/Yolo-Swift-Cxx-Umbrella.hpp +5 -0
- package/nitrogen/generated/ios/c++/HybridYoloModelSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridYoloModelSpecSwift.hpp +97 -0
- package/nitrogen/generated/ios/c++/HybridYoloSpecSwift.hpp +6 -24
- package/nitrogen/generated/ios/swift/HybridYoloModelSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridYoloModelSpec_cxx.swift +160 -0
- package/nitrogen/generated/ios/swift/HybridYoloSpec.swift +1 -3
- package/nitrogen/generated/ios/swift/HybridYoloSpec_cxx.swift +8 -38
- package/nitrogen/generated/shared/c++/HybridYoloModelSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridYoloModelSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/HybridYoloSpec.cpp +0 -2
- package/nitrogen/generated/shared/c++/HybridYoloSpec.hpp +5 -8
- package/package.json +1 -1
- package/src/index.ts +14 -4
- package/src/specs/yolo.nitro.ts +12 -3
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import TensorFlowLite
|
|
4
|
+
import VisionCamera
|
|
5
|
+
import CoreVideo
|
|
6
|
+
|
|
7
|
+
public class HybridYoloModel: HybridYoloModelSpec {
|
|
8
|
+
private static let tag = "YOLO_MODEL_TAG"
|
|
9
|
+
|
|
10
|
+
private let modelLoader = YoloModelLoader()
|
|
11
|
+
|
|
12
|
+
private var interpreter: Interpreter? = nil
|
|
13
|
+
private var inputBuffer: Data? = nil
|
|
14
|
+
private var inputWidth = 0
|
|
15
|
+
private var inputHeight = 0
|
|
16
|
+
private var inputDataType: Tensor.DataType? = nil
|
|
17
|
+
|
|
18
|
+
private let threadLock = NSLock()
|
|
19
|
+
|
|
20
|
+
public required init(modelPath: String) throws {
|
|
21
|
+
super.init()
|
|
22
|
+
try load(modelPath: modelPath)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func load(modelPath: String) throws {
|
|
26
|
+
do {
|
|
27
|
+
let modelData = try modelLoader.load(modelPath: modelPath)
|
|
28
|
+
|
|
29
|
+
// Initialisation de l'interpréteur TensorFlowLiteSwift
|
|
30
|
+
interpreter = try Interpreter(modelData: modelData)
|
|
31
|
+
guard let localInterpreter = interpreter else { return }
|
|
32
|
+
|
|
33
|
+
try localInterpreter.allocateTensors()
|
|
34
|
+
|
|
35
|
+
let inputTensor = try localInterpreter.inputTensor(at: 0)
|
|
36
|
+
let outputTensor = try localInterpreter.outputTensor(at: 0)
|
|
37
|
+
|
|
38
|
+
let shape = inputTensor.shape.dimensions
|
|
39
|
+
|
|
40
|
+
inputHeight = shape[1]
|
|
41
|
+
inputWidth = shape[2]
|
|
42
|
+
inputDataType = inputTensor.dataType
|
|
43
|
+
inputBuffer = try modelLoader.makeInputBuffer(interpreter: localInterpreter)
|
|
44
|
+
|
|
45
|
+
NSLog("[%@]: ✅ YOLO model instance loaded", HybridYoloModel.tag)
|
|
46
|
+
NSLog("[%@]: 📥 Input shape: %@", HybridYoloModel.tag, shape.description)
|
|
47
|
+
NSLog("[%@]: 📤 Output shape: %@", HybridYoloModel.tag, outputTensor.shape.dimensions.description)
|
|
48
|
+
} catch {
|
|
49
|
+
NSLog("[%@]: ❌ Failed to load model instance: %@", HybridYoloModel.tag, error.localizedDescription)
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public override func detect(frame: any HybridFrameSpec) throws -> [Detection] {
|
|
55
|
+
guard let localInterpreter = interpreter else {
|
|
56
|
+
NSLog("[%@]: ❌ This model instance is not loaded.", HybridYoloModel.tag)
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
guard FrameValidator.isValidYuv(frame: frame) else {
|
|
61
|
+
NSLog("[%@]: ❌ Invalid frame provided for detection.", HybridYoloModel.tag)
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
guard var input = inputBuffer else { return [] }
|
|
66
|
+
|
|
67
|
+
threadLock.lock()
|
|
68
|
+
defer { threadLock.unlock() }
|
|
69
|
+
|
|
70
|
+
// Remplissage du buffer d'entrée en analysant les données NV12
|
|
71
|
+
try fillInputFromYuvFrame(
|
|
72
|
+
frame: frame,
|
|
73
|
+
input: &input,
|
|
74
|
+
dstWidth: inputWidth,
|
|
75
|
+
dstHeight: inputHeight,
|
|
76
|
+
dataType: inputDataType ?? .float32
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Injection du tampon de données brut dans le tenseur d'entrée
|
|
80
|
+
try localInterpreter.copy(input, toInputAt: 0)
|
|
81
|
+
try localInterpreter.invoke()
|
|
82
|
+
|
|
83
|
+
// Récupération des résultats du tenseur de sortie
|
|
84
|
+
let outputTensor = try localInterpreter.outputTensor(at: 0)
|
|
85
|
+
|
|
86
|
+
// Extraction et conversion de la structure des tenseurs [1, 300, 6] en Float
|
|
87
|
+
let nativeOutputs = outputTensor.data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> [Float] in
|
|
88
|
+
let floatPtr = ptr.assumingMemoryBound(to: Float.self)
|
|
89
|
+
return Array(floatPtr)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parseNmsOutput(outputArray: nativeOutputs, confidenceThreshold: 0.5)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public override func close() throws {
|
|
96
|
+
interpreter = nil
|
|
97
|
+
inputBuffer = nil
|
|
98
|
+
NSLog("[%@]: 🧹 YOLO model disposed", HybridYoloModel.tag)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func parseNmsOutput(outputArray: [Float], confidenceThreshold: Float = 0.5) -> [Detection] {
|
|
102
|
+
var detections: [Detection] = []
|
|
103
|
+
|
|
104
|
+
let totalRows = 300
|
|
105
|
+
let columnsPerRow = 6
|
|
106
|
+
|
|
107
|
+
// Parcours du tableau aplati [300 * 6]
|
|
108
|
+
for i in 0..<totalRows {
|
|
109
|
+
let offset = i * columnsPerRow
|
|
110
|
+
guard offset + 5 < outputArray.count else { break }
|
|
111
|
+
|
|
112
|
+
let score = outputArray[offset + 4]
|
|
113
|
+
if score < confidenceThreshold { continue }
|
|
114
|
+
|
|
115
|
+
let box = BoundingBox(
|
|
116
|
+
x1: Double(outputArray[offset + 0]),
|
|
117
|
+
y1: Double(outputArray[offset + 1]),
|
|
118
|
+
x2: Double(outputArray[offset + 2]),
|
|
119
|
+
y2: Double(outputArray[offset + 3])
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
detections.append(
|
|
123
|
+
Detection(
|
|
124
|
+
boundingBox: box,
|
|
125
|
+
score: Double(score),
|
|
126
|
+
classId: Double(outputArray[offset + 5])
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return detections
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func fillInputFromYuvFrame(
|
|
135
|
+
frame: any HybridFrameSpec,
|
|
136
|
+
input: inout Data,
|
|
137
|
+
dstWidth: Int,
|
|
138
|
+
dstHeight: Int,
|
|
139
|
+
dataType: Tensor.DataType
|
|
140
|
+
) throws {
|
|
141
|
+
let srcWidth = Int(frame.width)
|
|
142
|
+
let srcHeight = Int(frame.height)
|
|
143
|
+
|
|
144
|
+
// Extraction du CVPixelBuffer matériel pour un accès direct ultra-rapide
|
|
145
|
+
let nativeBuffer = try frame.getNativeBuffer()
|
|
146
|
+
guard let rawPointer = nativeBuffer.pointer else { return }
|
|
147
|
+
let pixelBuffer = Unmanaged<CVPixelBuffer>.fromOpaque(rawPointer).takeUnretainedValue()
|
|
148
|
+
|
|
149
|
+
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
|
|
150
|
+
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
|
|
151
|
+
|
|
152
|
+
guard let yBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0),
|
|
153
|
+
let uvBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { return }
|
|
154
|
+
|
|
155
|
+
let yRowStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
|
|
156
|
+
let uvRowStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
|
|
157
|
+
|
|
158
|
+
let yPtr = yBaseAddress.assumingMemoryBound(to: UInt8.self)
|
|
159
|
+
let uvPtr = uvBaseAddress.assumingMemoryBound(to: UInt8.self)
|
|
160
|
+
|
|
161
|
+
// Allocation du flux d'écriture en octets
|
|
162
|
+
var bytes: [UInt8] = []
|
|
163
|
+
bytes.reserveCapacity(input.count)
|
|
164
|
+
|
|
165
|
+
for dy in 0..<dstHeight {
|
|
166
|
+
for dx in 0..<dstWidth {
|
|
167
|
+
let (srcX, srcY) = mapModelPixelToFramePixel(
|
|
168
|
+
dx: dx, dy: dy,
|
|
169
|
+
dstWidth: dstWidth, dstHeight: dstHeight,
|
|
170
|
+
srcWidth: srcWidth, srcHeight: srcHeight,
|
|
171
|
+
orientation: frame.orientation
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
let yIndex = srcY * yRowStride + srcX
|
|
175
|
+
|
|
176
|
+
// NV12 : Les composants U et V sont regroupés au même endroit (U, V, U, V)
|
|
177
|
+
// L'indice de ligne utilise uvRowStride. Chaque pixel partagé saute de 2 en 2 horizontalement.
|
|
178
|
+
let uvY = srcY / 2
|
|
179
|
+
let uvX = srcX / 2
|
|
180
|
+
let uvIndex = uvY * uvRowStride + uvX * 2
|
|
181
|
+
|
|
182
|
+
let y = Int(yPtr[yIndex])
|
|
183
|
+
let u = Int(uvPtr[uvIndex]) // Dans NV12, U se trouve au premier octet
|
|
184
|
+
let v = Int(uvPtr[uvIndex + 1]) // V se trouve juste à côté
|
|
185
|
+
|
|
186
|
+
// Formule standard de conversion YUV en RGB
|
|
187
|
+
let rFloat = Float(y) + 1.402 * Float(v - 128)
|
|
188
|
+
let gFloat = Float(y) - 0.344136 * Float(u - 128) - 0.714136 * Float(v - 128)
|
|
189
|
+
let bFloat = Float(y) + 1.772 * Float(u - 128)
|
|
190
|
+
|
|
191
|
+
let r = Int(rFloat.rounded()).clamped(to: 0...255)
|
|
192
|
+
let g = Int(gFloat.rounded()).clamped(to: 0...255)
|
|
193
|
+
let b = Int(bFloat.rounded()).clamped(to: 0...255)
|
|
194
|
+
|
|
195
|
+
if dataType == .float32 {
|
|
196
|
+
let rNorm = Float(r) / 255.0
|
|
197
|
+
let gNorm = Float(g) / 255.0
|
|
198
|
+
let bNorm = Float(b) / 255.0
|
|
199
|
+
|
|
200
|
+
withUnsafeBytes(of: rNorm) { bytes.append(contentsOf: $0) }
|
|
201
|
+
withUnsafeBytes(of: gNorm) { bytes.append(contentsOf: $0) }
|
|
202
|
+
withUnsafeBytes(of: bNorm) { bytes.append(contentsOf: $0) }
|
|
203
|
+
} else {
|
|
204
|
+
bytes.append(UInt8(r))
|
|
205
|
+
bytes.append(UInt8(g))
|
|
206
|
+
bytes.append(UInt8(b))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Copie globale du flux binaire structuré dans l'espace mémoire d'entrée
|
|
212
|
+
input.withUnsafeMutableBytes { dstPtr in
|
|
213
|
+
bytes.withUnsafeBytes { srcPtr in
|
|
214
|
+
dstPtr.copyBytes(from: srcPtr)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func mapModelPixelToFramePixel(
|
|
220
|
+
dx: Int, dy: Int,
|
|
221
|
+
dstWidth: Int, dstHeight: Int,
|
|
222
|
+
srcWidth: Int, srcHeight: Int,
|
|
223
|
+
orientation: CameraOrientation
|
|
224
|
+
) -> (x: Int, y: Int) {
|
|
225
|
+
let nx = Float(dx) / Float(dstWidth)
|
|
226
|
+
let ny = Float(dy) / Float(dstHeight)
|
|
227
|
+
|
|
228
|
+
switch orientation {
|
|
229
|
+
case .up:
|
|
230
|
+
let sx = Int(nx * Float(srcWidth))
|
|
231
|
+
let sy = Int(ny * Float(srcHeight))
|
|
232
|
+
return (sx.clamped(to: 0...(srcWidth - 1)), sy.clamped(to: 0...(srcHeight - 1)))
|
|
233
|
+
case .down:
|
|
234
|
+
let sx = Int((1.0 - nx) * Float(srcWidth))
|
|
235
|
+
let sy = Int((1.0 - ny) * Float(srcHeight))
|
|
236
|
+
return (sx.clamped(to: 0...(srcWidth - 1)), sy.clamped(to: 0...(srcHeight - 1)))
|
|
237
|
+
case .left:
|
|
238
|
+
let sx = Int(ny * Float(srcWidth))
|
|
239
|
+
let sy = Int((1.0 - nx) * Float(srcHeight))
|
|
240
|
+
return (sx.clamped(to: 0...(srcWidth - 1)), sy.clamped(to: 0...(srcHeight - 1)))
|
|
241
|
+
case .right:
|
|
242
|
+
let sx = Int((1.0 - ny) * Float(srcWidth))
|
|
243
|
+
let sy = Int(nx * Float(srcHeight))
|
|
244
|
+
return (sx.clamped(to: 0...(srcWidth - 1)), sy.clamped(to: 0...(srcHeight - 1)))
|
|
245
|
+
@unknown default:
|
|
246
|
+
return (0, 0)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import TensorFlowLite // Activé via votre s.dependency 'TensorFlowLiteSwift'
|
|
4
|
+
|
|
5
|
+
public class YoloModelLoader {
|
|
6
|
+
private static let tag = "YOLO_TAG_LOADER"
|
|
7
|
+
|
|
8
|
+
public init() {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Charge un modèle YOLO à partir du chemin spécifié (URL, URI de fichier, chemin absolu, ou ressource brute).
|
|
12
|
+
* Retourne un objet `Data` contenant les octets du modèle configurés via mmap (équivalent du MappedByteBuffer).
|
|
13
|
+
*/
|
|
14
|
+
public func load(modelPath: String) throws -> Data {
|
|
15
|
+
if modelPath.hasPrefix("http://") || modelPath.hasPrefix("https://") {
|
|
16
|
+
NSLog("[%@]: Loading model from URL", YoloModelLoader.tag)
|
|
17
|
+
let cachedFileURL = try downloadToCache(urlString: modelPath)
|
|
18
|
+
return try mapFile(fileURL: cachedFileURL)
|
|
19
|
+
|
|
20
|
+
} else if modelPath.hasPrefix("file://") {
|
|
21
|
+
NSLog("[%@]: Loading model from file URI", YoloModelLoader.tag)
|
|
22
|
+
guard let url = URL(string: modelPath) else {
|
|
23
|
+
throw NSError(domain: "YoloModelLoader", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid file URI"])
|
|
24
|
+
}
|
|
25
|
+
return try mapFile(fileURL: url)
|
|
26
|
+
|
|
27
|
+
} else if modelPath.hasPrefix("/") {
|
|
28
|
+
NSLog("[%@]: Loading model from absolute path", YoloModelLoader.tag)
|
|
29
|
+
let url = URL(fileURLWithPath: modelPath)
|
|
30
|
+
return try mapFile(fileURL: url)
|
|
31
|
+
|
|
32
|
+
} else if modelPath.hasPrefix("assets_") {
|
|
33
|
+
NSLog("[%@]: Loading model from RN raw resource", YoloModelLoader.tag)
|
|
34
|
+
let cachedFileURL = try copyRawResourceToCache(resourceName: modelPath)
|
|
35
|
+
return try mapFile(fileURL: cachedFileURL)
|
|
36
|
+
|
|
37
|
+
} else {
|
|
38
|
+
NSLog("[%@]: Loading model from Main App Bundle Assets", YoloModelLoader.tag)
|
|
39
|
+
let cleanName = modelPath.replacingOccurrences(of: ".tflite", with: "")
|
|
40
|
+
guard let bundleURL = ContextProvider.mainBundle.url(forResource: cleanName, withExtension: "tflite") else {
|
|
41
|
+
throw NSError(domain: "YoloModelLoader", code: 404, userInfo: [NSLocalizedDescriptionKey: "Model not found in main bundle: \(modelPath)"])
|
|
42
|
+
}
|
|
43
|
+
return try mapFile(fileURL: bundleURL)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private func copyRawResourceToCache(resourceName: String) throws -> URL {
|
|
48
|
+
guard let bundleURL = ContextProvider.mainBundle.url(forResource: resourceName, withExtension: "tflite") else {
|
|
49
|
+
throw NSError(domain: "YoloModelLoader", code: 404, userInfo: [NSLocalizedDescriptionKey: "Raw resource not found: \(resourceName)"])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let destinationURL = ContextProvider.cacheDirectory.appendingPathComponent("\(resourceName).tflite")
|
|
53
|
+
|
|
54
|
+
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
55
|
+
try? FileManager.default.removeItem(at: destinationURL)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try FileManager.default.copyItem(at: bundleURL, to: destinationURL)
|
|
59
|
+
|
|
60
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: destinationURL.path)
|
|
61
|
+
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
62
|
+
|
|
63
|
+
NSLog("[%@]: Copied raw resource to: %@", YoloModelLoader.tag, destinationURL.path)
|
|
64
|
+
NSLog("[%@]: Copied raw resource size: %lld bytes", YoloModelLoader.tag, fileSize)
|
|
65
|
+
|
|
66
|
+
return destinationURL
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private func mapFile(fileURL: URL) throws -> Data {
|
|
70
|
+
let path = fileURL.path
|
|
71
|
+
if !FileManager.default.fileExists(atPath: path) {
|
|
72
|
+
throw NSError(domain: "YoloModelLoader", code: 404, userInfo: [NSLocalizedDescriptionKey: "Model file does not exist: \(path)"])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
|
76
|
+
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
77
|
+
|
|
78
|
+
if fileSize <= 0 {
|
|
79
|
+
throw NSError(domain: "YoloModelLoader", code: 400, userInfo: [NSLocalizedDescriptionKey: "Model file is empty: \(path)"])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// .alwaysMapped utilise mmap au niveau du noyau iOS pour des performances instantanées (Zero CPU copy)
|
|
83
|
+
return try Data(contentsOf: fileURL, options: .alwaysMapped)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func downloadToCache(urlString: String) throws -> URL {
|
|
87
|
+
guard let url = URL(string: urlString) else {
|
|
88
|
+
throw NSError(domain: "YoloModelLoader", code: 400, userInfo: [NSLocalizedDescriptionKey: "Malformed URL"])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let destinationURL = ContextProvider.cacheDirectory.appendingPathComponent("yolo_model.tflite")
|
|
92
|
+
let modelData = try Data(contentsOf: url)
|
|
93
|
+
try modelData.write(to: destinationURL, options: .atomic)
|
|
94
|
+
|
|
95
|
+
NSLog("[%@]: Downloaded model to: %@", YoloModelLoader.tag, destinationURL.path)
|
|
96
|
+
NSLog("[%@]: Downloaded model size: %d bytes", YoloModelLoader.tag, modelData.count)
|
|
97
|
+
|
|
98
|
+
return destinationURL
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Alloue un tampon d'entrée brut ('Data') calibré sur les dimensions du tenseur d'entrée du modèle.
|
|
103
|
+
* Remplace parfaitement Direct ByteBuffer de Java.
|
|
104
|
+
*/
|
|
105
|
+
public func makeInputBuffer(interpreter: Interpreter) throws -> Data {
|
|
106
|
+
// Récupérer le premier tenseur d'entrée du modèle TFLite
|
|
107
|
+
let inputTensor = try interpreter.inputTensor(at: 0)
|
|
108
|
+
let shape = inputTensor.shape.dimensions // Généralement: [1, 640, 640, 3]
|
|
109
|
+
let dataType = inputTensor.dataType
|
|
110
|
+
|
|
111
|
+
let batch = shape[0]
|
|
112
|
+
let height = shape[1]
|
|
113
|
+
let width = shape[2]
|
|
114
|
+
let channels = shape[3]
|
|
115
|
+
|
|
116
|
+
// Vérifications strictes (équivalents de require() en Kotlin)
|
|
117
|
+
guard batch == 1 else {
|
|
118
|
+
fatalError("YOLO Input Violation: Batch size must be 1")
|
|
119
|
+
}
|
|
120
|
+
guard channels == 3 else {
|
|
121
|
+
fatalError("YOLO Input Violation: Channels count must be 3 (RGB)")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let bytesPerValue: Int
|
|
125
|
+
switch dataType {
|
|
126
|
+
case .float32:
|
|
127
|
+
bytesPerValue = 4
|
|
128
|
+
case .uInt8:
|
|
129
|
+
bytesPerValue = 1
|
|
130
|
+
default:
|
|
131
|
+
fatalError("Unsupported input type: \(dataType)")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let totalBytes = batch * width * height * channels * bytesPerValue
|
|
135
|
+
|
|
136
|
+
// Crée une structure Data Swift vide avec l'empreinte mémoire exacte
|
|
137
|
+
return Data(repeating: 0, count: totalBytes)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import NitroModules
|
|
4
|
+
import VisionCamera
|
|
5
|
+
|
|
6
|
+
public enum BitmapOrientationFixer {
|
|
7
|
+
public static func fix(
|
|
8
|
+
jpegBytes: [UInt8],
|
|
9
|
+
frame: any HybridFrameSpec,
|
|
10
|
+
quality: Int
|
|
11
|
+
) -> [UInt8] {
|
|
12
|
+
|
|
13
|
+
let data = Data(jpegBytes)
|
|
14
|
+
guard let image = UIImage(data: data),
|
|
15
|
+
let cgImage = image.cgImage
|
|
16
|
+
else {
|
|
17
|
+
return jpegBytes
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let uiOrientation: UIImage.Orientation
|
|
21
|
+
|
|
22
|
+
switch frame.orientation {
|
|
23
|
+
case .up:
|
|
24
|
+
uiOrientation = frame.isMirrored ? .upMirrored : .up
|
|
25
|
+
case .down:
|
|
26
|
+
uiOrientation = frame.isMirrored ? .downMirrored : .down
|
|
27
|
+
case .left:
|
|
28
|
+
uiOrientation = frame.isMirrored ? .leftMirrored : .left
|
|
29
|
+
case .right:
|
|
30
|
+
uiOrientation = frame.isMirrored ? .rightMirrored : .right
|
|
31
|
+
@unknown default:
|
|
32
|
+
uiOrientation = .up
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 1. Wrap the image with the correct EXIF layout metadata
|
|
36
|
+
let orientedImage = UIImage(
|
|
37
|
+
cgImage: cgImage,
|
|
38
|
+
scale: 1.0, // Force 1.0 to preserve raw camera matrix resolution
|
|
39
|
+
orientation: uiOrientation
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// 2. Configure a 1:1 hardware pixel canvas context format
|
|
43
|
+
let rendererFormat = UIGraphicsImageRendererFormat.default()
|
|
44
|
+
rendererFormat.scale = 1.0
|
|
45
|
+
rendererFormat.opaque = true // JPEG does not support transparency, making it opaque saves memory performance
|
|
46
|
+
|
|
47
|
+
// 3. Draw and physically bake the metadata rotation permanently into new pixel bytes
|
|
48
|
+
let bakedImage = UIGraphicsImageRenderer(
|
|
49
|
+
size: orientedImage.size,
|
|
50
|
+
format: rendererFormat
|
|
51
|
+
).image { _ in
|
|
52
|
+
orientedImage.draw(in: CGRect(origin: .zero, size: orientedImage.size))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let compressionQuality = CGFloat(max(0, min(quality, 100))) / 100.0
|
|
56
|
+
|
|
57
|
+
// 4. Compress the baked image. The metadata flag is now gone, and the pixels themselves are rotated.
|
|
58
|
+
guard let outputData = bakedImage.jpegData(compressionQuality: compressionQuality) else {
|
|
59
|
+
return jpegBytes
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [UInt8](outputData)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import NitroModules
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provides a way to access application-wide singletons and paths from anywhere in the app.
|
|
7
|
+
* This acts as the iOS equivalent to the Android ContextProvider.
|
|
8
|
+
*/
|
|
9
|
+
public enum ContextProvider {
|
|
10
|
+
|
|
11
|
+
/// Returns the main application instance (equivalent to Application instance)
|
|
12
|
+
public static var sharedApplication: UIApplication {
|
|
13
|
+
// Must be accessed on the main thread
|
|
14
|
+
if Thread.isMainThread {
|
|
15
|
+
return UIApplication.shared
|
|
16
|
+
} else {
|
|
17
|
+
var app: UIApplication!
|
|
18
|
+
DispatchQueue.main.sync {
|
|
19
|
+
app = UIApplication.shared
|
|
20
|
+
}
|
|
21
|
+
return app
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Returns the main bundle containing application assets (equivalent to context.resources)
|
|
26
|
+
public static var mainBundle: Bundle {
|
|
27
|
+
return Bundle.main
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Returns the application cache directory URL (equivalent to context.cacheDir)
|
|
31
|
+
public static var cacheDirectory: URL {
|
|
32
|
+
guard let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
|
33
|
+
fatalError("iOS Cache Directory is null or inaccessible")
|
|
34
|
+
}
|
|
35
|
+
return url
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Returns the application document directory URL (equivalent to context.filesDir)
|
|
39
|
+
public static var documentsDirectory: URL {
|
|
40
|
+
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
41
|
+
fatalError("iOS Documents Directory is null or inaccessible")
|
|
42
|
+
}
|
|
43
|
+
return url
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import VisionCamera
|
|
4
|
+
|
|
5
|
+
public enum FrameJpegConverter {
|
|
6
|
+
private static let tag = "YOLO_TAG_FrameJpegConverter"
|
|
7
|
+
|
|
8
|
+
public static func toJpegBytes(frame: any HybridFrameSpec, quality: Int = 80) -> [UInt8] {
|
|
9
|
+
// 1. Extraction et arrondi propre des dimensions du frame
|
|
10
|
+
let width = Int(frame.width.rounded())
|
|
11
|
+
let height = Int(frame.height.rounded())
|
|
12
|
+
|
|
13
|
+
// 2. Conversion ultra-rapide via memcpy vers le format matériel NV12
|
|
14
|
+
let nv12 = Yuv420ToNv12Converter.convert(frame: frame, width: width, height: height)
|
|
15
|
+
|
|
16
|
+
if nv12.isEmpty {
|
|
17
|
+
NSLog("[%@]: ❌ Failed to convert frame to NV12 array", tag)
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 3. Encodage matériel en JPEG via le GPU (CoreImage / CIContext)
|
|
22
|
+
let jpegBytes = Nv12JpegEncoder.encode(
|
|
23
|
+
nv12: nv12,
|
|
24
|
+
width: width,
|
|
25
|
+
height: height,
|
|
26
|
+
quality: quality
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if jpegBytes.isEmpty {
|
|
30
|
+
NSLog("[%@]: ❌ Failed to encode NV12 data to JPEG bytes", tag)
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 4. Redessin de sécurité via UIGraphicsImageRenderer pour fixer définitivement les pixels
|
|
35
|
+
return BitmapOrientationFixer.fix(
|
|
36
|
+
jpegBytes: jpegBytes,
|
|
37
|
+
frame: frame,
|
|
38
|
+
quality: quality
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import VisionCamera
|
|
4
|
+
|
|
5
|
+
public enum FrameValidator {
|
|
6
|
+
private static let tag = "YOLO_TAG_FrameValidator"
|
|
7
|
+
|
|
8
|
+
public static func isValidYuv(frame: any HybridFrameSpec) -> Bool {
|
|
9
|
+
// 1. Vérification de la validité globale du Frame
|
|
10
|
+
if !frame.isValid {
|
|
11
|
+
NSLog("[%@]: ❌ Frame is not valid", tag)
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
do {
|
|
16
|
+
let planes = try frame.getPlanes()
|
|
17
|
+
|
|
18
|
+
// 2. Sur iOS, le format matériel natif NV12 contient exactement 2 plans.
|
|
19
|
+
// On s'assure d'avoir au moins ces 2 plans requis.
|
|
20
|
+
if planes.count < 2 {
|
|
21
|
+
NSLog("[%@]: ❌ Expected at least 2 YUV planes (NV12), got %d", tag, planes.count)
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 3. Parcours et validation de chaque plan individuel
|
|
26
|
+
for (index, plane) in planes.enumerated() {
|
|
27
|
+
if !plane.isValid {
|
|
28
|
+
NSLog("[%@]: ❌ Plane %d is not valid", tag, index)
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true
|
|
34
|
+
|
|
35
|
+
} catch {
|
|
36
|
+
NSLog("[%@]: ❌ Failed to retrieve planes from frame: %@", tag, error.localizedDescription)
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreImage
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
public enum Nv12JpegEncoder {
|
|
6
|
+
// Réutiliser le CIContext permet d'éviter des fuites de mémoire massives à chaque frame
|
|
7
|
+
private static let ciContext = CIContext(options: [CIContextOption.useSoftwareRenderer: false])
|
|
8
|
+
|
|
9
|
+
public static func encode(
|
|
10
|
+
nv12: [UInt8],
|
|
11
|
+
width: Int,
|
|
12
|
+
height: Int,
|
|
13
|
+
quality: Int
|
|
14
|
+
) -> [UInt8] {
|
|
15
|
+
|
|
16
|
+
// 1. Convertir la qualité (0-100 sur Android) en CGFloat (0.0-1.0 sur iOS)
|
|
17
|
+
let compressionQuality = CGFloat(max(0, min(quality, 100))) / 100.0
|
|
18
|
+
|
|
19
|
+
let ySize = width * height
|
|
20
|
+
let uvSize = ySize / 2
|
|
21
|
+
|
|
22
|
+
// Sécurité : s'assurer que la taille du tableau correspond bien aux dimensions fournies
|
|
23
|
+
guard nv12.count >= (ySize + uvSize) else { return [] }
|
|
24
|
+
|
|
25
|
+
// 2. Transformer le tableau d'octets [UInt8] en objet Data Swift
|
|
26
|
+
let rawData = Data(bytes: nv12, count: ySize + uvSize)
|
|
27
|
+
|
|
28
|
+
// 3. Spécifier le format de couleur NV12 pour CoreImage (kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
|
|
29
|
+
let imageOptions: [CIImageOption: Any] = [
|
|
30
|
+
.colorSpace: CGColorSpaceCreateDeviceRGB()
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// On indique à CoreImage la structure exacte du NV12 (Plan 0: Y, Plan 1: UV entrelacé)
|
|
34
|
+
guard let ciImage = CIImage(
|
|
35
|
+
imageWithFormat: Int32(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
|
|
36
|
+
size: CGSize(width: width, height: height),
|
|
37
|
+
data: rawData,
|
|
38
|
+
rowBytes: width,
|
|
39
|
+
options: imageOptions
|
|
40
|
+
) else {
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 4. Rendu de l'image GPU vers une structure d'image CoreGraphics
|
|
45
|
+
guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 5. Conversion en UIImage puis compression matérielle en JPEG
|
|
50
|
+
let uiImage = UIImage(cgImage: cgImage)
|
|
51
|
+
guard let jpegData = uiImage.jpegData(compressionQuality: compressionQuality) else {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 6. Retourner le tableau d'octets natif [UInt8] requis par votre modèle
|
|
56
|
+
return [UInt8](jpegData)
|
|
57
|
+
}
|
|
58
|
+
}
|