react-native-yolo 0.0.3 → 0.0.5

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 (50) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +51 -51
  3. package/Yolo.podspec +31 -31
  4. package/android/CMakeLists.txt +35 -32
  5. package/android/build.gradle +154 -152
  6. package/android/fix-prefab.gradle +50 -50
  7. package/android/gradle.properties +5 -5
  8. package/android/src/main/AndroidManifest.xml +3 -2
  9. package/android/src/main/cpp/cpp-adapter.cpp +8 -8
  10. package/android/src/main/java/com/yolo/HybridYolo.kt +265 -42
  11. package/android/src/main/java/com/yolo/YoloPackage.kt +20 -20
  12. package/android/src/main/java/com/yolo/loader/YoloModelLoader.kt +31 -0
  13. package/android/src/main/java/com/yolo/utils/BitmapOrientationFixer.kt +56 -0
  14. package/android/src/main/java/com/yolo/utils/FrameJpegConverter.kt +27 -0
  15. package/android/src/main/java/com/yolo/utils/FrameValidator.kt +29 -0
  16. package/android/src/main/java/com/yolo/utils/Nv21JpegEncoder.kt +34 -0
  17. package/android/src/main/java/com/yolo/utils/Yuv420ToNv21Converter.kt +62 -0
  18. package/ios/Bridge.h +8 -8
  19. package/ios/HybridYolo.swift +14 -14
  20. package/lib/commonjs/index.js +10 -1
  21. package/lib/commonjs/index.js.map +1 -1
  22. package/lib/module/index.js +10 -1
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/typescript/src/index.d.ts +3 -1
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/specs/yolo.nitro.d.ts +15 -0
  27. package/lib/typescript/src/specs/yolo.nitro.d.ts.map +1 -1
  28. package/nitro.json +29 -29
  29. package/nitrogen/generated/android/c++/JBoundingBox.hpp +69 -0
  30. package/nitrogen/generated/android/c++/JDetection.hpp +66 -0
  31. package/nitrogen/generated/android/c++/JHybridYoloSpec.cpp +33 -1
  32. package/nitrogen/generated/android/c++/JHybridYoloSpec.hpp +2 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/BoundingBox.kt +66 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/Detection.kt +61 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/HybridYoloSpec.kt +9 -0
  36. package/nitrogen/generated/ios/Yolo-Swift-Cxx-Bridge.cpp +11 -0
  37. package/nitrogen/generated/ios/Yolo-Swift-Cxx-Bridge.hpp +54 -0
  38. package/nitrogen/generated/ios/Yolo-Swift-Cxx-Umbrella.hpp +12 -0
  39. package/nitrogen/generated/ios/c++/HybridYoloSpecSwift.hpp +27 -1
  40. package/nitrogen/generated/ios/swift/BoundingBox.swift +44 -0
  41. package/nitrogen/generated/ios/swift/Detection.swift +39 -0
  42. package/nitrogen/generated/ios/swift/HybridYoloSpec.swift +3 -0
  43. package/nitrogen/generated/ios/swift/HybridYoloSpec_cxx.swift +39 -0
  44. package/nitrogen/generated/shared/c++/BoundingBox.hpp +95 -0
  45. package/nitrogen/generated/shared/c++/Detection.hpp +92 -0
  46. package/nitrogen/generated/shared/c++/HybridYoloSpec.cpp +2 -0
  47. package/nitrogen/generated/shared/c++/HybridYoloSpec.hpp +10 -1
  48. package/package.json +127 -122
  49. package/src/index.ts +11 -11
  50. package/src/specs/yolo.nitro.ts +24 -7
@@ -1,51 +1,51 @@
1
- tasks.configureEach { task ->
2
- // Make sure that we generate our prefab publication file only after having built the native library
3
- // so that not a header publication file, but a full configuration publication will be generated, which
4
- // will include the .so file
5
-
6
- def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
7
- def matcher = task.name =~ prefabConfigurePattern
8
- if (matcher.matches()) {
9
- def variantName = matcher[0][1]
10
- task.outputs.upToDateWhen { false }
11
- task.dependsOn("externalNativeBuild${variantName}")
12
- }
13
- }
14
-
15
- afterEvaluate {
16
- def abis = reactNativeArchitectures()
17
- rootProject.allprojects.each { proj ->
18
- if (proj === rootProject) return
19
-
20
- def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
21
- config.dependencies.any { dep ->
22
- dep.group == project.group && dep.name == project.name
23
- }
24
- }
25
- if (!dependsOnThisLib && proj != project) return
26
-
27
- if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
28
- return
29
- }
30
-
31
- def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
32
- // Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
33
- // generate a libnameConfig.cmake file that will contain our native library (.so).
34
- // See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
35
- variants.all { variant ->
36
- def variantName = variant.name
37
- abis.each { abi ->
38
- def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
39
- if (!searchDir.exists()) return
40
- def matches = []
41
- searchDir.eachDir { randomDir ->
42
- def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
43
- if (prefabFile.exists()) matches << prefabFile
44
- }
45
- matches.each { prefabConfig ->
46
- prefabConfig.setLastModified(System.currentTimeMillis())
47
- }
48
- }
49
- }
50
- }
1
+ tasks.configureEach { task ->
2
+ // Make sure that we generate our prefab publication file only after having built the native library
3
+ // so that not a header publication file, but a full configuration publication will be generated, which
4
+ // will include the .so file
5
+
6
+ def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
7
+ def matcher = task.name =~ prefabConfigurePattern
8
+ if (matcher.matches()) {
9
+ def variantName = matcher[0][1]
10
+ task.outputs.upToDateWhen { false }
11
+ task.dependsOn("externalNativeBuild${variantName}")
12
+ }
13
+ }
14
+
15
+ afterEvaluate {
16
+ def abis = reactNativeArchitectures()
17
+ rootProject.allprojects.each { proj ->
18
+ if (proj === rootProject) return
19
+
20
+ def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
21
+ config.dependencies.any { dep ->
22
+ dep.group == project.group && dep.name == project.name
23
+ }
24
+ }
25
+ if (!dependsOnThisLib && proj != project) return
26
+
27
+ if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
28
+ return
29
+ }
30
+
31
+ def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
32
+ // Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
33
+ // generate a libnameConfig.cmake file that will contain our native library (.so).
34
+ // See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
35
+ variants.all { variant ->
36
+ def variantName = variant.name
37
+ abis.each { abi ->
38
+ def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
39
+ if (!searchDir.exists()) return
40
+ def matches = []
41
+ searchDir.eachDir { randomDir ->
42
+ def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
43
+ if (prefabFile.exists()) matches << prefabFile
44
+ }
45
+ matches.each { prefabConfig ->
46
+ prefabConfig.setLastModified(System.currentTimeMillis())
47
+ }
48
+ }
49
+ }
50
+ }
51
51
  }
@@ -1,5 +1,5 @@
1
- Yolo_kotlinVersion=2.1.20
2
- Yolo_minSdkVersion=23
3
- Yolo_targetSdkVersion=35
4
- Yolo_compileSdkVersion=34
5
- Yolo_ndkVersion=27.1.12297006
1
+ Yolo_kotlinVersion=2.1.20
2
+ Yolo_minSdkVersion=23
3
+ Yolo_targetSdkVersion=35
4
+ Yolo_compileSdkVersion=34
5
+ Yolo_ndkVersion=27.1.12297006
@@ -1,2 +1,3 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- </manifest>
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-feature android:name="android.hardware.camera" android:required="false" />
3
+ </manifest>
@@ -1,9 +1,9 @@
1
- #include <jni.h>
2
- #include <fbjni/fbjni.h>
3
- #include "YoloOnLoad.hpp"
4
-
5
- JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
6
- return facebook::jni::initialize(vm, []() {
7
- margelo::nitro::yolo::registerAllNatives();
8
- });
1
+ #include <jni.h>
2
+ #include <fbjni/fbjni.h>
3
+ #include "YoloOnLoad.hpp"
4
+
5
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
6
+ return facebook::jni::initialize(vm, []() {
7
+ margelo::nitro::yolo::registerAllNatives();
8
+ });
9
9
  }
@@ -1,42 +1,265 @@
1
- package com.yolo
2
-
3
- import android.net.Uri
4
- import android.util.Log
5
- import com.margelo.nitro.NitroModules
6
- import com.margelo.nitro.yolo.HybridYoloSpec
7
- import java.io.File
8
- import java.io.RandomAccessFile
9
- import java.net.URL
10
- import java.nio.MappedByteBuffer
11
- import java.nio.channels.FileChannel
12
- import org.tensorflow.lite.Interpreter
13
- import org.tensorflow.lite.support.common.FileUtil
14
- import yolo.com.loader.YoloModelLoader
15
-
16
- class HybridYolo : HybridYoloSpec() {
17
- private var interpreter: Interpreter? = null
18
- private val modelLoader = YoloModelLoader()
19
-
20
- override fun sum(num1: Double, num2: Double): Double {
21
- return num1 + num2
22
- }
23
-
24
- override fun loadModel(modelPath: String) {
25
- val context =
26
- NitroModules.applicationContext ?: throw IllegalStateException("Context is null")
27
-
28
- Log.d("YOLO_TAG", "Trying to load: $modelPath")
29
-
30
- try {
31
- val modelBuffer = modelLoader.load(modelPath)
32
-
33
- interpreter?.close()
34
- interpreter = Interpreter(modelBuffer)
35
-
36
- Log.d("YOLO_TAG", "✅ Model loaded successfully!")
37
- } catch (e: Exception) {
38
- Log.e("YOLO_TAG", "❌ Failed to load model: ${e.message}", e)
39
- }
40
- }
41
-
42
- }
1
+ package com.yolo
2
+
3
+ import android.net.Uri
4
+ import android.util.Log
5
+ import com.margelo.nitro.NitroModules
6
+ import com.margelo.nitro.yolo.HybridYoloSpec
7
+ import java.io.File
8
+ import java.io.RandomAccessFile
9
+ import java.net.URL
10
+ import java.nio.MappedByteBuffer
11
+ import java.nio.channels.FileChannel
12
+ import org.tensorflow.lite.Interpreter
13
+ import org.tensorflow.lite.support.common.FileUtil
14
+ import yolo.com.loader.YoloModelLoader
15
+ import com.margelo.nitro.camera.HybridFrameSpec
16
+
17
+ import android.util.Base64
18
+ import com.yolo.utils.FrameJpegConverter
19
+ import com.yolo.utils.FrameValidator
20
+
21
+ import com.margelo.nitro.yolo.Detection
22
+ import com.margelo.nitro.yolo.BoundingBox
23
+
24
+ import java.nio.ByteBuffer
25
+ import java.nio.ByteOrder
26
+ import org.tensorflow.lite.DataType
27
+ import kotlin.math.roundToInt
28
+
29
+
30
+ class HybridYolo : HybridYoloSpec() {
31
+ companion object {
32
+ private const val TAG = "YOLO_TAG"
33
+ }
34
+ private var interpreter: Interpreter? = null
35
+
36
+ private var inputBuffer: ByteBuffer? = null
37
+ private var inputWidth = 0
38
+ private var inputHeight = 0
39
+ private var inputDataType: DataType? = null
40
+
41
+ private val modelLoader = YoloModelLoader()
42
+
43
+ override fun sum(num1: Double, num2: Double): Double {
44
+ return num1 + num2
45
+ }
46
+
47
+ override fun loadModel(modelPath: String) {
48
+ val context =
49
+ NitroModules.applicationContext ?: throw IllegalStateException("Context is null")
50
+
51
+ Log.d(TAG, "Trying to load: $modelPath")
52
+
53
+ try {
54
+ val modelBuffer = modelLoader.load(modelPath)
55
+
56
+ interpreter?.close()
57
+ interpreter = Interpreter(modelBuffer)
58
+
59
+ val inputTensor = interpreter!!.getInputTensor(0)
60
+ val shape = inputTensor.shape()
61
+
62
+ inputHeight = shape[1]
63
+ inputWidth = shape[2]
64
+ inputDataType = inputTensor.dataType()
65
+ inputBuffer = modelLoader.makeInputBuffer(interpreter!!)
66
+
67
+ val outputTensor = interpreter!!.getOutputTensor(0)
68
+ Log.d(TAG, "✅ YOLO model loaded")
69
+ Log.d(TAG, "📥 Input shape: ${inputTensor.shape().contentToString()}")
70
+ Log.d(TAG, "📤 Output shape: ${outputTensor.shape().contentToString()}")
71
+ Log.d(TAG, "📥 Input type: ${inputTensor.dataType()}")
72
+ Log.d(TAG, "📤 Output type: ${outputTensor.dataType()}")
73
+
74
+ Log.d(TAG, "Input shape=${shape.contentToString()} type=$inputDataType")
75
+ Log.d(TAG, "✅ Model loaded successfully!")
76
+ } catch (e: Exception) {
77
+ Log.e(TAG, "❌ Failed to load model: ${e.message}", e)
78
+ }
79
+ }
80
+ override fun frameToBase64(frame: HybridFrameSpec): String {
81
+ return try {
82
+ if (!FrameValidator.isValidYuv(frame)) return ""
83
+
84
+ val jpegBytes = FrameJpegConverter.toJpegBytes(
85
+ frame = frame,
86
+ quality = 80
87
+ )
88
+
89
+ Base64.encodeToString(jpegBytes, Base64.NO_WRAP)
90
+ } catch (e: Exception) {
91
+ Log.e(TAG, "❌ frameToBase64 failed", e)
92
+ ""
93
+ }
94
+ }
95
+
96
+ override fun detect(frame: HybridFrameSpec): Array<Detection> {
97
+ val localInterpreter = interpreter ?: run {
98
+ Log.e(TAG, "❌ Model is not loaded. Please call loadModel() first.")
99
+ return emptyArray()
100
+ }
101
+
102
+ if (!FrameValidator.isValidYuv(frame)) {
103
+ Log.e(TAG, "❌ Invalid frame provided for detection.")
104
+ return emptyArray()
105
+ }
106
+
107
+ val input = inputBuffer ?: return emptyArray()
108
+
109
+ fillInputFromYuvFrame(
110
+ frame = frame,
111
+ input = input,
112
+ dstWidth = inputWidth,
113
+ dstHeight = inputHeight,
114
+ dataType = inputDataType ?: DataType.FLOAT32
115
+ )
116
+
117
+ val output = Array(1) { Array(300) { FloatArray(6) } }
118
+
119
+ localInterpreter.run(input, output)
120
+
121
+ val detections = parseNmsOutput(output, confidenceThreshold = 0.5f)
122
+ if (detections.isEmpty()) {
123
+ val best = output[0].maxByOrNull { it[4] }
124
+ Log.d(TAG, "No detections. Best row=${best?.contentToString()}")
125
+ } else {
126
+ Log.d(TAG, "✅ Detections: ${detections.size}")
127
+ }
128
+
129
+
130
+ return detections.toTypedArray()
131
+ }
132
+
133
+ private fun parseNmsOutput(
134
+ output: Array<Array<FloatArray>>,
135
+ confidenceThreshold: Float = 0.5f
136
+ ): List<Detection> {
137
+ val detections = mutableListOf<Detection>()
138
+
139
+ val batchMatrix = output[0]
140
+
141
+ for (i in batchMatrix.indices) {
142
+ val row = batchMatrix[i]
143
+
144
+ val score = row[4]
145
+ if (score < confidenceThreshold) continue
146
+
147
+ val x1 = row[0].toDouble()
148
+ val y1 = row[1].toDouble()
149
+ val x2 = row[2].toDouble()
150
+ val y2 = row[3].toDouble()
151
+ val classId = row[5].toDouble()
152
+
153
+ detections.add(
154
+ Detection(
155
+ boundingBox = BoundingBox(
156
+ x1 = x1,
157
+ y1 = y1,
158
+ x2 = x2,
159
+ y2 = y2
160
+ ),
161
+ score = score.toDouble(),
162
+ classId = classId
163
+ )
164
+ )
165
+ }
166
+
167
+ return detections
168
+ }
169
+
170
+
171
+
172
+ private fun fillInputFromYuvFrame(
173
+ frame: HybridFrameSpec,
174
+ input: ByteBuffer,
175
+ dstWidth: Int,
176
+ dstHeight: Int,
177
+ dataType: DataType
178
+ ) {
179
+ val srcWidth = frame.width.toInt()
180
+ val srcHeight = frame.height.toInt()
181
+
182
+ val planes = frame.getPlanes()
183
+
184
+ val yPlane = planes[0]
185
+ val uPlane = planes[1]
186
+ val vPlane = planes[2]
187
+
188
+ val yBytes = yPlane.getPixelBuffer().toByteArray()
189
+ val uBytes = uPlane.getPixelBuffer().toByteArray()
190
+ val vBytes = vPlane.getPixelBuffer().toByteArray()
191
+
192
+ val yRowStride = yPlane.bytesPerRow.roundToInt()
193
+ val uRowStride = uPlane.bytesPerRow.roundToInt()
194
+ val vRowStride = vPlane.bytesPerRow.roundToInt()
195
+
196
+ input.rewind()
197
+
198
+ for (dy in 0 until dstHeight) {
199
+ // val sy = dy * srcHeight / dstHeight
200
+
201
+ for (dx in 0 until dstWidth) {
202
+ val srcX = dy * srcWidth / dstHeight
203
+ val srcY = srcHeight - 1 - (dx * srcHeight / dstWidth)
204
+
205
+ val yIndex = srcY * yRowStride + srcX
206
+
207
+ val uvX = srcX / 2
208
+ val uvY = srcY / 2
209
+
210
+ val uIndex = uvY * uRowStride + uvX * 2
211
+ val vIndex = uvY * vRowStride + uvX * 2
212
+
213
+ if (
214
+ yIndex >= yBytes.size ||
215
+ uIndex >= uBytes.size ||
216
+ vIndex >= vBytes.size
217
+ ) {
218
+ continue
219
+ }
220
+
221
+ val y = yBytes[yIndex].toInt() and 0xFF
222
+ val u = uBytes[uIndex].toInt() and 0xFF
223
+ val v = vBytes[vIndex].toInt() and 0xFF
224
+
225
+ val rFloat = y + 1.402f * (v - 128)
226
+ val gFloat = y - 0.344136f * (u - 128) - 0.714136f * (v - 128)
227
+ val bFloat = y + 1.772f * (u - 128)
228
+
229
+ val r = rFloat.roundToInt().coerceIn(0, 255)
230
+ val g = gFloat.roundToInt().coerceIn(0, 255)
231
+ val b = bFloat.roundToInt().coerceIn(0, 255)
232
+
233
+ when (dataType) {
234
+ DataType.FLOAT32 -> {
235
+ input.putFloat(r / 255f)
236
+ input.putFloat(g / 255f)
237
+ input.putFloat(b / 255f)
238
+ }
239
+
240
+ DataType.UINT8 -> {
241
+ input.put(r.toByte())
242
+ input.put(g.toByte())
243
+ input.put(b.toByte())
244
+ }
245
+
246
+ else -> error("Unsupported input type: $dataType")
247
+ }
248
+ }
249
+ }
250
+ input.rewind()
251
+ }
252
+
253
+ private fun yuvToRgb(y: Int, u: Int, v: Int): IntArray {
254
+ val yf = y.toFloat()
255
+ val uf = u.toFloat() - 128f
256
+ val vf = v.toFloat() - 128f
257
+
258
+ val r = (yf + 1.402f * vf).roundToInt().coerceIn(0, 255)
259
+ val g = (yf - 0.344136f * uf - 0.714136f * vf).roundToInt().coerceIn(0, 255)
260
+ val b = (yf + 1.772f * uf).roundToInt().coerceIn(0, 255)
261
+
262
+ return intArrayOf(r, g, b)
263
+ }
264
+
265
+ }
@@ -1,20 +1,20 @@
1
- package com.yolo;
2
-
3
- import com.facebook.react.bridge.NativeModule;
4
- import com.facebook.react.bridge.ReactApplicationContext;
5
- import com.facebook.react.module.model.ReactModuleInfoProvider;
6
- import com.facebook.react.BaseReactPackage;
7
- import com.margelo.nitro.yolo.YoloOnLoad;
8
-
9
-
10
- public class YoloPackage : BaseReactPackage() {
11
- override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
12
-
13
- override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
14
-
15
- companion object {
16
- init {
17
- YoloOnLoad.initializeNative();
18
- }
19
- }
20
- }
1
+ package com.yolo;
2
+
3
+ import com.facebook.react.bridge.NativeModule;
4
+ import com.facebook.react.bridge.ReactApplicationContext;
5
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
6
+ import com.facebook.react.BaseReactPackage;
7
+ import com.margelo.nitro.yolo.YoloOnLoad;
8
+
9
+
10
+ public class YoloPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
14
+
15
+ companion object {
16
+ init {
17
+ YoloOnLoad.initializeNative();
18
+ }
19
+ }
20
+ }
@@ -10,6 +10,12 @@ import java.nio.MappedByteBuffer
10
10
  import java.nio.channels.FileChannel
11
11
  import org.tensorflow.lite.support.common.FileUtil
12
12
 
13
+ import org.tensorflow.lite.DataType
14
+ import java.nio.ByteBuffer
15
+ import java.nio.ByteOrder
16
+ import kotlin.math.roundToInt
17
+ import org.tensorflow.lite.Interpreter
18
+
13
19
 
14
20
 
15
21
  /**
@@ -127,4 +133,29 @@ class YoloModelLoader {
127
133
 
128
134
  return file
129
135
  }
136
+
137
+
138
+ fun makeInputBuffer(interpreter: Interpreter): ByteBuffer {
139
+ val inputTensor = interpreter.getInputTensor(0)
140
+ val shape = inputTensor.shape() // usually [1, 640, 640, 3]
141
+ val dataType = inputTensor.dataType()
142
+
143
+ val batch = shape[0]
144
+ val height = shape[1]
145
+ val width = shape[2]
146
+ val channels = shape[3]
147
+
148
+ require(batch == 1)
149
+ require(channels == 3)
150
+
151
+ val bytesPerValue = when (dataType) {
152
+ DataType.FLOAT32 -> 4
153
+ DataType.UINT8 -> 1
154
+ else -> error("Unsupported input type: $dataType")
155
+ }
156
+
157
+ return ByteBuffer
158
+ .allocateDirect(batch * width * height * channels * bytesPerValue)
159
+ .order(ByteOrder.nativeOrder())
160
+ }
130
161
  }
@@ -0,0 +1,56 @@
1
+ package com.yolo.utils
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.graphics.Matrix
6
+ import com.margelo.nitro.camera.HybridFrameSpec
7
+ import java.io.ByteArrayOutputStream
8
+
9
+
10
+
11
+ object BitmapOrientationFixer {
12
+ fun fix(
13
+ jpegBytes: ByteArray,
14
+ frame: HybridFrameSpec,
15
+ quality: Int
16
+ ): ByteArray {
17
+ val bitmap = BitmapFactory.decodeByteArray(
18
+ jpegBytes,
19
+ 0,
20
+ jpegBytes.size
21
+ ) ?: return jpegBytes
22
+
23
+ val rotationDegrees = 90f //TODO: Get the actual rotation degrees from the frame metadata if available
24
+
25
+ val matrix = Matrix().apply {
26
+ postRotate(rotationDegrees)
27
+
28
+ if (frame.isMirrored) {
29
+ postScale(-1f, 1f)
30
+ }
31
+ }
32
+
33
+ val rotatedBitmap = Bitmap.createBitmap(
34
+ bitmap,
35
+ 0,
36
+ 0,
37
+ bitmap.width,
38
+ bitmap.height,
39
+ matrix,
40
+ true
41
+ )
42
+
43
+ val output = ByteArrayOutputStream()
44
+
45
+ rotatedBitmap.compress(
46
+ Bitmap.CompressFormat.JPEG,
47
+ quality,
48
+ output
49
+ )
50
+
51
+ bitmap.recycle()
52
+ rotatedBitmap.recycle()
53
+
54
+ return output.toByteArray()
55
+ }
56
+ }
@@ -0,0 +1,27 @@
1
+ package com.yolo.utils
2
+
3
+ import android.util.Log
4
+ import com.margelo.nitro.camera.HybridFrameSpec
5
+ import kotlin.math.roundToInt
6
+
7
+ object FrameJpegConverter {
8
+ private const val TAG = "YOLO_TAG_FrameJpegConverter"
9
+ fun toJpegBytes(frame : HybridFrameSpec, quality: Int = 80): ByteArray {
10
+ val width = frame.width.roundToInt()
11
+ val height = frame.height.roundToInt()
12
+
13
+ val nv21 = Yuv420ToNv21Converter.convert(frame, width, height)
14
+
15
+ val jpegBytes = Nv21JpegEncoder.encode(
16
+ nv21 = nv21,
17
+ width = width,
18
+ height = height,
19
+ quality = quality
20
+ )
21
+ return BitmapOrientationFixer.fix(
22
+ jpegBytes = jpegBytes,
23
+ frame = frame,
24
+ quality = quality
25
+ )
26
+ }
27
+ }
@@ -0,0 +1,29 @@
1
+ package com.yolo.utils
2
+
3
+ import android.util.Log
4
+ import com.margelo.nitro.camera.HybridFrameSpec
5
+
6
+
7
+ object FrameValidator {
8
+ private const val TAG = "YOLO_TAG_FrameValidator"
9
+ fun isValidYuv(frame: HybridFrameSpec): Boolean {
10
+ if (!frame.isValid) {
11
+ Log.e(TAG, "❌ Frame is not valid")
12
+ return false
13
+ }
14
+
15
+ val planes = frame.getPlanes()
16
+ if (planes.size < 3) {
17
+ Log.e(TAG, "❌ Expected 3 YUV planes, got ${planes.size}")
18
+ return false
19
+ }
20
+
21
+ planes.forEachIndexed { index, plane ->
22
+ if (!plane.isValid) {
23
+ Log.e(TAG, "❌ Plane $index is not valid")
24
+ return false
25
+ }
26
+ }
27
+ return true
28
+ }
29
+ }
@@ -0,0 +1,34 @@
1
+ package com.yolo.utils
2
+
3
+ import android.graphics.ImageFormat
4
+ import android.graphics.Rect
5
+ import android.graphics.YuvImage
6
+ import java.io.ByteArrayOutputStream
7
+
8
+
9
+ object Nv21JpegEncoder {
10
+ fun encode(
11
+ nv21: ByteArray,
12
+ width: Int,
13
+ height: Int,
14
+ quality: Int
15
+ ): ByteArray {
16
+ val yuvImage = YuvImage(
17
+ nv21,
18
+ ImageFormat.NV21,
19
+ width,
20
+ height,
21
+ null
22
+ )
23
+
24
+ val output = ByteArrayOutputStream()
25
+
26
+ yuvImage.compressToJpeg(
27
+ Rect(0, 0, width, height),
28
+ quality,
29
+ output
30
+ )
31
+
32
+ return output.toByteArray()
33
+ }
34
+ }