react-native-camera-vision-pixel-colors 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/android/src/main/java/com/cameravisionpixelcolors/ImageToBitmapConverter.kt +92 -0
- package/android/src/main/java/com/cameravisionpixelcolors/PixelAnalyzerEngine.kt +12 -8
- package/android/src/main/java/com/cameravisionpixelcolors/PixelColorsFrameProcessor.kt +7 -2
- package/ios/PixelAnalyzerEngine.swift +8 -4
- package/ios/PixelColorsFrameProcessor.swift +8 -0
- package/lib/typescript/src/specs/camera-vision-pixel-colors.nitro.d.ts +2 -0
- package/lib/typescript/src/specs/camera-vision-pixel-colors.nitro.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/specs/camera-vision-pixel-colors.nitro.ts +2 -0
- package/android/src/main/java/com/cameravisionpixelcolors/YuvToBitmapConverter.kt +0 -33
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
High-performance **Vision Camera Frame Processor** for React Native (Expo compatible) that analyzes pixel colors **in real time**.
|
|
4
4
|
This plugin extracts:
|
|
5
|
-
- **
|
|
6
|
-
- **
|
|
5
|
+
- **Up to 10 most frequent colors (RGB)** (configurable)
|
|
6
|
+
- **Up to 10 brightest colors (RGB)** (configurable)
|
|
7
7
|
- **Total number of unique colors**
|
|
8
8
|
- **ROI analysis (configurable region)**
|
|
9
9
|
- **Motion detection (frame diff)**
|
|
@@ -97,6 +97,10 @@ const frameProcessor = useFrameProcessor((frame) => {
|
|
|
97
97
|
// Enable motion detection
|
|
98
98
|
enableMotionDetection: true,
|
|
99
99
|
motionThreshold: 0.1, // 0-1, default: 0.1
|
|
100
|
+
|
|
101
|
+
// Configure color counts (1-10, default: 3)
|
|
102
|
+
maxTopColors: 5,
|
|
103
|
+
maxBrightestColors: 5,
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
const result = analyzePixelColors(frame, options);
|
|
@@ -145,6 +149,8 @@ type AnalysisOptions = {
|
|
|
145
149
|
enableMotionDetection?: boolean; // default: false
|
|
146
150
|
motionThreshold?: number; // default: 0.1
|
|
147
151
|
roi?: ROIConfig; // if provided, analyze only this region
|
|
152
|
+
maxTopColors?: number; // default: 3, range: 1-10
|
|
153
|
+
maxBrightestColors?: number; // default: 3, range: 1-10
|
|
148
154
|
};
|
|
149
155
|
|
|
150
156
|
type MotionResult = {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
package com.cameravisionpixelcolors
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.ImageFormat
|
|
6
|
+
import android.graphics.PixelFormat
|
|
7
|
+
import android.graphics.Rect
|
|
8
|
+
import android.graphics.YuvImage
|
|
9
|
+
import android.media.Image
|
|
10
|
+
import java.io.ByteArrayOutputStream
|
|
11
|
+
|
|
12
|
+
object ImageToBitmapConverter {
|
|
13
|
+
fun convert(image: Image): Bitmap {
|
|
14
|
+
return when (image.format) {
|
|
15
|
+
ImageFormat.YUV_420_888 -> convertYuv420(image)
|
|
16
|
+
PixelFormat.RGBA_8888 -> convertRgba8888(image)
|
|
17
|
+
PixelFormat.RGB_888 -> convertRgb888(image)
|
|
18
|
+
else -> throw IllegalArgumentException("Unsupported image format: ${image.format}")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private fun convertYuv420(image: Image): Bitmap {
|
|
23
|
+
val yBuffer = image.planes[0].buffer
|
|
24
|
+
val uBuffer = image.planes[1].buffer
|
|
25
|
+
val vBuffer = image.planes[2].buffer
|
|
26
|
+
|
|
27
|
+
val ySize = yBuffer.remaining()
|
|
28
|
+
val uSize = uBuffer.remaining()
|
|
29
|
+
val vSize = vBuffer.remaining()
|
|
30
|
+
|
|
31
|
+
val nv21 = ByteArray(ySize + uSize + vSize)
|
|
32
|
+
|
|
33
|
+
yBuffer.get(nv21, 0, ySize)
|
|
34
|
+
vBuffer.get(nv21, ySize, vSize)
|
|
35
|
+
uBuffer.get(nv21, ySize + vSize, uSize)
|
|
36
|
+
|
|
37
|
+
val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
|
|
38
|
+
val out = ByteArrayOutputStream()
|
|
39
|
+
yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 90, out)
|
|
40
|
+
val imageBytes = out.toByteArray()
|
|
41
|
+
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun convertRgba8888(image: Image): Bitmap {
|
|
45
|
+
val plane = image.planes[0]
|
|
46
|
+
val buffer = plane.buffer
|
|
47
|
+
val rowStride = plane.rowStride
|
|
48
|
+
val pixelStride = plane.pixelStride
|
|
49
|
+
val width = image.width
|
|
50
|
+
val height = image.height
|
|
51
|
+
|
|
52
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
53
|
+
val pixels = IntArray(width * height)
|
|
54
|
+
|
|
55
|
+
for (y in 0 until height) {
|
|
56
|
+
for (x in 0 until width) {
|
|
57
|
+
val offset = y * rowStride + x * pixelStride
|
|
58
|
+
val r = buffer.get(offset).toInt() and 0xFF
|
|
59
|
+
val g = buffer.get(offset + 1).toInt() and 0xFF
|
|
60
|
+
val b = buffer.get(offset + 2).toInt() and 0xFF
|
|
61
|
+
val a = buffer.get(offset + 3).toInt() and 0xFF
|
|
62
|
+
pixels[y * width + x] = (a shl 24) or (r shl 16) or (g shl 8) or b
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
|
66
|
+
return bitmap
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fun convertRgb888(image: Image): Bitmap {
|
|
70
|
+
val plane = image.planes[0]
|
|
71
|
+
val buffer = plane.buffer
|
|
72
|
+
val rowStride = plane.rowStride
|
|
73
|
+
val pixelStride = plane.pixelStride
|
|
74
|
+
val width = image.width
|
|
75
|
+
val height = image.height
|
|
76
|
+
|
|
77
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
78
|
+
val pixels = IntArray(width * height)
|
|
79
|
+
|
|
80
|
+
for (y in 0 until height) {
|
|
81
|
+
for (x in 0 until width) {
|
|
82
|
+
val offset = y * rowStride + x * pixelStride
|
|
83
|
+
val r = buffer.get(offset).toInt() and 0xFF
|
|
84
|
+
val g = buffer.get(offset + 1).toInt() and 0xFF
|
|
85
|
+
val b = buffer.get(offset + 2).toInt() and 0xFF
|
|
86
|
+
pixels[y * width + x] = 0xFF000000.toInt() or (r shl 16) or (g shl 8) or b
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
|
90
|
+
return bitmap
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -12,7 +12,9 @@ import kotlin.math.min
|
|
|
12
12
|
data class AnalysisOptions(
|
|
13
13
|
val enableMotionDetection: Boolean = false,
|
|
14
14
|
val motionThreshold: Float = 0.1f,
|
|
15
|
-
val roi: ROIConfig? = null
|
|
15
|
+
val roi: ROIConfig? = null,
|
|
16
|
+
val maxTopColors: Int = 3,
|
|
17
|
+
val maxBrightestColors: Int = 3
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
data class ROIConfig(
|
|
@@ -200,16 +202,18 @@ object PixelAnalyzerEngine {
|
|
|
200
202
|
localBrightnessSum[idx] += brightness
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
val
|
|
204
|
-
val
|
|
205
|
+
val clampedTop = max(1, min(10, options.maxTopColors))
|
|
206
|
+
val clampedBright = max(1, min(10, options.maxBrightestColors))
|
|
207
|
+
val topColors = ArrayList<Pair<Int, Int>>(clampedTop)
|
|
208
|
+
val topBright = ArrayList<Pair<Int, Int>>(clampedBright)
|
|
205
209
|
var uniqueCount = 0
|
|
206
210
|
for (i in 0 until BUCKETS) {
|
|
207
211
|
val count = localHistogram[i]
|
|
208
212
|
if (count == 0) continue
|
|
209
213
|
uniqueCount++
|
|
210
|
-
insertTop(topColors, i, count)
|
|
214
|
+
insertTop(topColors, i, count, clampedTop)
|
|
211
215
|
val avgBrightness = localBrightnessSum[i] / max(count, 1)
|
|
212
|
-
insertTop(topBright, i, avgBrightness)
|
|
216
|
+
insertTop(topBright, i, avgBrightness, clampedBright)
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
fun decode(idx: Int): Map<String, Int> {
|
|
@@ -245,12 +249,12 @@ object PixelAnalyzerEngine {
|
|
|
245
249
|
cachedResult.set(result)
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
private fun insertTop(list: MutableList<Pair<Int, Int>>, idx: Int, value: Int) {
|
|
252
|
+
private fun insertTop(list: MutableList<Pair<Int, Int>>, idx: Int, value: Int, maxSize: Int) {
|
|
249
253
|
var i = 0
|
|
250
254
|
while (i < list.size && value <= list[i].second) i++
|
|
251
|
-
if (i <
|
|
255
|
+
if (i < maxSize) {
|
|
252
256
|
list.add(i, idx to value)
|
|
253
|
-
if (list.size >
|
|
257
|
+
if (list.size > maxSize) list.removeAt(maxSize)
|
|
254
258
|
}
|
|
255
259
|
}
|
|
256
260
|
}
|
|
@@ -7,7 +7,7 @@ import com.mrousavy.camera.frameprocessors.VisionCameraProxy
|
|
|
7
7
|
class PixelColorsFrameProcessor(proxy: VisionCameraProxy, options: Map<String, Any>?) : FrameProcessorPlugin() {
|
|
8
8
|
override fun callback(frame: Frame, arguments: Map<String, Any>?): Any {
|
|
9
9
|
val image = frame.image ?: return emptyMap<String, Any>()
|
|
10
|
-
val bitmap =
|
|
10
|
+
val bitmap = ImageToBitmapConverter.convert(image)
|
|
11
11
|
|
|
12
12
|
// Parse options from arguments
|
|
13
13
|
val analysisOptions = parseOptions(arguments)
|
|
@@ -31,10 +31,15 @@ class PixelColorsFrameProcessor(proxy: VisionCameraProxy, options: Map<String, A
|
|
|
31
31
|
ROIConfig(x, y, width, height)
|
|
32
32
|
} else null
|
|
33
33
|
|
|
34
|
+
val maxTopColors = (optionsMap["maxTopColors"] as? Number)?.toInt() ?: 3
|
|
35
|
+
val maxBrightestColors = (optionsMap["maxBrightestColors"] as? Number)?.toInt() ?: 3
|
|
36
|
+
|
|
34
37
|
return AnalysisOptions(
|
|
35
38
|
enableMotionDetection = enableMotionDetection,
|
|
36
39
|
motionThreshold = motionThreshold,
|
|
37
|
-
roi = roi
|
|
40
|
+
roi = roi,
|
|
41
|
+
maxTopColors = maxTopColors,
|
|
42
|
+
maxBrightestColors = maxBrightestColors
|
|
38
43
|
)
|
|
39
44
|
}
|
|
40
45
|
}
|
|
@@ -8,6 +8,8 @@ struct AnalysisOptionsNative {
|
|
|
8
8
|
var enableMotionDetection: Bool = false
|
|
9
9
|
var motionThreshold: Float = 0.1
|
|
10
10
|
var roi: (x: Float, y: Float, width: Float, height: Float)?
|
|
11
|
+
var maxTopColors: Int = 3
|
|
12
|
+
var maxBrightestColors: Int = 3
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
final class PixelAnalyzerEngine {
|
|
@@ -305,7 +307,7 @@ final class PixelAnalyzerEngine {
|
|
|
305
307
|
format: .RGBA32,
|
|
306
308
|
colorSpace: CGColorSpaceCreateDeviceRGB())
|
|
307
309
|
|
|
308
|
-
var result = reduceHistogram(bitmap)
|
|
310
|
+
var result = reduceHistogram(bitmap, maxTopColors: options.maxTopColors, maxBrightestColors: options.maxBrightestColors)
|
|
309
311
|
|
|
310
312
|
// Add ROI applied flag
|
|
311
313
|
if options.roi != nil {
|
|
@@ -321,7 +323,7 @@ final class PixelAnalyzerEngine {
|
|
|
321
323
|
return result
|
|
322
324
|
}
|
|
323
325
|
|
|
324
|
-
private func reduceHistogram(_ data: [UInt32]) -> [String: Any] {
|
|
326
|
+
private func reduceHistogram(_ data: [UInt32], maxTopColors: Int = 3, maxBrightestColors: Int = 3) -> [String: Any] {
|
|
325
327
|
struct Stat { let r: Int; let g: Int; let b: Int; let count: Int; let brightness: Float }
|
|
326
328
|
var stats: [Stat] = []
|
|
327
329
|
for i in stride(from: 0, to: data.count, by: 4) {
|
|
@@ -334,8 +336,10 @@ final class PixelAnalyzerEngine {
|
|
|
334
336
|
stats.append(Stat(r: r, g: g, b: b, count: count, brightness: brightness))
|
|
335
337
|
}
|
|
336
338
|
|
|
337
|
-
let
|
|
338
|
-
let
|
|
339
|
+
let clampedTop = max(1, min(10, maxTopColors))
|
|
340
|
+
let clampedBright = max(1, min(10, maxBrightestColors))
|
|
341
|
+
let top = stats.sorted { $0.count > $1.count }.prefix(clampedTop).map { ["r": $0.r, "g": $0.g, "b": $0.b] }
|
|
342
|
+
let bright = stats.sorted { $0.brightness > $1.brightness }.prefix(clampedBright).map { ["r": $0.r, "g": $0.g, "b": $0.b] }
|
|
339
343
|
|
|
340
344
|
return [
|
|
341
345
|
"uniqueColorCount": stats.count,
|
|
@@ -45,6 +45,14 @@ public final class PixelColorsFrameProcessor: FrameProcessorPlugin {
|
|
|
45
45
|
options.roi = (x: Float(x), y: Float(y), width: Float(width), height: Float(height))
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
if let maxTopColors = optionsDict["maxTopColors"] as? Int {
|
|
49
|
+
options.maxTopColors = maxTopColors
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if let maxBrightestColors = optionsDict["maxBrightestColors"] as? Int {
|
|
53
|
+
options.maxBrightestColors = maxBrightestColors
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
return options
|
|
49
57
|
}
|
|
50
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"camera-vision-pixel-colors.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/camera-vision-pixel-colors.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,MAAM,QAAQ,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1D,MAAM,MAAM,SAAS,GAAG;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,GAAG,CAAC,EAAE,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"camera-vision-pixel-colors.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/camera-vision-pixel-colors.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,MAAM,QAAQ,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1D,MAAM,MAAM,SAAS,GAAG;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,eAAe,EAAE,QAAQ,EAAE,CAAA;IAC3B,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,WAAW,CAAA;CAClB,CAAA;AAED,MAAM,WAAW,uBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;CAChE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-camera-vision-pixel-colors",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Vision Camera Frame Processor plugin for real-time pixel color analysis - extract dominant colors, brightness, ROI, motion detection",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -13,6 +13,8 @@ export type AnalysisOptions = {
|
|
|
13
13
|
enableMotionDetection?: boolean // default: false
|
|
14
14
|
motionThreshold?: number // default: 0.1
|
|
15
15
|
roi?: ROIConfig // if provided, analyze only this region
|
|
16
|
+
maxTopColors?: number // default: 3, range: 1-10
|
|
17
|
+
maxBrightestColors?: number // default: 3, range: 1-10
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type MotionResult = {
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
package com.cameravisionpixelcolors
|
|
2
|
-
|
|
3
|
-
import android.graphics.Bitmap
|
|
4
|
-
import android.graphics.BitmapFactory
|
|
5
|
-
import android.graphics.ImageFormat
|
|
6
|
-
import android.graphics.Rect
|
|
7
|
-
import android.graphics.YuvImage
|
|
8
|
-
import android.media.Image
|
|
9
|
-
import java.io.ByteArrayOutputStream
|
|
10
|
-
|
|
11
|
-
object YuvToBitmapConverter {
|
|
12
|
-
fun convert(image: Image): Bitmap {
|
|
13
|
-
val yBuffer = image.planes[0].buffer
|
|
14
|
-
val uBuffer = image.planes[1].buffer
|
|
15
|
-
val vBuffer = image.planes[2].buffer
|
|
16
|
-
|
|
17
|
-
val ySize = yBuffer.remaining()
|
|
18
|
-
val uSize = uBuffer.remaining()
|
|
19
|
-
val vSize = vBuffer.remaining()
|
|
20
|
-
|
|
21
|
-
val nv21 = ByteArray(ySize + uSize + vSize)
|
|
22
|
-
|
|
23
|
-
yBuffer.get(nv21, 0, ySize)
|
|
24
|
-
vBuffer.get(nv21, ySize, vSize)
|
|
25
|
-
uBuffer.get(nv21, ySize + vSize, uSize)
|
|
26
|
-
|
|
27
|
-
val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
|
|
28
|
-
val out = ByteArrayOutputStream()
|
|
29
|
-
yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 90, out)
|
|
30
|
-
val imageBytes = out.toByteArray()
|
|
31
|
-
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
32
|
-
}
|
|
33
|
-
}
|