现在市面上有很多 APP,都或多或少对图片有模糊上的设计,所以图片模糊效果到底怎么实现的呢?
首先,我们来了解下模糊效果的对比:
从视觉上,两张图片,有一张是模糊的,那么,在实现图片模糊效果之前,我们首先需要了解图片模糊的本质是什么?
在此介绍模糊本质之前,我们来了解下当前主流的两个移动端平台(Android 与 iOS)的实现。
对 Android 开发者而言,比较熟悉且完善的图片变换三方库以 glide-transformations 为样例,来看看它是基于什么实现的。
https://github.com/wasabeef/glide-transformations
Android 中有两种实现:
①FastBlur,根据 stackBlur 模糊算法来操作图片的像素点实现效果,但效率低,已过时。
②RenderScript,这个是 Google 官方提供的,用来在 Android 上编写一套高性能代码的语言,可以运行在 CPU 及其 GPU 上,效率较高。
而对 iOS 开发者而言,GPUImage 比较主流:
https://github.com/BradLarson/GPUImage/
我们可以在其中看到高斯模糊过滤器(GPUImageGaussianBlurFilter),它里面是根据 OpenGL 来实现,通过 GLSL 语言定义的着色器,操作 GPU 单元,达到模糊效果。
所以,我们可以看出,操作 GPU 来达到我们所需要的效果效率更高。因此我们在 OpenHarmony 上也能通过操作 GPU,来实现我们想要的高性能模糊效果。
回归正题,先来了解下模糊的本质是什么?
模糊的本质
模糊,可以理解为图片中的每个像素点都取其周边像素的平均值。
上图M点的像素点就是我们的焦点像素。周围 ABCDEFGH 都是 M 点(焦点)周围的像素点,那么根据模糊的概念:
我们根据像素点的 r、g、b 值,得到 M 点的像素点值,就这样,一个一个像素点的操作,中间点相当于失去视觉上的焦点,整个图片就产生模糊的效果。
但这样一边倒的方式,在模糊的效果上,达不到需求的,所以,我们就需要根据这个模糊的本质概念,去想想,加一些东西或者更改取平均值的规则,完成我们想要的效果。
故,高斯模糊,一个家喻户晓的名字,就出现在我们面前。
高斯模糊
高斯模糊,运用了正态分布函数,进行各个加权平均,正态分布函数如下:
其中参数:μ 为期望值,σ 为标准差,当 μ=0,σ=0 的时候,为标准的正态分布,其形状参考如下图:
可以看出:
其一,离中心点越近,分配的权重就越高。
这样我们在计算图片的焦点像素值时,将该点当作中心点,当作 1 的权重,其他周围的点,按照该正态分布的位置,去分配它的权重。
这样我们就可以根据该正态分布函数及其各个点的像素 ARGB 值,算出经过正态分布之后的像素 ARGB 值。
其二,离中心点越近,若是设置的模糊半径很小,代表其模糊的焦点周围的像素点离焦点的像素相差就不大,这样模糊的效果就清晰。
而模糊半径越大,其周围分布的像素色差就很大,这样的模糊效果就越模糊。
通过图片的宽高拿到每个像素点的数据,再根据这个正态分布公式,得到我们想要的像素点的 ARGB 值,之后将处理过的像素点重新写入到图片中,就能实现我们想要的图片模糊效果。
实现流程
根据上面的阐述,就可以梳理出在 OpenHarmony 中的具体的实现流程:
-
获取整张图片的像素点数据
-
循环图片的宽高,获取每个像素点的焦点
-
在上述循环里,根据焦点按照正态分布公式进行加权平均,算出各个焦点周围新的像素值
-
将各个像素点写入图片
关键依赖 OpenHarmony 系统基础能力如下:
第一、获取图片的像素点,系统有提供一次性获取整张图片的像素点数据。
其接口如下:
readPixelsToBuffer(dst: ArrayBuffer): Promise<void>;
readPixelsToBuffer(dst: ArrayBuffer, callback: AsyncCallback<void>): void;
可以看出,系统将获取到像素点数据 ARGB 值,存储到 ArrayBuffer 中去。
第二、循环获取每个像素点,将其 x、y 点的像素点当作焦点。
for (y = 0; y < imageHeight; y++) {
for (x = 0; x < imageWidth; x++) {
//...... 获取当前的像素焦点x、y
}
}
第三、循环获取焦点周围的像素点(以焦点为原点,以设置的模糊半径为半径)。
for ( let m = centPointY-radius; m < centPointY+radius; m++) {
for ( let n = centPointX-radius; n < centPointX+radius; n++) {
//......
this.calculatedByNormality(...); //正态分布公式化处理像素点
//......
}
}
第四、将各个图片的像素数据写入图片中。系统有提供一次性写入像素点,其接口如下。
writeBufferToPixels(src: ArrayBuffer): Promise<void>;
writeBufferToPixels(src: ArrayBuffer, callback: AsyncCallback<void>): void;
通过上面的流程,我们可以在 OpenHarmony 系统下,获取到经过正态分布公式处理的像素点,至此图片模糊效果已经实现。
但是,经过测试发现,这个方式实现模糊化的过程,很耗时,达不到我们的性能要求。
若是一张很大的图片,就单单宽高循环来看,比如 1920*1080 宽高的图片就要循环 2,073,600 次,非常耗时且对设备的 CPU 也有非常大的消耗,因此我们还需要对其进行性能优化。
模糊性能优化思路
如上面所诉,考虑到 OpenHarmony 的环境的特点及其系统提供的能力,可以考虑如下几个方面进行优化:
第一:参照社区已有成熟的图片模糊算法处理,如(Android 的 FastBlur)。
第二:C 层性能要比 JS 层更好,将像素点的数据处理,通过 NAPI 机制,将其放入 C 层处理。如:将其循环获取焦点及其通过正态分布公式处理的都放到 C 层中处理。
第三:基于系统底层提供的 OpenGL,操作顶点着色器及片元着色器操作 GPU,得到我们要的模糊效果。
首先,我们来根据 Android 中的 FastBlur 模糊化处理,参照其实现原理进行在基于 OpenHarmony 系统下实现的代码如下:
let imageInfo = await bitmap.getImageInfo();
let size = {
width: imageInfo.size.width,
height: imageInfo.size.height
}
if (!size) {
func(new Error("fastBlur The image size does not exist."), null)
return;
}
let w = size.width;
let h = size.height;
var pixEntry: Array<PixelEntry> = new Array()
var pix: Array<number> = new Array()
let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber());
await bitmap.readPixelsToBuffer(bufferData);
let dataArray = new Uint8Array(bufferData);
for (let index = 0; index < dataArray.length; index+=4) {
const r = dataArray[index];
const g = dataArray[index+1];
const b = dataArray[index+2];
const f = dataArray[index+3];
let entry = new PixelEntry();
entry.a = 0;
entry.b = b;
entry.g = g;
entry.r = r;
entry.f = f;
entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b);
pixEntry.push(entry);
pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b));
}
let wm = w - 1;
let hm = h - 1;
let wh = w * h;
let div = radius + radius + 1;
let r = CalculatePixelUtils.createIntArray(wh);
let g = CalculatePixelUtils.createIntArray(wh);
let b = CalculatePixelUtils.createIntArray(wh);
let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number;
let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h));
let divsum = (div + 1) >> 1;
divsum *= divsum;
let dv = CalculatePixelUtils.createIntArray(256 * divsum);
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}
yw = yi = 0;
let stack = CalculatePixelUtils.createInt2DArray(div, 3);
let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number;
let sir: Array<number>;
let r1 = radius + 1;
for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;
for (x = 0; x < w; x++) {
r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];
sir[0] = (p & 0xff0000)