Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ fun ContentArea(item: NavItem) {
title = "Preparing Input",
what = "Machine learning models require data in a specific format (ByteBuffers) rather than raw images. Preparing input involves resizing, color space conversion, and optional normalization.",
whenToUse = "Before calling `Kflite.run()`, you must transform your raw data (like a Bitmap or ByteArray) into the exact tensor shape and type expected by the model.",
howToUse = "Use the `preprocessing` module to convert images to `ByteBuffer`. Ensure dimensions match your model's input tensor:\n\n```kotlin\nval inputImage = bitmap.toScaledByteBuffer(\n inputWidth = 640,\n inputHeight = 640,\n inputAllocateSize = 640 * 640 * 3 * 4, // 3 channels, 4 bytes (Float32)\n normalize = true // Scale pixels to [0, 1]\n)\n\n// Run inference with the prepared buffer\nKflite.run(inputs = listOf(inputImage), ...)\n```"
howToUse = "Use the `preprocessing` module to convert images to `ByteBuffer`. Ensure dimensions match your model's input tensor:\n\n```kotlin\nval inputImage = bitmap.imageToScaledByteBuffer(\n inputWidth = 640,\n inputHeight = 640,\n inputAllocateSize = 640 * 640 * 3 * 4, // 3 channels, 4 bytes (Float32)\n normalize = true // Scale pixels to [0, 1]\n)\n\n// Run inference with the prepared buffer\nKflite.run(inputs = listOf(inputImage), ...)\n```"
)
is NavItem.TFLite -> DetailPage(
title = "TFLite Runtime",
Expand Down Expand Up @@ -216,9 +216,9 @@ fun ContentArea(item: NavItem) {
)
is NavItem.PreProcessing.Image -> DetailPage(
title = "Preprocessing: Image",
what = "Image preprocessing involves resizing, cropping, and converting image pixels into a normalized byte buffer format that a machine learning model can ingest as input.",
whenToUse = "Use this before every image-based inference. It ensures the input data matches the model's expected dimensions, color space, and data type (Float32 or Uint8).",
howToUse = "Use the `toScaledByteBuffer` extension on an `ImageBitmap` or `ByteArray`:\n\n```kotlin\nval inputBuffer = bitmap.toScaledByteBuffer(\n inputWidth = 640,\n inputHeight = 640,\n inputAllocateSize = 640 * 640 * 3 * 4, // 4 bytes for Float32\n normalize = true // Scales pixels to [0, 1]\n)\n```"
what = "Image preprocessing involves resizing, cropping, rotating, and converting image pixels into a normalized byte buffer format that a machine learning model can ingest as input.",
whenToUse = "Use this before every image-based inference. It ensures the input data matches the model's expected dimensions, color space, and orientation.",
howToUse = "You can use the simple shorthand or the flexible `preprocess` DSL for complex pipelines:\n\n```kotlin\n// Shorthand for simple resizing\nval buffer = bitmap.imageToScaledByteBuffer(\n inputWidth = 640,\n inputHeight = 640,\n inputAllocateSize = 640 * 640 * 3 * 4,\n normalize = true\n)\n\n// DSL for complex pipelines\nval dslBuffer = bitmap.preprocess(allocateSize = 640 * 640 * 3 * 4) {\n rotate(90f)\n crop(x = 0, y = 0, width = 300, height = 300)\n resize(width = 640, height = 640)\n normalize = true\n}\n```"
)
}
Spacer(modifier = Modifier.height(64.dp))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.kmp.playground.kflite.preprocessing.image

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.Matrix
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.graphics.get
import androidx.core.graphics.scale
import java.nio.ByteBuffer
import java.nio.ByteOrder

actual fun ImageBitmap.preprocess(
allocateSize: Int,
block: ImageProcessorConfig.() -> Unit
): TensorBuffer {
val config = ImageProcessorConfig().apply(block)
val processedBitmap = this.asAndroidBitmap().applyConfig(config)
return processedBitmap.toTensorBuffer(allocateSize, config.normalize)
}

actual fun ByteArray.preprocess(
allocateSize: Int,
block: ImageProcessorConfig.() -> Unit
): TensorBuffer {
val config = ImageProcessorConfig().apply(block)
val originalBitmap = BitmapFactory.decodeByteArray(this, 0, this.size)
?: throw IllegalArgumentException("Could not decode ByteArray to Bitmap")
val processedBitmap = originalBitmap.applyConfig(config)
return processedBitmap.toTensorBuffer(allocateSize, config.normalize)
}

private fun Bitmap.applyConfig(config: ImageProcessorConfig): Bitmap {
var result = this

// 1. Rotate
config.rotationDegrees?.let { degrees ->
val matrix = Matrix().apply { postRotate(degrees) }
result = Bitmap.createBitmap(result, 0, 0, result.width, result.height, matrix, true)
}

// 2. Crop
if (config.cropX != null && config.cropY != null && config.cropWidth != null && config.cropHeight != null) {
result = Bitmap.createBitmap(
result,
config.cropX!!,
config.cropY!!,
config.cropWidth!!,
config.cropHeight!!
)
}

// 3. Resize
if (config.resizeWidth != null && config.resizeHeight != null) {
result = result.scale(config.resizeWidth!!, config.resizeHeight!!)
}

return result
}

private fun Bitmap.toTensorBuffer(allocateSize: Int, normalize: Boolean): TensorBuffer {
val width = this.width
val height = this.height
val byteBuffer = ByteBuffer.allocateDirect(allocateSize)
byteBuffer.order(ByteOrder.nativeOrder())

for (y in 0 until height) {
for (x in 0 until width) {
val pixel = this[x, y]

val r = Color.red(pixel)
val g = Color.green(pixel)
val b = Color.blue(pixel)

if (normalize) {
byteBuffer.putFloat(r / 255.0f)
byteBuffer.putFloat(g / 255.0f)
byteBuffer.putFloat(b / 255.0f)
} else {
byteBuffer.put(r.toByte())
byteBuffer.put(g.toByte())
byteBuffer.put(b.toByte())
}
}
}

return byteBuffer
}

actual fun ImageBitmap.imageToScaledByteBuffer(
inputWidth: Int,
inputHeight: Int,
inputAllocateSize: Int,
normalize: Boolean
): TensorBuffer {
return preprocess(inputAllocateSize) {
resize(inputWidth, inputHeight)
this.normalize = normalize
}
}

actual fun ByteArray.bytesToScaledByteBuffer(
inputWidth: Int,
inputHeight: Int,
inputAllocateSize: Int,
normalize: Boolean
): TensorBuffer {
return preprocess(inputAllocateSize) {
resize(inputWidth, inputHeight)
this.normalize = normalize
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@ import androidx.compose.ui.graphics.ImageBitmap

typealias TensorBuffer = Any

class ImageProcessorConfig {
var resizeWidth: Int? = null
var resizeHeight: Int? = null
var cropX: Int? = null
var cropY: Int? = null
var cropWidth: Int? = null
var cropHeight: Int? = null
var rotationDegrees: Float? = null
var normalize: Boolean = false

fun resize(width: Int, height: Int) {
resizeWidth = width
resizeHeight = height
}

fun crop(x: Int, y: Int, width: Int, height: Int) {
cropX = x
cropY = y
cropWidth = width
cropHeight = height
}

fun rotate(degrees: Float) {
rotationDegrees = degrees
}
}

expect fun ImageBitmap.preprocess(
allocateSize: Int,
block: ImageProcessorConfig.() -> Unit
): TensorBuffer

expect fun ByteArray.preprocess(
allocateSize: Int,
block: ImageProcessorConfig.() -> Unit
): TensorBuffer

expect fun ImageBitmap.imageToScaledByteBuffer(
inputWidth: Int,
inputHeight: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,11 @@ package org.kmp.playground.kflite.preprocessing.image
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asSkiaBitmap
import kotlinx.cinterop.*
import platform.CoreGraphics.*
import platform.CoreGraphics.CGColorRenderingIntent.kCGRenderingIntentDefault
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGDataProviderCreateWithData
import platform.CoreGraphics.CGImageAlphaInfo
import platform.CoreGraphics.CGImageCreate
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGSizeMake
import platform.CoreGraphics.kCGBitmapByteOrder32Big
import platform.CoreGraphics.CGContextDrawImage
import platform.CoreGraphics.CGBitmapContextCreate
import platform.Foundation.*
import platform.UIKit.UIGraphicsBeginImageContextWithOptions
import platform.UIKit.UIGraphicsEndImageContext
import platform.UIKit.UIGraphicsGetImageFromCurrentImageContext
import platform.UIKit.UIImage
import platform.UIKit.*
import kotlin.math.PI
import kotlin.math.roundToInt

@OptIn(ExperimentalForeignApi::class)
Expand All @@ -39,6 +29,49 @@ fun UIImage.scaleTo(width: Int, height: Int): UIImage {
return scaledImage ?: error("Failed to scale image")
}

@OptIn(ExperimentalForeignApi::class)
fun UIImage.rotate(degrees: Float): UIImage {
val radians = degrees * PI / 180.0
val size = this.size.useContents { CGSizeMake(width, height) }
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
val context = UIGraphicsGetCurrentContext()

size.useContents {
CGContextTranslateCTM(context, width / 2.0, height / 2.0)
CGContextRotateCTM(context, radians)
this@rotate.drawInRect(CGRectMake(-width / 2.0, -height / 2.0, width, height))
}

val rotatedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return rotatedImage ?: this
}

@OptIn(ExperimentalForeignApi::class)
fun UIImage.crop(x: Int, y: Int, width: Int, height: Int): UIImage {
val rect = CGRectMake(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble())
val cgImage = this.CGImage()?.let {
CGImageCreateWithImageInRect(it, rect)
}
return cgImage?.let { UIImage.imageWithCGImage(it) } ?: this
}

internal fun UIImage.applyConfig(config: ImageProcessorConfig): UIImage {
var result = this
// 1. Rotate
config.rotationDegrees?.let {
result = result.rotate(it)
}
// 2. Crop
if (config.cropX != null && config.cropY != null && config.cropWidth != null && config.cropHeight != null) {
result = result.crop(config.cropX!!, config.cropY!!, config.cropWidth!!, config.cropHeight!!)
}
// 3. Resize
if (config.resizeWidth != null && config.resizeHeight != null) {
result = result.scaleTo(config.resizeWidth!!, config.resizeHeight!!)
}
return result
}

@OptIn(ExperimentalForeignApi::class)
internal fun ImageBitmap.toUIImage(): UIImage? {
Expand Down
Loading