图像由排列在二维网格中的若干像素组成;行和列。图像的分辨率是行数和列数的乘积,行表示图像的高度,列表示图像的宽度。一个1080x720的图像总共有777600个像素,每个像素都由一些称为通道的颜色组成。数字图像通常由3个通道组成;红,绿,蓝。灰度图像只有一个通道;黑色或白色。每个像素由8位(~1字节)表示,其值范围为0-255。每当模糊等效果应用于图像时,实际上,这是一组直接应用于像素的数学运算,以获得所需的结果。
有各种各样的技术用来模糊图像,其中一些是:
1、均值滤波器
2、高斯模糊
3、双边滤波器
应用滤波器的一种常用方法是核卷积,即一组数字(内核)通过图像的像素传递。内核是用于模糊和其他图像效果的小矩阵。核的大小必须是奇数,并且核的值越大,模糊效果就越大。内核应用于图像中的每个像素,当前选定的像素位于内核的中心。内核中的每个值都乘以由内核突出显示的矩阵范围内的各个像素值,并相加在一起。中心像素值最终被这些操作的结果所取代。
模糊图像的原生方法
为了在android系统中实现均值滤波,我们可以通过将图像转换成位图来获得像素,并对每个像素应用内核。
以下是我对此的实现:
private suspend fun meanFilter(bitmap: Bitmap) {
val kernel = floatArrayOf(1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f, 1 / 9f)
return withContext(Dispatchers.IO) {
val diameter = 3 // 3x3 matrix
val cols = bitmap.width
val rows = bitmap.height
val step = (diameter - 1) / 2
val imagePixels = IntArray(rows * cols)
val newImagePixels = IntArray(rows * cols)
bitmap.getPixels(imagePixels, 0, cols, 0, 0, cols, rows) // here we are assigning all pixel values to the IntArray so we can process the individual pixels via the kernel
for (i in 0 until cols) {
for (j in 0 until rows) {
val startCoord = Point(j - step, i - step)
val endCoord = Point(j + step, i + step)
val matrix = ArrayList<Int>()
for (_col in startCoord.y..endCoord.y) {
for (_row in startCoord.x..endCoord.x) {
if (_row < 0 || _row >= rows || _col < 0 || _col >= cols) {
//ignore pixels out of matrix bounds
matrix.add(0)
} else {
val pixel = imagePixels[_row * cols + _col]
matrix.add(pixel)
}
}
}
val sum = matrix.mapIndexed { index, value ->
val multiplier = kernel[index]
val alpha = value shr 24 and 0xFF
val red = ((value ushr 16 and 0xFF) * multiplier).toInt()
val green = ((value ushr 8 and 0xFF) * multiplier).toInt()
val blue = ((value and 0xFF) * multiplier).toInt()
((alpha) shl 24) or ((red) shl 16) or ((green) shl 8) or (blue)
}.sum()
newImagePixels[j * cols + i] = sum
}
}
Bitmap.createBitmap(newImagePixels, cols, rows, Bitmap.Config.ARGB_8888)
}
}
你会注意到,在应用内核之前,我们对像素值做了一些按位移位的操作,这是因为每个像素都是由某些8位颜色值组成的,这些值在被处理之前需要被隔离,然后再次组合以形成像素值。
使用OpenCV模糊
现在我们已经了解了模糊过滤器是如何工作的,OpenCV是一个有用的图像处理库,它可以更快地完成许多这些特殊类型的操作。在这里,与我们最初写的相比,只需几行代码就可以更精确地模糊图像。你应该有一个新的或现有的android项目的OpenCV设置,开始在你的应用程序中实现它们。
用OpenCV尝试同样的模糊过滤器;
private suspend fun blurImage(bitmap: Bitmap):Bitmap {
return withContext(Dispatchers.IO) {
val imageSrc = Mat()
Utils.bitmapToMat(bitmap, imageSrc)
val size = 24.0
val destination = Mat()
Imgproc.blur(imageSrc, destination, Size(size, size))
val copy = Bitmap.createBitmap(
destination.width(),
destination.height(),
Bitmap.Config.ARGB_8888
)
Utils.matToBitmap(destination, copy)
copy
}
}
如果我们只能模糊图像的一个特定部分,而不是整个图像呢。我们将使用submat()函数选择我们感兴趣的区域,只模糊我们需要的部分。例如,我们将尝试从距离原点30%的位置在图像的顶部和左侧模糊图像,其宽度/高度为图像大小的30%。
private suspend fun blurImage(bitmap: Bitmap):Bitmap {
return withContext(Dispatchers.IO) {
val imageSrc = Mat()
Utils.bitmapToMat(bitmap, imageSrc)
val size = 24.0
val rows = imageSrc.rows()
val cols = imageSrc.cols()
val rowStart = (0.3 * rows).toInt()
val rowEnd = (0.6 * rows).toInt()
val colStart = (0.3 * cols).toInt()
val colEnd = (0.6 * cols).toInt()
val subRegion = imageSrc.submat(rowStart, rowEnd, colStart, colEnd)
Imgproc.blur(subRegion, subRegion, Size(size, size))
val copy = Bitmap.createBitmap(
imageSrc.width(),
imageSrc.height(),
Bitmap.Config.ARGB_8888
)
Utils.matToBitmap(imageSrc, copy)
copy
}
}
这与以前的实现有点不同,解释也很简单。这个submat函数接受许多参数,这些参数将我们需要的矩形点定义为感兴趣的区域。即使此函数只尝试从原始图像中选择一个片段并返回它,它仍然保留对原始图像的直接引用,因此我们对该子区域所做的任何操作都会反映在所选区域上,也会无意中影响原始图像。
现在,我们对图像所需的模糊操作有一点控制,但没有我们想要的那么多控制。在上一个例子中,我们只能模糊掉矩形中的某些部分。如果我们需要使模糊蒙版的基础上各种形式,如圆,矩形,甚至手绘形状。如何使用OpenCV实现这一点?
OpenCV中的高级模糊处理
在这个例子中,我们将尝试在尝试其他方法之前使用更动态的方法重新实现矩形模糊。OpenCV包含许多方法,这些方法允许创建各种形状的遮罩,包括矩形、三角形和其他多边形。
对于矩形,我们将创建一个黑白的矩形遮罩,并对该遮罩执行按位运算,这样我们的模糊操作只能在这些遮罩中进行。要在OpenCV中创建矩形遮罩,我们将使用Imgproc.rectangle()功能:
private suspend fun blurImage(bitmap: Bitmap):Bitmap {
return withContext(Dispatchers.IO) {
val imageSrc = Mat()
Utils.bitmapToMat(bitmap, imageSrc)
val rows = imageSrc.rows()
val cols = imageSrc.cols()
val startY = (0.3*rows).toInt()
val endY = (0.7*rows).toInt()
val startX = (0.3*cols).toInt()
val endX = (0.7*cols).toInt()
val innerMask = Mat.zeros(imageSrc.size(), imageSrc.type())
innerMask.setTo(Scalar.all(0.0))
//thickness set to -1 for inner fill
Imgproc.rectangle(
innerMask,
Rect(startX,startY,endX,endY),
Scalar.all(255.0),
-1
)
val outerMask = Mat()
Core.bitwise_not(innerMask, outerMask)
val blurredImage = Mat()
Imgproc.blur(imageSrc, blurredImage, Size(64.0, 64.0))
//original image minus cutout
val originalCut = Mat()
Core.bitwise_and(imageSrc, outerMask, originalCut)
//blurred image minus cutout
val blurredCut = Mat()
Core.bitwise_and(blurredImage, innerMask, blurredCut)
val merge = Mat()
Core.bitwise_or(originalCut, blurredCut, merge)
val copy = Bitmap.createBitmap(merge.width(), merge.height(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(merge, copy)
copy
}
}
val innerMask = Mat.zeros(imageSrc.size(), imageSrc.type())
innerMask.setTo(Scalar.all(0.0))
这里我们创建一个空图像,像素值设置为0,这意味着这是一个黑色图像
Imgproc.rectangle(innerMask, Rect(startX,startY,endX,endY), Scalar.all(255.0), -1)
上面的代码意味着我们正在创建的黑色图像中绘制一个矩形。起点和终点在rect对象中提供。这个区域充满了一个白色的蒙版Scalar.all(255.0)厚度为-1。根据文档,负厚度值可以填充形状,而不是绘制形状的笔划。
这里有一个黑色的蒙版。我们需要这个蒙版来执行一些位操作,并将我们的模糊部分嵌入到这个白色蒙版中,而原始图像的其余部分嵌入到黑色区域中。
val outerMask = Mat()
Core.bitwise_not(innerMask, outerMask)
从黑白图像中,我们试图反转这些片段,使黑色区域变为白色,白色区域变为黑色。它遵循使用非门的二进制运算符的基本原理。因此,当我们有一个像1110011的值,并且这里应用了一个not gate,那么1变成0,反之亦然。幸运的是,OpenCV有一个允许执行这些操作形式的函数。
对于外部遮罩,我们将尝试使用另一个按位运算符将原始图像像素值复制到此变量,and。这也遵循了将两个二进制数组合在一起时的按位运算的原理,即只有当所有涉及的值都非零时,才有一个真值,否则我们将得到一个假值。这样,原始图像的彩色部分保持不变,而黑色区域根据需要保持为零。
val originalCut = Mat()
Core.bitwise_and(imageSrc, outerMask, originalCut)
为了得到一个模糊不清的图像:
val blurredCut = Mat()
Core.bitwise_and(blurredImage, innerMask, blurredCut)
最后,我们将原始切割和模糊切割相结合,得到代表两幅图像融合的单一图像。黑色像素将与模糊像素相加以产生模糊像素,从而替换两个图像之间的所有黑色像素。只要参与算术运算的一个值是非零,我们就有一个真实值。下面是它的实现方式。
val merge = Mat()
Core.bitwise_or(originalCut, blurredCut, merge)
现在merge代表了我们真正的模糊区域结果,我们正在尝试实现,就像我们使用submat一样。以下是您应该得到的结果:
如果我们想用同样的方法实现圆模糊,我们所要做的就是替换Imgproc.rectangle()具有Imgproc.circle()并传入绘制圆所需的参数。例如:
val circleMask = Mat()
Imgproc.circle(circleMask, org.opencv.core.Point(rowEnd/2.0, rowEnd/2.0), rowEnd/2, Scalar(1.0))
看看前面的图像,你会注意到在模糊的图像边界和原始图像的其余部分之间有一个锐利的边缘。如果我们想避免这种情况,使模糊区域看起来与原始图像混合,应该怎么做呢?如下图所示:
我们要做的事情叫做alpha混合,这是一个在背景图像上覆盖透明前景图像的过程。在图像的每个像素处,我们需要使用alpha遮罩将前景图像与背景图像结合起来。前景图像为模糊图像,背景图像为原始图像,alpha掩模由模糊黑白掩模表示。
以下是我们在kotlin的实现:
private fun alphaBlend(imageSrc:Mat, blurredImage: Mat, blurredMask: Mat): Mat{
val destination = Mat(imageSrc.size(), imageSrc.type())
for (col in 0 until destination.cols()){
for (row in 0 until destination.rows()){
val pixel = imageSrc.get(row, col)
val blurredPixel = blurredImage.get(row, col)
val blurVal = blurredMask.get(row, col)[0] / 255
val newPixelValue = pixel.mapIndexed { index, value ->
(blurVal * blurredPixel[index]) + ((1.0f - blurVal) * value)
}.toDoubleArray()
destination.put(row, col, *newPixelValue)
}
}
return destination
}
我们从模糊掩模中得到每一个像素,除以255,得到一个介于0和1之间的值,然后用每个模糊的像素乘以这个值,用1.0减去相同的值,再乘以原始图像。我们最终获得新的像素值并将其分配给新的图像变量。原始和模糊图像的透明度将为0,从而产生相同的像素值,但是该值随着接近模糊图像的边缘而变化,从而导致沿着模糊遮罩边缘的透明模糊效果。
我们现在更新blurImage()函数匹配此混合函数:
private suspend fun blurImage(bitmap: Bitmap):Bitmap {
return withContext(Dispatchers.IO) {
val imageSrc = Mat()
Utils.bitmapToMat(bitmap, imageSrc)
val innerMask = Mat.zeros(imageSrc.size(), imageSrc.type())
//thickness set to -1 for inner fill
Imgproc.circle(
innerMask,
Point(imageSrc.width() / 2.0, imageSrc.height() / 1.5),
600,
Scalar.all(255.0),
-1
)
val blurredImage = Mat(imageSrc.size(), imageSrc.type())
Imgproc.blur(imageSrc, blurredImage, Size(64.0, 64.0))
val blurredMask = Mat(innerMask.size(), innerMask.type())
Imgproc.blur(innerMask, blurredMask, Size(64.0, 64.0))
val merge = alphaBlend(imageSrc, blurredImage, blurredMask)
val copy = Bitmap.createBitmap(merge.width(), merge.height(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(merge, copy)
copy
}
}
一个新的问题出现了!!!
有一点你会注意到,我们的模糊过程变得非常缓慢,需要超过15秒才能达到模糊效果。这是因为两个原因;
我们循环遍历二维网格中的每个像素,这意味着对于1080x1080的图像,我们实际上在图像中迭代1166400次,这是相当多的!
我们使用get和设置方法,这是相当复杂的操作。
摆脱混乱的方法
在对优化这一过程的各种方法进行了大量研究之后,我终于找到了一个解决问题的方法邮递它提供了一些关于OpenCV的见解。
从这篇文章中,我认为优化整个过程的最佳方法是将像素转换为像IntArray/FloatArray这样的原语,然后对这个对象执行操作,将处理过的像素重新分配回image对象。get/set操作将比way更快,而且还可以允许每个像素的单个交互,因为图像现在被表示为一维数组。
首先,我想强调一些一开始对我不起作用的方法。我将像素分配给ByteArray,并对这些像素执行正常的alpha混合操作,但由于某些原因,我无法获得所需的结果。我是这样做的:
private fun alphaBlend(imageSrc:Mat, blurredImage: Mat, blurredMask: Mat): Mat{
val total = imageSrc.width() * imageSrc.height()
val channels = imageSrc.channels()
val size = total * channels
val array = ByteArray(size)
val array1 = ByteArray(size)
val array2 = ByteArray(size)
val array3 = ByteArray(size)
val destination = Mat(imageSrc.size(), imageSrc.type())
imageSrc.get(0,0, array)
blurredImage.get(0,0, array1)
blurredMask.get(0,0, array2)
for (index in 0 until size){
val pixel = array[index]
val blurredPixel = array1[index]
val blurVal = (array2[index]) / 255.0f
val newValue = ((blurVal * blurredPixel) + ((1.0f - blurVal) * pixel)).toInt().toByte()
array3[index] = newValue
}
destination.put(0,0, array3)
return destination
}
然后我花了好几天时间弄清楚为什么这不起作用,并决定选择其他有效的原语。我没有使用ByteArray,而是选择了FloatArray因为alpha掩码值应该是0.0到1.0之间的十进制值。在执行此操作之后,我遇到了一些错误。
经过几个小时的调试之后,我意识到图像的数据类型应该与分配给它们的数组原语相匹配,这是由于OpenCV在后台实现的某些条件。
所以在将像素分配给FloatArray原语之前,我将图像转换为float数组所需的数据类型,如下所示:
imageSrc.convertTo(imageSrc, CV_32F)
blurredImage.convertTo(blurredImage, CV_32F)
blurredMask.convertTo(blurredMask, CV_32F)
上述方法解决了这个错误,只是这种转换形式产生了另一个错误。解决方案是通过以下方法将目标mat对象转换为这两个对象之一:
destination.convertTo(destination, CV_8UC3)
以下是使用alpha混合优化图像模糊的完整代码实现:
private fun alphaBlend(imageSrc:Mat, blurredImage: Mat, blurredMask: Mat): Mat{
val total = imageSrc.width() * imageSrc.height()
val channels = imageSrc.channels()
val size = total * channels
val array = FloatArray(size)
val array1 = FloatArray(size)
val array2 = FloatArray(size)
val array3 = FloatArray(size)
imageSrc.convertTo(imageSrc, CV_32F)
blurredImage.convertTo(blurredImage, CV_32F)
blurredMask.convertTo(blurredMask, CV_32F)
val destination = Mat(imageSrc.size(), imageSrc.type())
imageSrc.get(0,0, array)
blurredImage.get(0,0, array1)
blurredMask.get(0,0, array2)
for (index in 0 until size){
val pixel = array[index]
val blurredPixel = array1[index]
val blurVal = (array2[index]) / 255.0f
val newValue = ((blurVal * blurredPixel) + ((1.0f - blurVal) * pixel))
array3[index] = newValue
}
destination.put(0,0, array3)
destination.convertTo(destination, CV_8UC3)
return destination
}
private suspend fun blurImage(bitmap: Bitmap):Bitmap {
return withContext(Dispatchers.IO) {
val imageSrc = Mat()
Utils.bitmapToMat(bitmap, imageSrc)
val innerMask = Mat.zeros(imageSrc.size(), imageSrc.type())
//thickness set to -1 for inner fill
Imgproc.circle(
innerMask,
Point(imageSrc.width() / 2.0, imageSrc.height() / 1.5),
600,
Scalar.all(255.0),
-1
)
val blurredImage = Mat(imageSrc.size(), imageSrc.type())
Imgproc.blur(imageSrc, blurredImage, Size(64.0, 64.0))
val blurredMask = Mat(innerMask.size(), innerMask.type())
Imgproc.blur(innerMask, blurredMask, Size(64.0, 64.0))
val merge = alphaBlend(imageSrc, blurredImage, blurredMask)
val copy = Bitmap.createBitmap(merge.width(), merge.height(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(merge, copy)
copy
}
}
由于我们在一次迭代中使用kotlin数组原语来执行位操作,而不是使用直接访问方法,因此这种方法工作良好,优化效果更好,执行模糊操作的速度也比之前的实现快。
更多技术交流,更多Android资料,请加下方微信领取。