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 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
- - **Top 3 most frequent colors (RGB)**
6
- - **Top 3 brightest colors (RGB)**
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 topColors = ArrayList<Pair<Int, Int>>(3)
204
- val topBright = ArrayList<Pair<Int, Int>>(3)
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 < 3) {
255
+ if (i < maxSize) {
252
256
  list.add(i, idx to value)
253
- if (list.size > 3) list.removeAt(3)
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 = YuvToBitmapConverter.convert(image)
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 top = stats.sorted { $0.count > $1.count }.prefix(3).map { ["r": $0.r, "g": $0.g, "b": $0.b] }
338
- let bright = stats.sorted { $0.brightness > $1.brightness }.prefix(3).map { ["r": $0.r, "g": $0.g, "b": $0.b] }
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
  }
@@ -14,6 +14,8 @@ export type AnalysisOptions = {
14
14
  enableMotionDetection?: boolean;
15
15
  motionThreshold?: number;
16
16
  roi?: ROIConfig;
17
+ maxTopColors?: number;
18
+ maxBrightestColors?: number;
17
19
  };
18
20
  export type MotionResult = {
19
21
  score: number;
@@ -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;CAChB,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"}
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": "0.1.0",
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
- }