You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android设备上MNIST手写数字照片识别的图像预处理问题咨询

Android端MNIST手写数字识别:拍摄照片预处理与推理实现

嘿,刚好我之前在Android上做过类似的功能——用预训练MNIST模型识别照片里的单手写数字,给你梳理下具体的实现步骤,重点对应你提到的预处理环节:

一、核心预处理流程

这部分是保证模型识别准确率的关键,毕竟拍摄的照片和MNIST训练集的格式差异很大:

  • 原始图像获取:用Android的CameraX或者系统相机API拿到拍摄后的Bitmap对象就行。如果是竖屏拍摄,记得先做旋转校正(不过你说应用内图像尺寸一致,这步可以根据你的实际场景调整)。
  • 转为黑白二值图像:MNIST训练集是白底黑字的单通道灰度图,所以必须把彩色照片转成符合要求的黑白格式。这里分两步:先转灰度,再做二值化(设定阈值区分背景和数字),代码示例(Kotlin):
// 第一步:将彩色图转为灰度图
val grayBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(grayBitmap)
val paint = Paint().apply {
    colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
}
canvas.drawBitmap(originalBitmap, 0f, 0f, paint)

// 第二步:二值化处理,区分背景和数字
val threshold = 127 // 可根据拍摄光线调整,比如光线暗的话降低阈值
val pixelArray = IntArray(grayBitmap.width * grayBitmap.height)
grayBitmap.getPixels(pixelArray, 0, grayBitmap.width, 0, 0, grayBitmap.width, grayBitmap.height)

for (i in pixelArray.indices) {
    // 提取灰度值
    val grayValue = pixelArray[i] and 0xff
    // 低于阈值设为黑色(数字),高于设为白色(背景)
    pixelArray[i] = if (grayValue < threshold) 0xff000000.toInt() else 0xffffffff.toInt()
}
grayBitmap.setPixels(pixelArray, 0, grayBitmap.width, 0, 0, grayBitmap.width, grayBitmap.height)

二、适配模型输入尺寸

MNIST模型的标准输入是28x28的单通道灰度图,所以需要把处理后的黑白图调整到这个尺寸,同时要避免数字拉伸变形:

// 先裁剪出数字的核心区域(避免多余背景影响缩放)
var leftBound = grayBitmap.width
var rightBound = 0
var topBound = grayBitmap.height
var bottomBound = 0

// 遍历像素找到数字的边界
for (y in 0 until grayBitmap.height) {
    for (x in 0 until grayBitmap.width) {
        if (grayBitmap.getPixel(x, y) == 0xff000000.toInt()) { // 黑色像素是数字
            leftBound = min(leftBound, x)
            rightBound = max(rightBound, x)
            topBound = min(topBound, y)
            bottomBound = max(bottomBound, y)
        }
    }
}

// 给数字留一点边距,避免裁剪太紧凑
val margin = 10
val cropLeft = max(0, leftBound - margin)
val cropTop = max(0, topBound - margin)
val cropWidth = min(grayBitmap.width - cropLeft, rightBound - leftBound + 2 * margin)
val cropHeight = min(grayBitmap.height - cropTop, bottomBound - topBound + 2 * margin)

// 裁剪出数字区域
val croppedBitmap = Bitmap.createBitmap(grayBitmap, cropLeft, cropTop, cropWidth, cropHeight)

// 缩放到模型要求的28x28尺寸
val modelInputBitmap = Bitmap.createScaledBitmap(croppedBitmap, 28, 28, true)

三、模型推理(以TensorFlow Lite为例)

把处理好的图像转成模型需要的张量格式,然后运行推理得到结果:

// 初始化预训练的TFLite模型(这里假设你已经把模型文件放到assets目录)
val model = MnistModel.newInstance(context)

// 创建输入张量:MNIST模型输入是[1,28,28,1]的float数组,值归一化到0-1
val inputBuffer = TensorBuffer.createFixedSize(intArrayOf(1, 28, 28, 1), DataType.FLOAT32)
val inputPixels = FloatArray(28 * 28)

for (i in 0 until 28 * 28) {
    val x = i % 28
    val y = i / 28
    val pixelValue = modelInputBitmap.getPixel(x, y) and 0xff
    // 注意:MNIST训练集是黑字白底,所以要把像素值反转(黑=255转为1,白=0转为0)
    inputPixels[i] = (255 - pixelValue) / 255.0f
}
inputBuffer.loadArray(inputPixels)

// 运行推理
val outputs = model.process(inputBuffer)
val resultArray = outputs.outputFeature0AsTensorBuffer.floatArray

// 获取预测概率最高的数字
val predictedDigit = resultArray.indices.maxByOrNull { resultArray[it] } ?: -1

一些实用小提示

  • 拍摄时尽量让数字居中,光线均匀,避免阴影或者反光;
  • 二值化的阈值可以做成动态的,比如计算图像的平均灰度值作为阈值,适配不同光线环境;
  • 如果你的模型是其他框架(比如ONNX Runtime),核心逻辑一样,只是张量转换的代码略有不同。

内容的提问来源于stack exchange,提问作者Lingviston

火山引擎 最新活动