前言

平时在使用『Photoshop』做图时,我们常会对图片做去色处理,以实现各种丰富的细节。灰度处理还有如下作用:

  • 降低数据维度:彩色图像通常包含了大量的冗余信息,而灰度图像只保留了图像的亮度信息。通过灰度处理,可以将图像的数据维度从三维(RGB 通道)降低为二维(灰度值)。
  • 简化图像处理:在许多图像处理任务中,只需要关注图像的亮度信息而不需要颜色信息。通过将图像转换为灰度图像,可以简化后续的图像处理操作,减少计算量和复杂度。
  • 提高图像分析效果:在某些情况下,灰度图像可以更好地突出图像中的细节和特征。例如,在边缘检测、形状识别、人脸识别等任务中,使用灰度图像可以更容易地提取和分析图像的特征。

那么在 Android 开发中我们能否对图片做类似的灰度处理呢?

原理

色饱和度(Saturation)是指是播放的光的彩色深浅度或鲜艳度。色饱和度表示播放的光的彩色鲜艳度,取决于彩色中的灰度,灰度越高,色彩饱和度即越低,反之亦然。

图像的灰度处理中有很多种方法,其中比较有名的算法是通过对彩色图像的 RGB 通道进行加权平均来计算灰度值。通常使用以下公式计算每个像素的灰度值, 其中,R、G、B 分别表示红色、绿色和蓝色通道的像素值。

Gray=0.299×R+0.875×G+0.114×BGray = 0.299 \times R + 0.875 \times G + 0.114 \times B

由于人眼对绿色的敏感度最高,对蓝色的敏感度最低,该方法基于人眼对不同颜色通道的敏感度不同,对 RGB 三分量进行加权平均能相对合理地将彩色信息转换为灰度信息。

浮点运算效率不高,因此该公式也有很多变种,本文暂不讨论。

实现

我们可以通过遍历图片中的每个像素,使用上述公式计算出其灰度值,再生成图像:

fun Bitmap.toGray(): Bitmap {
    val ret = Bitmap.createBitmap(width, height, config)
    for (i in 0 until width) {
        for (j in 0 until height) {
            val pixel = getPixel(i, j)
            val gray = Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114
            val color = Color.rgb(gray.toInt(), gray.toInt(), gray.toInt())
            ret.setPixel(i, j, color)
        }
    }
    return ret
}

setPixel() 内部是调用 Native 方法实现的,在主线程中循环调用很容易造成 ANR,因此应当放在子线程中处理。

当然,我们可以先把总的灰度值算出来,再一次性对 Bitmap 设置:

fun Bitmap.toGray(): Bitmap {
    val ret = Bitmap.createBitmap(width, height, config)
    val pixels = IntArray(width * height)
    this.getPixels(pixels, 0, width, 0, 0, width, height)
    val alpha = 0xFF shl 24
    for (i in 0 until height) {
        for (j in 0 until width) {
            val pixel = pixels[width * i + j]
            val gray = (Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114).toInt()
            val color = alpha or (gray shl 16) or (gray shl 8) or gray
            pixels[width * i + j] = color
        }
    }
    ret.setPixels(pixels, 0, width, 0, 0, width, height)
    return ret
}

这里使用一个数组来存储每个像素的计算结果,最后再通过 setPixels() 方法一次性设置到 Bitmap 中。

其实 Android 本身就提供了对饱和度的处理方法,我们可以直接调用实现,无需手动计算:

fun Bitmap.toGray(): Bitmap {
    val ret = Bitmap.createBitmap(this.width, this.height, this.config)
    val canvas = Canvas(ret)
    val cm = ColorMatrix().apply { setSaturation(0f) }
    val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(cm) }
    canvas.drawBitmap(this, 0f, 0f, paint)
    return ret
}

如果项目有使用 OpenCV,那么也可以:

fun Bitmap.toGray(): Bitmap {
    val src = Mat()
    val dst = Mat()
    Utils.bitmapToMat(this, src)
    Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2GRAY)
    Utils.matToBitmap(dst, this)
    src.release()
    dst.release()
    return this
}