react-native-camera-vision-pixel-colors 0.1.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.
Files changed (79) hide show
  1. package/CameraVisionPixelColors.podspec +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +190 -0
  4. package/android/CMakeLists.txt +32 -0
  5. package/android/build.gradle +151 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  10. package/android/src/main/java/com/cameravisionpixelcolors/CameraVisionPixelColorsPackage.kt +24 -0
  11. package/android/src/main/java/com/cameravisionpixelcolors/PixelAnalyzerEngine.kt +256 -0
  12. package/android/src/main/java/com/cameravisionpixelcolors/PixelColorsFrameProcessor.kt +40 -0
  13. package/android/src/main/java/com/cameravisionpixelcolors/YuvToBitmapConverter.kt +33 -0
  14. package/android/src/main/java/com/margelo/nitro/cameravisionpixelcolors/HybridCameraVisionPixelColors.kt +50 -0
  15. package/app.plugin.js +1 -0
  16. package/ios/Bridge.h +8 -0
  17. package/ios/HybridCameraVisionPixelColors.swift +53 -0
  18. package/ios/PixelAnalyzerEngine.swift +346 -0
  19. package/ios/PixelColorsFrameProcessor.m +5 -0
  20. package/ios/PixelColorsFrameProcessor.swift +50 -0
  21. package/lib/commonjs/index.js +28 -0
  22. package/lib/commonjs/index.js.map +1 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/commonjs/specs/camera-vision-pixel-colors.nitro.js +6 -0
  25. package/lib/commonjs/specs/camera-vision-pixel-colors.nitro.js.map +1 -0
  26. package/lib/module/index.js +23 -0
  27. package/lib/module/index.js.map +1 -0
  28. package/lib/module/specs/camera-vision-pixel-colors.nitro.js +4 -0
  29. package/lib/module/specs/camera-vision-pixel-colors.nitro.js.map +1 -0
  30. package/lib/typescript/src/index.d.ts +6 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -0
  32. package/lib/typescript/src/specs/camera-vision-pixel-colors.nitro.d.ts +40 -0
  33. package/lib/typescript/src/specs/camera-vision-pixel-colors.nitro.d.ts.map +1 -0
  34. package/nitro.json +25 -0
  35. package/nitrogen/generated/.gitattributes +1 -0
  36. package/nitrogen/generated/android/CameraVisionPixelColors+autolinking.cmake +81 -0
  37. package/nitrogen/generated/android/CameraVisionPixelColors+autolinking.gradle +27 -0
  38. package/nitrogen/generated/android/CameraVisionPixelColorsOnLoad.cpp +44 -0
  39. package/nitrogen/generated/android/CameraVisionPixelColorsOnLoad.hpp +25 -0
  40. package/nitrogen/generated/android/c++/JHybridCameraVisionPixelColorsSpec.cpp +90 -0
  41. package/nitrogen/generated/android/c++/JHybridCameraVisionPixelColorsSpec.hpp +66 -0
  42. package/nitrogen/generated/android/c++/JImageData.hpp +66 -0
  43. package/nitrogen/generated/android/c++/JMotionResult.hpp +61 -0
  44. package/nitrogen/generated/android/c++/JPixelColorsResult.hpp +114 -0
  45. package/nitrogen/generated/android/c++/JRGBColor.hpp +65 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cameravisionpixelcolors/CameraVisionPixelColorsOnLoad.kt +35 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cameravisionpixelcolors/HybridCameraVisionPixelColorsSpec.kt +58 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cameravisionpixelcolors/ImageData.kt +44 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cameravisionpixelcolors/MotionResult.kt +41 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cameravisionpixelcolors/PixelColorsResult.kt +50 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/cameravisionpixelcolors/RGBColor.kt +44 -0
  52. package/nitrogen/generated/ios/CameraVisionPixelColors+autolinking.rb +60 -0
  53. package/nitrogen/generated/ios/CameraVisionPixelColors-Swift-Cxx-Bridge.cpp +49 -0
  54. package/nitrogen/generated/ios/CameraVisionPixelColors-Swift-Cxx-Bridge.hpp +162 -0
  55. package/nitrogen/generated/ios/CameraVisionPixelColors-Swift-Cxx-Umbrella.hpp +59 -0
  56. package/nitrogen/generated/ios/CameraVisionPixelColorsAutolinking.mm +33 -0
  57. package/nitrogen/generated/ios/CameraVisionPixelColorsAutolinking.swift +26 -0
  58. package/nitrogen/generated/ios/c++/HybridCameraVisionPixelColorsSpecSwift.cpp +11 -0
  59. package/nitrogen/generated/ios/c++/HybridCameraVisionPixelColorsSpecSwift.hpp +99 -0
  60. package/nitrogen/generated/ios/swift/Func_void_PixelColorsResult.swift +47 -0
  61. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  62. package/nitrogen/generated/ios/swift/HybridCameraVisionPixelColorsSpec.swift +56 -0
  63. package/nitrogen/generated/ios/swift/HybridCameraVisionPixelColorsSpec_cxx.swift +146 -0
  64. package/nitrogen/generated/ios/swift/ImageData.swift +40 -0
  65. package/nitrogen/generated/ios/swift/MotionResult.swift +35 -0
  66. package/nitrogen/generated/ios/swift/PixelColorsResult.swift +81 -0
  67. package/nitrogen/generated/ios/swift/RGBColor.swift +40 -0
  68. package/nitrogen/generated/shared/c++/HybridCameraVisionPixelColorsSpec.cpp +21 -0
  69. package/nitrogen/generated/shared/c++/HybridCameraVisionPixelColorsSpec.hpp +67 -0
  70. package/nitrogen/generated/shared/c++/ImageData.hpp +91 -0
  71. package/nitrogen/generated/shared/c++/MotionResult.hpp +87 -0
  72. package/nitrogen/generated/shared/c++/PixelColorsResult.hpp +105 -0
  73. package/nitrogen/generated/shared/c++/RGBColor.hpp +91 -0
  74. package/package.json +143 -0
  75. package/plugin/withPixelColors.js +12 -0
  76. package/plugin/withPixelColorsAndroid.js +11 -0
  77. package/plugin/withPixelColorsIOS.js +11 -0
  78. package/src/index.ts +42 -0
  79. package/src/specs/camera-vision-pixel-colors.nitro.ts +40 -0
@@ -0,0 +1,256 @@
1
+ package com.cameravisionpixelcolors
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Rect
5
+ import java.nio.ByteBuffer
6
+ import java.util.concurrent.Executors
7
+ import java.util.concurrent.atomic.AtomicReference
8
+ import kotlin.math.abs
9
+ import kotlin.math.max
10
+ import kotlin.math.min
11
+
12
+ data class AnalysisOptions(
13
+ val enableMotionDetection: Boolean = false,
14
+ val motionThreshold: Float = 0.1f,
15
+ val roi: ROIConfig? = null
16
+ )
17
+
18
+ data class ROIConfig(
19
+ val x: Float,
20
+ val y: Float,
21
+ val width: Float,
22
+ val height: Float
23
+ )
24
+
25
+ object PixelAnalyzerEngine {
26
+ private const val BUCKETS = 32 * 32 * 32
27
+ private const val MAX_RAW_PIXEL_DIMENSION = 1920
28
+ private val executor = Executors.newSingleThreadExecutor()
29
+ private val cachedResult = AtomicReference<Map<String, Any>>(mapOf(
30
+ "uniqueColorCount" to 0,
31
+ "topColors" to emptyList<Map<String, Int>>(),
32
+ "brightestColors" to emptyList<Map<String, Int>>()
33
+ ))
34
+
35
+ private val histogram = IntArray(BUCKETS)
36
+ private val brightnessSum = IntArray(BUCKETS)
37
+
38
+ // Motion detection state
39
+ private var previousGrayscale: IntArray? = null
40
+ private var previousWidth: Int = 0
41
+ private var previousHeight: Int = 0
42
+
43
+ fun analyzeAsync(bitmap: Bitmap, options: AnalysisOptions = AnalysisOptions()) {
44
+ executor.execute {
45
+ analyze(bitmap, options)
46
+ }
47
+ }
48
+
49
+ fun analyzeSync(): Map<String, Any> {
50
+ return cachedResult.get()
51
+ }
52
+
53
+ // Calculate ROI in pixel coordinates
54
+ private fun calculateROI(config: ROIConfig, width: Int, height: Int): Rect {
55
+ val x = (config.x * width).toInt()
56
+ val y = (config.y * height).toInt()
57
+ val w = max(1, (config.width * width).toInt())
58
+ val h = max(1, (config.height * height).toInt())
59
+ return Rect(x, y, min(x + w, width), min(y + h, height))
60
+ }
61
+
62
+ // Calculate motion score between current and previous frame
63
+ private fun calculateMotion(bitmap: Bitmap, threshold: Float): Map<String, Any> {
64
+ val width = bitmap.width
65
+ val height = bitmap.height
66
+ val totalPixels = width * height
67
+ val pixels = IntArray(totalPixels)
68
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
69
+
70
+ // Convert to grayscale
71
+ val currentGrayscale = IntArray(totalPixels) { i ->
72
+ val px = pixels[i]
73
+ val r = (px shr 16) and 0xFF
74
+ val g = (px shr 8) and 0xFF
75
+ val b = px and 0xFF
76
+ ((0.299 * r + 0.587 * g + 0.114 * b).toInt())
77
+ }
78
+
79
+ val prev = previousGrayscale
80
+ if (prev == null || previousWidth != width || previousHeight != height) {
81
+ // First frame - save and return zero motion
82
+ previousGrayscale = currentGrayscale
83
+ previousWidth = width
84
+ previousHeight = height
85
+ return mapOf("score" to 0.0, "hasMotion" to false)
86
+ }
87
+
88
+ // Count pixels exceeding threshold
89
+ val thresholdValue = (threshold * 255).toInt()
90
+ var changedCount = 0
91
+ for (i in 0 until totalPixels) {
92
+ if (abs(currentGrayscale[i] - prev[i]) > thresholdValue) {
93
+ changedCount++
94
+ }
95
+ }
96
+
97
+ val score = changedCount.toDouble() / totalPixels
98
+
99
+ // Swap buffers
100
+ previousGrayscale = currentGrayscale
101
+ previousWidth = width
102
+ previousHeight = height
103
+
104
+ return mapOf("score" to score, "hasMotion" to (score > threshold))
105
+ }
106
+
107
+ // Extract raw RGBA pixels with optional scaling
108
+ private fun extractRawPixels(bitmap: Bitmap, roi: Rect?): ByteBuffer? {
109
+ var workBitmap = bitmap
110
+
111
+ // Apply ROI if provided
112
+ if (roi != null) {
113
+ workBitmap = Bitmap.createBitmap(
114
+ bitmap,
115
+ roi.left,
116
+ roi.top,
117
+ roi.width(),
118
+ roi.height()
119
+ )
120
+ }
121
+
122
+ var width = workBitmap.width
123
+ var height = workBitmap.height
124
+
125
+ // Scale down if exceeds 1080p
126
+ if (width > MAX_RAW_PIXEL_DIMENSION || height > MAX_RAW_PIXEL_DIMENSION) {
127
+ val scale = min(
128
+ MAX_RAW_PIXEL_DIMENSION.toFloat() / width,
129
+ MAX_RAW_PIXEL_DIMENSION.toFloat() / height
130
+ )
131
+ val newWidth = (width * scale).toInt()
132
+ val newHeight = (height * scale).toInt()
133
+ workBitmap = Bitmap.createScaledBitmap(workBitmap, newWidth, newHeight, true)
134
+ width = newWidth
135
+ height = newHeight
136
+ }
137
+
138
+ // Extract RGBA data
139
+ val pixels = IntArray(width * height)
140
+ workBitmap.getPixels(pixels, 0, width, 0, 0, width, height)
141
+
142
+ val buffer = ByteBuffer.allocateDirect(width * height * 4)
143
+ for (px in pixels) {
144
+ // ARGB -> RGBA conversion
145
+ val a = (px shr 24) and 0xFF
146
+ val r = (px shr 16) and 0xFF
147
+ val g = (px shr 8) and 0xFF
148
+ val b = px and 0xFF
149
+ buffer.put(r.toByte())
150
+ buffer.put(g.toByte())
151
+ buffer.put(b.toByte())
152
+ buffer.put(a.toByte())
153
+ }
154
+ buffer.rewind()
155
+
156
+ return buffer
157
+ }
158
+
159
+ fun analyzeImageData(width: Int, height: Int, data: ByteArray): Map<String, Any> {
160
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
161
+ bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data))
162
+ return analyzeImmediate(bitmap)
163
+ }
164
+
165
+ private fun analyzeImmediate(bitmap: Bitmap, options: AnalysisOptions = AnalysisOptions()): Map<String, Any> {
166
+ var workBitmap = bitmap
167
+ var roiRect: Rect? = null
168
+
169
+ // Apply ROI if configured
170
+ if (options.roi != null) {
171
+ roiRect = calculateROI(options.roi, bitmap.width, bitmap.height)
172
+ workBitmap = Bitmap.createBitmap(
173
+ bitmap,
174
+ roiRect.left,
175
+ roiRect.top,
176
+ roiRect.width(),
177
+ roiRect.height()
178
+ )
179
+ }
180
+
181
+ val width = workBitmap.width
182
+ val height = workBitmap.height
183
+ val size = width * height
184
+ val pixels = IntArray(size)
185
+ workBitmap.getPixels(pixels, 0, width, 0, 0, width, height)
186
+
187
+ val localHistogram = IntArray(BUCKETS)
188
+ val localBrightnessSum = IntArray(BUCKETS)
189
+
190
+ for (px in pixels) {
191
+ val r = (px shr 16) and 0xFF
192
+ val g = (px shr 8) and 0xFF
193
+ val b = px and 0xFF
194
+ val rq = r shr 3
195
+ val gq = g shr 3
196
+ val bq = b shr 3
197
+ val idx = (rq shl 10) or (gq shl 5) or bq
198
+ localHistogram[idx]++
199
+ val brightness = (2126 * r + 7152 * g + 722 * b) / 10000
200
+ localBrightnessSum[idx] += brightness
201
+ }
202
+
203
+ val topColors = ArrayList<Pair<Int, Int>>(3)
204
+ val topBright = ArrayList<Pair<Int, Int>>(3)
205
+ var uniqueCount = 0
206
+ for (i in 0 until BUCKETS) {
207
+ val count = localHistogram[i]
208
+ if (count == 0) continue
209
+ uniqueCount++
210
+ insertTop(topColors, i, count)
211
+ val avgBrightness = localBrightnessSum[i] / max(count, 1)
212
+ insertTop(topBright, i, avgBrightness)
213
+ }
214
+
215
+ fun decode(idx: Int): Map<String, Int> {
216
+ return mapOf(
217
+ "r" to ((idx shr 10) and 31 shl 3),
218
+ "g" to ((idx shr 5) and 31 shl 3),
219
+ "b" to ((idx and 31) shl 3)
220
+ )
221
+ }
222
+
223
+ val result = mutableMapOf<String, Any>(
224
+ "uniqueColorCount" to uniqueCount,
225
+ "topColors" to topColors.map { decode(it.first) },
226
+ "brightestColors" to topBright.map { decode(it.first) }
227
+ )
228
+
229
+ // Add ROI applied flag
230
+ if (options.roi != null) {
231
+ result["roiApplied"] = true
232
+ }
233
+
234
+ // Add motion detection if enabled
235
+ if (options.enableMotionDetection) {
236
+ val motionResult = calculateMotion(bitmap, options.motionThreshold)
237
+ result["motion"] = motionResult
238
+ }
239
+
240
+ return result
241
+ }
242
+
243
+ private fun analyze(bitmap: Bitmap, options: AnalysisOptions = AnalysisOptions()) {
244
+ val result = analyzeImmediate(bitmap, options)
245
+ cachedResult.set(result)
246
+ }
247
+
248
+ private fun insertTop(list: MutableList<Pair<Int, Int>>, idx: Int, value: Int) {
249
+ var i = 0
250
+ while (i < list.size && value <= list[i].second) i++
251
+ if (i < 3) {
252
+ list.add(i, idx to value)
253
+ if (list.size > 3) list.removeAt(3)
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,40 @@
1
+ package com.cameravisionpixelcolors
2
+
3
+ import com.mrousavy.camera.frameprocessors.Frame
4
+ import com.mrousavy.camera.frameprocessors.FrameProcessorPlugin
5
+ import com.mrousavy.camera.frameprocessors.VisionCameraProxy
6
+
7
+ class PixelColorsFrameProcessor(proxy: VisionCameraProxy, options: Map<String, Any>?) : FrameProcessorPlugin() {
8
+ override fun callback(frame: Frame, arguments: Map<String, Any>?): Any {
9
+ val image = frame.image ?: return emptyMap<String, Any>()
10
+ val bitmap = YuvToBitmapConverter.convert(image)
11
+
12
+ // Parse options from arguments
13
+ val analysisOptions = parseOptions(arguments)
14
+
15
+ PixelAnalyzerEngine.analyzeAsync(bitmap, analysisOptions)
16
+ return PixelAnalyzerEngine.analyzeSync()
17
+ }
18
+
19
+ private fun parseOptions(arguments: Map<String, Any>?): AnalysisOptions {
20
+ val optionsMap = arguments?.get("options") as? Map<*, *> ?: return AnalysisOptions()
21
+
22
+ val enableMotionDetection = optionsMap["enableMotionDetection"] as? Boolean ?: false
23
+ val motionThreshold = (optionsMap["motionThreshold"] as? Number)?.toFloat() ?: 0.1f
24
+
25
+ val roiMap = optionsMap["roi"] as? Map<*, *>
26
+ val roi = if (roiMap != null) {
27
+ val x = (roiMap["x"] as? Number)?.toFloat() ?: 0f
28
+ val y = (roiMap["y"] as? Number)?.toFloat() ?: 0f
29
+ val width = (roiMap["width"] as? Number)?.toFloat() ?: 1f
30
+ val height = (roiMap["height"] as? Number)?.toFloat() ?: 1f
31
+ ROIConfig(x, y, width, height)
32
+ } else null
33
+
34
+ return AnalysisOptions(
35
+ enableMotionDetection = enableMotionDetection,
36
+ motionThreshold = motionThreshold,
37
+ roi = roi
38
+ )
39
+ }
40
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,50 @@
1
+ package com.margelo.nitro.cameravisionpixelcolors
2
+
3
+ import com.cameravisionpixelcolors.PixelAnalyzerEngine
4
+ import com.margelo.nitro.core.Promise
5
+ import java.util.concurrent.Executors
6
+
7
+ class HybridCameraVisionPixelColors: HybridCameraVisionPixelColorsSpec() {
8
+ private val executor = Executors.newSingleThreadExecutor()
9
+
10
+ override fun analyzeImageAsync(image: ImageData): Promise<PixelColorsResult> {
11
+ return Promise.async {
12
+ val width = image.width.toInt()
13
+ val height = image.height.toInt()
14
+ val data = ByteArray(image.data.size.toInt())
15
+ image.data.getBuffer(false).get(data)
16
+
17
+ val result = PixelAnalyzerEngine.analyzeImageData(width, height, data)
18
+
19
+ val uniqueColorCount = result["uniqueColorCount"] as? Int ?: 0
20
+ @Suppress("UNCHECKED_CAST")
21
+ val topColorsMap = result["topColors"] as? List<Map<String, Int>> ?: emptyList()
22
+ @Suppress("UNCHECKED_CAST")
23
+ val brightestColorsMap = result["brightestColors"] as? List<Map<String, Int>> ?: emptyList()
24
+
25
+ val topColors = topColorsMap.map { map ->
26
+ RGBColor(
27
+ r = (map["r"] ?: 0).toDouble(),
28
+ g = (map["g"] ?: 0).toDouble(),
29
+ b = (map["b"] ?: 0).toDouble()
30
+ )
31
+ }.toTypedArray()
32
+
33
+ val brightestColors = brightestColorsMap.map { map ->
34
+ RGBColor(
35
+ r = (map["r"] ?: 0).toDouble(),
36
+ g = (map["g"] ?: 0).toDouble(),
37
+ b = (map["b"] ?: 0).toDouble()
38
+ )
39
+ }.toTypedArray()
40
+
41
+ PixelColorsResult(
42
+ uniqueColorCount = uniqueColorCount.toDouble(),
43
+ topColors = topColors,
44
+ brightestColors = brightestColors,
45
+ motion = null,
46
+ roiApplied = null
47
+ )
48
+ }
49
+ }
50
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/withPixelColors');
package/ios/Bridge.h ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Bridge.h
3
+ // camera-vision-pixel-colors
4
+ //
5
+ // Created by Yevhen Lahodiuk on 1/17/2026
6
+ //
7
+
8
+ #pragma once
@@ -0,0 +1,53 @@
1
+ //
2
+ // HybridCameraVisionPixelColors.swift
3
+ // CameraVisionPixelColors
4
+ //
5
+
6
+ import Foundation
7
+ import NitroModules
8
+
9
+ class HybridCameraVisionPixelColors: HybridCameraVisionPixelColorsSpec {
10
+ func analyzeImageAsync(image: ImageData) throws -> Promise<PixelColorsResult> {
11
+ return Promise { resolve, reject in
12
+ DispatchQueue.global(qos: .userInitiated).async {
13
+ let width = Int(image.width)
14
+ let height = Int(image.height)
15
+ let data = Data(image.data)
16
+
17
+ let result = PixelAnalyzerEngine.shared.analyzeImageData(
18
+ width: width,
19
+ height: height,
20
+ data: data
21
+ )
22
+
23
+ let uniqueColorCount = result["uniqueColorCount"] as? Int ?? 0
24
+ let topColorsDict = result["topColors"] as? [[String: Int]] ?? []
25
+ let brightestColorsDict = result["brightestColors"] as? [[String: Int]] ?? []
26
+
27
+ let topColors = topColorsDict.map { dict in
28
+ RGBColor(
29
+ r: Double(dict["r"] ?? 0),
30
+ g: Double(dict["g"] ?? 0),
31
+ b: Double(dict["b"] ?? 0)
32
+ )
33
+ }
34
+
35
+ let brightestColors = brightestColorsDict.map { dict in
36
+ RGBColor(
37
+ r: Double(dict["r"] ?? 0),
38
+ g: Double(dict["g"] ?? 0),
39
+ b: Double(dict["b"] ?? 0)
40
+ )
41
+ }
42
+
43
+ let pixelColorsResult = PixelColorsResult(
44
+ uniqueColorCount: Double(uniqueColorCount),
45
+ topColors: topColors,
46
+ brightestColors: brightestColors
47
+ )
48
+
49
+ resolve(pixelColorsResult)
50
+ }
51
+ }
52
+ }
53
+ }