react-native-yolo 0.0.7 → 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 CHANGED
@@ -27,5 +27,8 @@ Pod::Spec.new do |s|
27
27
 
28
28
  s.dependency 'React-jsi'
29
29
  s.dependency 'React-callinvoker'
30
+ s.dependency 'VisionCamera'
31
+ s.dependency 'TensorFlowLiteSwift'
32
+
30
33
  install_modules_dependencies(s)
31
34
  end
@@ -11,6 +11,7 @@ import org.tensorflow.lite.DataType
11
11
  import org.tensorflow.lite.Interpreter
12
12
  import yolo.com.loader.YoloModelLoader
13
13
  import kotlin.math.roundToInt
14
+ import com.margelo.nitro.camera.CameraOrientation
14
15
 
15
16
  class HybridYoloModel(
16
17
  modelPath: String
@@ -158,8 +159,15 @@ class HybridYoloModel(
158
159
 
159
160
  for (dy in 0 until dstHeight) {
160
161
  for (dx in 0 until dstWidth) {
161
- val srcX = dy * srcWidth / dstHeight
162
- val srcY = srcHeight - 1 - (dx * srcHeight / dstWidth)
162
+ val (srcX, srcY) = mapModelPixelToFramePixel(
163
+ dx = dx,
164
+ dy = dy,
165
+ dstWidth = dstWidth,
166
+ dstHeight = dstHeight,
167
+ srcWidth = srcWidth,
168
+ srcHeight = srcHeight,
169
+ orientation = frame.orientation
170
+ )
163
171
 
164
172
  val yIndex = srcY * yRowStride + srcX
165
173
 
@@ -209,4 +217,43 @@ class HybridYoloModel(
209
217
 
210
218
  input.rewind()
211
219
  }
220
+
221
+ private fun mapModelPixelToFramePixel(
222
+ dx: Int,
223
+ dy: Int,
224
+ dstWidth: Int,
225
+ dstHeight: Int,
226
+ srcWidth: Int,
227
+ srcHeight: Int,
228
+ orientation: CameraOrientation
229
+ ): Pair<Int, Int> {
230
+ val nx = dx.toFloat() / dstWidth
231
+ val ny = dy.toFloat() / dstHeight
232
+
233
+ return when (orientation) {
234
+ CameraOrientation.UP -> {
235
+ val srcX = (nx * srcWidth).toInt()
236
+ val srcY = (ny * srcHeight).toInt()
237
+ srcX.coerceIn(0, srcWidth - 1) to srcY.coerceIn(0, srcHeight - 1)
238
+ }
239
+
240
+ CameraOrientation.DOWN -> {
241
+ val srcX = ((1f - nx) * srcWidth).toInt()
242
+ val srcY = ((1f - ny) * srcHeight).toInt()
243
+ srcX.coerceIn(0, srcWidth - 1) to srcY.coerceIn(0, srcHeight - 1)
244
+ }
245
+
246
+ CameraOrientation.LEFT -> {
247
+ val srcX = (ny * srcWidth).toInt()
248
+ val srcY = ((1f - nx) * srcHeight).toInt()
249
+ srcX.coerceIn(0, srcWidth - 1) to srcY.coerceIn(0, srcHeight - 1)
250
+ }
251
+
252
+ CameraOrientation.RIGHT -> {
253
+ val srcX = ((1f - ny) * srcWidth).toInt()
254
+ val srcY = (nx * srcHeight).toInt()
255
+ srcX.coerceIn(0, srcWidth - 1) to srcY.coerceIn(0, srcHeight - 1)
256
+ }
257
+ }
258
+ }
212
259
  }
@@ -1,14 +1,56 @@
1
- //
2
- // HybridYolo.swift
3
- // Pods
4
- //
5
- // Created by Khaoula-Ghalimi on 22/06/2026.
6
- //
7
-
8
1
  import Foundation
2
+ import NitroModules
3
+ import VisionCamera
9
4
 
10
- class HybridYolo: HybridYoloSpec {
11
- func sum(num1: Double, num2: Double) throws -> Double {
12
- return num1 + num2
5
+ public class HybridYolo: HybridYoloSpec {
6
+ private static let tag = "YOLO_TAG"
7
+
8
+ // Initialisateur obligatoire pour les modules Nitro
9
+ public required init() {
10
+ super.init()
11
+ }
12
+
13
+ /**
14
+ * Charge l'objet modèle YOLO spécifié par son chemin.
15
+ * Implémente la méthode obligatoire du protocole Nitro TypeScript.
16
+ */
17
+ public override func loadModel(modelPath: String) throws -> any HybridYoloModelSpec {
18
+ NSLog("[%@]: Trying to load model object: %@", HybridYolo.tag, modelPath)
19
+
20
+ // Initialise et retourne votre sous-classe de modèle Nitro (assurez-vous qu'elle s'appelle bien HybridYoloModel)
21
+ return try HybridYoloModel(modelPath: modelPath)
22
+ }
23
+
24
+ /**
25
+ * Valide un Frame de la caméra, le convertit en JPEG permanent (NV12 -> JPEG -> Fix Rotation),
26
+ * puis l'encode instantanément en chaîne de caractères Base64 standard.
27
+ */
28
+ public override func frameToBase64(frame: any HybridFrameSpec) throws -> String {
29
+ NSLog("[%@]: Trying to convert frame to base64", HybridYolo.tag)
30
+
31
+ do {
32
+ // 1. Validation du buffer vidéo via notre validateur adapté à iOS (NV12 à 2 plans minimum)
33
+ guard FrameValidator.isValidYuv(frame: frame) else {
34
+ return ""
35
+ }
36
+ NSLog("[%@]: frameToBase64: frame is valid YUV", HybridYolo.tag)
37
+
38
+ // 2. Traitement complet de l'image (Conversion NV12 -> Encodage GPU JPEG -> Rendu de sécurité)
39
+ let jpegBytes = FrameJpegConverter.toJpegBytes(frame: frame, quality: 80)
40
+
41
+ guard !jpegBytes.isEmpty else {
42
+ NSLog("[%@]: ❌ frameToBase64 failed: converted JPEG bytes array is empty", HybridYolo.tag)
43
+ return ""
44
+ }
45
+ NSLog("[%@]: frameToBase64: jpegBytes size: %d bytes", HybridYolo.tag, jpegBytes.count)
46
+
47
+ // 3. Encapsulation dans un conteneur Data pour un encodage Base64 matériel (sans sauts de ligne, équivalent de NO_WRAP)
48
+ let data = Data(jpegBytes)
49
+ return data.base64EncodedString(options: [])
50
+
51
+ } catch {
52
+ NSLog("[%@]: ❌ frameToBase64 failed: %@", HybridYolo.tag, error.localizedDescription)
53
+ return ""
54
+ }
13
55
  }
14
56
  }
@@ -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
+ }
@@ -0,0 +1,71 @@
1
+ import Foundation
2
+ import AVFoundation
3
+ import NitroModules
4
+ import VisionCamera
5
+ import CoreVideo
6
+
7
+ public enum Yuv420ToNv12Converter {
8
+ public static func convert(frame: any HybridFrameSpec, width: Int, height: Int) -> [UInt8] {
9
+ do {
10
+ let nativeBuffer = try frame.getNativeBuffer()
11
+ guard let rawPointer = nativeBuffer.pointer else { return [] }
12
+
13
+ let pixelBuffer = Unmanaged<CVPixelBuffer>
14
+ .fromOpaque(rawPointer)
15
+ .takeUnretainedValue()
16
+
17
+ CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
18
+ defer {
19
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
20
+ }
21
+
22
+ guard
23
+ let yBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0),
24
+ let uvBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)
25
+ else {
26
+ return []
27
+ }
28
+
29
+ let yRowStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
30
+ let uvRowStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
31
+
32
+ let ySize = width * height
33
+ let uvSize = ySize / 2
34
+
35
+ var nv12 = [UInt8](repeating: 0, count: ySize + uvSize)
36
+
37
+ nv12.withUnsafeMutableBytes { dstBuffer in
38
+ guard let dstBase = dstBuffer.baseAddress else { return }
39
+
40
+ let ySrc = yBaseAddress.assumingMemoryBound(to: UInt8.self)
41
+ let uvSrc = uvBaseAddress.assumingMemoryBound(to: UInt8.self)
42
+ let dst = dstBase.assumingMemoryBound(to: UInt8.self)
43
+
44
+ // 1. Copy Y Plane row-by-row to safely discard padding/stride
45
+ for row in 0..<height {
46
+ memcpy(
47
+ dst.advanced(by: row * width),
48
+ ySrc.advanced(by: row * yRowStride),
49
+ width
50
+ )
51
+ }
52
+
53
+ // 2. Copy UV Plane row-by-row safely
54
+ let uvDstStart = ySize
55
+ let chromaHeight = height / 2
56
+
57
+ for row in 0..<chromaHeight {
58
+ memcpy(
59
+ dst.advanced(by: uvDstStart + (row * width)), // Fixed layout math
60
+ uvSrc.advanced(by: row * uvRowStride),
61
+ width
62
+ )
63
+ }
64
+ }
65
+
66
+ return nv12
67
+ } catch {
68
+ return []
69
+ }
70
+ }
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-yolo",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "react-native-yolo is a react native package built with Nitro",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",