package com.pixeldata import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix import android.graphics.Paint import android.graphics.Rect import kotlin.math.min import kotlin.math.max import kotlin.math.roundToInt /** * Processes bitmap images to extract and transform pixel data */ object PixelProcessor { /** * Process an image with the given options */ fun process(bitmap: Bitmap, options: GetPixelDataOptions): PixelDataResult { val startTime = System.nanoTime() var processedBitmap = bitmap // Apply ROI if specified options.roi?.let { roi -> processedBitmap = applyRoi(processedBitmap, roi) } // Apply resize if specified options.resize?.let { resize -> processedBitmap = applyResize(processedBitmap, resize) } // Extract pixel data as RGBA val rgbaPixels = extractRgbaPixels(processedBitmap) // Convert to target color format val colorData = convertColorFormat( rgbaPixels, processedBitmap.width, processedBitmap.height, options.colorFormat ) // Apply normalization val normalizedData = applyNormalization( colorData, options.colorFormat, options.normalization ) // Convert to target data layout val layoutData = convertLayout( normalizedData, processedBitmap.width, processedBitmap.height, options.colorFormat.channels, options.dataLayout ) // Calculate shape based on layout val shape = calculateShape( processedBitmap.width, processedBitmap.height, options.colorFormat.channels, options.dataLayout ) val endTime = System.nanoTime() val processingTimeMs = (endTime - startTime) / 1_000_000.0 return PixelDataResult( data = layoutData, width = processedBitmap.width, height = processedBitmap.height, channels = options.colorFormat.channels, dataLayout = options.dataLayout, shape = shape, processingTimeMs = processingTimeMs ) } /** * Apply region of interest cropping */ private fun applyRoi(bitmap: Bitmap, roi: Roi): Bitmap { // Validate ROI bounds if (roi.x < 0 || roi.y < 0 || roi.width <= 0 || roi.height <= 0) { throw PixelDataException( "INVALID_ROI", "ROI dimensions must be positive and coordinates non-negative" ) } if (roi.x + roi.width > bitmap.width || roi.y + roi.height > bitmap.height) { throw PixelDataException( "INVALID_ROI", "ROI extends beyond image bounds (image: ${bitmap.width}x${bitmap.height}, ROI: x=${roi.x}, y=${roi.y}, w=${roi.width}, h=${roi.height})" ) } return Bitmap.createBitmap(bitmap, roi.x, roi.y, roi.width, roi.height) } /** * Apply resize with the specified strategy */ private fun applyResize(bitmap: Bitmap, resize: ResizeOptions): Bitmap { val targetWidth = resize.width val targetHeight = resize.height return when (resize.strategy) { ResizeStrategy.STRETCH -> { Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true) } ResizeStrategy.COVER -> { resizeCover(bitmap, targetWidth, targetHeight) } ResizeStrategy.CONTAIN -> { resizeContain(bitmap, targetWidth, targetHeight) } } } /** * Resize with cover strategy (fill target, crop excess) */ private fun resizeCover(bitmap: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap { val sourceWidth = bitmap.width.toFloat() val sourceHeight = bitmap.height.toFloat() val scaleX = targetWidth / sourceWidth val scaleY = targetHeight / sourceHeight val scale = max(scaleX, scaleY) val scaledWidth = (sourceWidth * scale).roundToInt() val scaledHeight = (sourceHeight * scale).roundToInt() val scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true) // Center crop val x = (scaledWidth - targetWidth) / 2 val y = (scaledHeight - targetHeight) / 2 return Bitmap.createBitmap(scaledBitmap, x, y, targetWidth, targetHeight) } /** * Resize with contain strategy (fit within target, letterbox) */ private fun resizeContain(bitmap: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap { val sourceWidth = bitmap.width.toFloat() val sourceHeight = bitmap.height.toFloat() val scaleX = targetWidth / sourceWidth val scaleY = targetHeight / sourceHeight val scale = min(scaleX, scaleY) val scaledWidth = (sourceWidth * scale).roundToInt() val scaledHeight = (sourceHeight * scale).roundToInt() val scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true) // Create target bitmap with letterboxing (black background) val result = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) val canvas = Canvas(result) canvas.drawColor(Color.BLACK) // Center the scaled bitmap val x = (targetWidth - scaledWidth) / 2 val y = (targetHeight - scaledHeight) / 2 canvas.drawBitmap(scaledBitmap, x.toFloat(), y.toFloat(), null) return result } /** * Extract RGBA pixel values from bitmap */ private fun extractRgbaPixels(bitmap: Bitmap): IntArray { val width = bitmap.width val height = bitmap.height val pixels = IntArray(width * height) bitmap.getPixels(pixels, 0, width, 0, 0, width, height) return pixels } /** * Convert ARGB pixels to target color format */ private fun convertColorFormat( argbPixels: IntArray, width: Int, height: Int, format: ColorFormat ): FloatArray { val channels = format.channels val result = FloatArray(width * height * channels) for (i in argbPixels.indices) { val argb = argbPixels[i] val a = (argb shr 24) and 0xFF val r = (argb shr 16) and 0xFF val g = (argb shr 8) and 0xFF val b = argb and 0xFF val baseIdx = i * channels when (format) { ColorFormat.RGB -> { result[baseIdx] = r.toFloat() result[baseIdx + 1] = g.toFloat() result[baseIdx + 2] = b.toFloat() } ColorFormat.RGBA -> { result[baseIdx] = r.toFloat() result[baseIdx + 1] = g.toFloat() result[baseIdx + 2] = b.toFloat() result[baseIdx + 3] = a.toFloat() } ColorFormat.BGR -> { result[baseIdx] = b.toFloat() result[baseIdx + 1] = g.toFloat() result[baseIdx + 2] = r.toFloat() } ColorFormat.BGRA -> { result[baseIdx] = b.toFloat() result[baseIdx + 1] = g.toFloat() result[baseIdx + 2] = r.toFloat() result[baseIdx + 3] = a.toFloat() } ColorFormat.GRAYSCALE -> { // ITU-R BT.601 formula for luminance result[baseIdx] = 0.299f * r + 0.587f * g + 0.114f * b } } } return result } /** * Apply normalization to pixel data */ private fun applyNormalization( data: FloatArray, format: ColorFormat, normalization: Normalization ): FloatArray { val channels = format.channels // Get mean and std based on preset val (mean, std) = when (normalization.preset) { NormalizationPreset.IMAGENET -> { Pair( floatArrayOf(0.485f * 255f, 0.456f * 255f, 0.406f * 255f), floatArrayOf(0.229f * 255f, 0.224f * 255f, 0.225f * 255f) ) } NormalizationPreset.TENSORFLOW -> { Pair( floatArrayOf(127.5f, 127.5f, 127.5f), floatArrayOf(127.5f, 127.5f, 127.5f) ) } NormalizationPreset.SCALE -> { Pair( floatArrayOf(0f, 0f, 0f), floatArrayOf(255f, 255f, 255f) ) } NormalizationPreset.RAW -> { return data // No normalization } NormalizationPreset.CUSTOM -> { Pair(normalization.mean, normalization.std) } } val result = FloatArray(data.size) val numPixels = data.size / channels for (i in 0 until numPixels) { for (c in 0 until channels) { val idx = i * channels + c val channelIdx = min(c, mean.size - 1) result[idx] = (data[idx] - mean[channelIdx]) / std[channelIdx] } } return result } /** * Convert data layout from HWC to target format */ private fun convertLayout( data: FloatArray, width: Int, height: Int, channels: Int, layout: DataLayout ): FloatArray { return when (layout) { DataLayout.HWC -> data // Already in HWC format DataLayout.CHW -> convertHwcToChw(data, width, height, channels) DataLayout.NHWC -> data // Same as HWC for single image (batch size 1) DataLayout.NCHW -> convertHwcToChw(data, width, height, channels) } } /** * Convert from HWC (Height x Width x Channels) to CHW (Channels x Height x Width) */ private fun convertHwcToChw( hwcData: FloatArray, width: Int, height: Int, channels: Int ): FloatArray { val chwData = FloatArray(hwcData.size) for (h in 0 until height) { for (w in 0 until width) { for (c in 0 until channels) { val hwcIdx = (h * width + w) * channels + c val chwIdx = c * height * width + h * width + w chwData[chwIdx] = hwcData[hwcIdx] } } } return chwData } /** * Calculate shape array based on data layout */ private fun calculateShape( width: Int, height: Int, channels: Int, layout: DataLayout ): IntArray { return when (layout) { DataLayout.HWC -> intArrayOf(height, width, channels) DataLayout.CHW -> intArrayOf(channels, height, width) DataLayout.NHWC -> intArrayOf(1, height, width, channels) DataLayout.NCHW -> intArrayOf(1, channels, height, width) } } }