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.
- package/LICENSE +20 -20
- package/README.md +51 -51
- package/Yolo.podspec +31 -31
- package/android/CMakeLists.txt +35 -32
- package/android/build.gradle +154 -152
- package/android/fix-prefab.gradle +50 -50
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifest.xml +3 -2
- package/android/src/main/cpp/cpp-adapter.cpp +8 -8
- package/android/src/main/java/com/yolo/HybridYolo.kt +265 -42
- package/android/src/main/java/com/yolo/YoloPackage.kt +20 -20
- package/android/src/main/java/com/yolo/loader/YoloModelLoader.kt +31 -0
- package/android/src/main/java/com/yolo/utils/BitmapOrientationFixer.kt +56 -0
- package/android/src/main/java/com/yolo/utils/FrameJpegConverter.kt +27 -0
- package/android/src/main/java/com/yolo/utils/FrameValidator.kt +29 -0
- package/android/src/main/java/com/yolo/utils/Nv21JpegEncoder.kt +34 -0
- package/android/src/main/java/com/yolo/utils/Yuv420ToNv21Converter.kt +62 -0
- package/ios/Bridge.h +8 -8
- package/ios/HybridYolo.swift +14 -14
- package/lib/commonjs/index.js +10 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +10 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/yolo.nitro.d.ts +15 -0
- package/lib/typescript/src/specs/yolo.nitro.d.ts.map +1 -1
- package/nitro.json +29 -29
- package/nitrogen/generated/android/c++/JBoundingBox.hpp +69 -0
- package/nitrogen/generated/android/c++/JDetection.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridYoloSpec.cpp +33 -1
- package/nitrogen/generated/android/c++/JHybridYoloSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/BoundingBox.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/Detection.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/yolo/HybridYoloSpec.kt +9 -0
- package/nitrogen/generated/ios/Yolo-Swift-Cxx-Bridge.cpp +11 -0
- package/nitrogen/generated/ios/Yolo-Swift-Cxx-Bridge.hpp +54 -0
- package/nitrogen/generated/ios/Yolo-Swift-Cxx-Umbrella.hpp +12 -0
- package/nitrogen/generated/ios/c++/HybridYoloSpecSwift.hpp +27 -1
- package/nitrogen/generated/ios/swift/BoundingBox.swift +44 -0
- package/nitrogen/generated/ios/swift/Detection.swift +39 -0
- package/nitrogen/generated/ios/swift/HybridYoloSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridYoloSpec_cxx.swift +39 -0
- package/nitrogen/generated/shared/c++/BoundingBox.hpp +95 -0
- package/nitrogen/generated/shared/c++/Detection.hpp +92 -0
- package/nitrogen/generated/shared/c++/HybridYoloSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridYoloSpec.hpp +10 -1
- package/package.json +127 -122
- package/src/index.ts +11 -11
- 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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|