“万恶”的马赛克是怎么实现的呢?
最近参加[声网的RTC开发者大赛](https://segmentfault.com/page/rtc-
hackathon-2020)开发了一个陌生人[视频聊天应用](https://github.com/Luomingbear/RTC-
Hackathon/tree/master/SDKChallengeProject/likemosaic),为了降低聊天时的紧张感,对视频画面进行马赛克处理。此文便是对马赛克算法实现的总结。
原理
图片是由一个个像素点组成的,由于间距很小,便形成了图像的效果。将图片变成马赛克样式的本质就是修改像素点,使得一定范围内的像素用相同的颜色值表示。这里的“一定范围”我们暂且称之为马赛克块,马赛克块的大小也就决定了图片马赛克程度的不同。以3x3的像素点大小的马赛克块为例,原始像素点的值为1...9
,修改之后全部为n
,如下图所示。
根据这个原理,我们只需要将图片分为一个一个的马赛克块,然后遍历它们,使每一个马赛克块用相同的颜色值表示即可。
确定马赛克块坐标
将一个马赛克块看作一个整体,图片就被分成了m_width * m_height
个马赛克块,那么对于横坐标x,纵坐标y的马赛克块的下标可以通过如下的公式获得。
index = (y - 1) * m_width + x
确定颜色值
得到马赛克块之后,就可以把这个区域的像素点修改为同一个值了,这里有几个思路,分别是
- 使用左上角的值直接替换其他点的值;
- 计算区域像素点的平均值然后替换每一个点;
- 使用区域内像素点的中位数值作为其他点的值。
三种方案在显示效果和计算速度上也有差异,方案1计算快,但是显示会不够平滑。方案2显示最为平滑,计算量比方案1多,但是比方案3更少。方案3因为需要求出中位数,会增加很多比较操作,所以计算量最多,显示效果上也没有平均值的方案平滑,但是轮廓感更强。
颜色空间
上面仅仅是在理论层面介绍了如何实现马赛克效果,但是在实际的编码中,我们还需要了解图形在数据里面是怎么表示的。这就涉及到 颜色空间
的概念了,常见的颜色空间有RGB、YUV和CMYK(主要应用于打印场景,本文不讲解)等。
RGB
RGB颜色空间是根据颜色在自然界的产生原理来设计的,把颜色分为R:红色、G:绿色和B:蓝色,也就是三原色。一般使用RGB24(也称为RGB888)的颜色空间,即RGB三个分量各使用8bit表示,所以一张图片占用的内存大小就是width _height_ 3
字节。RGB颜色空间在计算机里面主要用于图形的采集、显示等领域,例如我们的显示屏显示画面时会使用RGB颜色空间对每一个像素点进行赋值显示。
RGB24的数据排列方式为按照像素点将数据分组,每一组有RGB三个分量,对于每一组数据,按照B->G->R
的顺序排列,如下图所示。
YUV
YUV颜色空间将颜色分为了亮度分量Y和色度分量U和V,因为YUV将亮度分量独立了出来,所以可以兼容老的黑白电视机。常见的YUV格式有YUV444、YUV422和YUV420。对于YUV444、YUV422和YUV420格式的区别,主要是UV分量的分布,如图,空心圆表示UV分量,实心圆表示Y分量。
使用YUV420格式相比RGB可以减少一半的带宽,YUV420格式占用的内存大小为width _height + width_ height /4 + width * height /4
字节。
由于YUV420可以节省大量带宽,而人眼对色度信息没有亮度敏感,所以观感不会有明显区别,日常应用最为广泛。根据UV分量在数据中的排布方式,YUV420又分为I420、NV12和YV12等格式。
I420 I420格式的数据排布如下图所示,先将Y分量全部排进去,然后排U分量最后是V分量。
YV12 YV12格式的数据排布如下图所示,先将Y分量全部排进去,然后排V分量最后是U分量。
NV12 NV12格式的数据排布如下图所示,先将Y分量全部排进去,然后U和V分量交替排列。
C语言实现
下面给出I420格式及RGB24格式图像进行马赛克处理的C语言实现,其中马赛克块的颜色值使用平均值法获得,当然此算法还有很多可以改进的地方,例如可考虑加入多线程并发处理提高算法效率。
输入RGB格式数据
/**
对像素数据进行马赛克处理
input_data : 输入的原始像素数据
width : 图片的宽度
height: 图片的高度
channel : 图像通道数,例如RGB24则有3个通道
scale : 马赛克块的大小
return : 处理之后的像素数据
/
void mosaic(unsigned char *input_data, unsigned char *out_data,int width, int height, int channel, int scale)
{
int index, tindex;
int pix[channel];
for (int i = 0; i < height; i += scale)
{for (int j = 0; j < width; j += scale) { index = (width * i + j) * channel; for (int d = 0; d < channel; d++) { pix[d] = 0; } //平均值 for (int k = 0; k < scale; k++) { for (int p = 0; p < scale; p++) { tindex = index + (k * width + p) * channel; if (tindex < width * height * channel - channel) { for (int d = 0; d < channel; d++) { pix[d] += input_data[tindex + d]; } } } } for (int d = 0; d < channel; d++) { pix[d] = pix[d] / scale / scale; } for (int k = 0; k < scale; k++) { for (int p = 0; p < scale; p++) { tindex = index + (k * width + p) * channel; if (tindex < width * height * channel - channel) { for (int d = 0; d < channel; d++) { out_data[tindex + d] = pix[d]; } } } } }
}
}
输入I420格式数据
/**
对I420格式图像进行马赛克处理
input_y : 输入数据的y分量
input_u : 输入数据的u分量
input_v : 输入数据的v分量
out_y : 输出数据的y分量
out_u : 输出数据的u分量
out_v : 输出数据的v分量
width : 图片的宽度
height: 图片的高度
scale : 马赛克块的大小
/
void mosaicyuv(unsigned char *input_y, unsigned char *input_u, unsigned char *input_v,unsigned char *out_y, unsigned char *out_u, unsigned char *out_v, int width, int height, int scale)
{
int len = width * height;
memcpy(out_y, input_y, len);
memcpy(out_u, input_u, len / 4);
memcpy(out_v, input_v, len / 4);
int index, tindex, y;
for (int i = 0; i < height; i += scale)
{for (int j = 0; j < width; j += scale) { index = width * i + j; y = out_y[index]; for (int k = 0; k < scale; k++) { for (int p = 0; p < scale; p++) { tindex = index + (k * width + p); if (tindex < len) { y += out_y[tindex]; } } } y = y / scale / scale; for (int k = 0; k < scale; k++) { for (int p = 0; p < scale; p++) { tindex = index + (k * width + p); if (tindex < len) { out_y[tindex] = y; } } } }
}
//处理UV分量
int u, v;
index = tindex = u = v = 0;
scale = scale / 2;
for (int i = 0; i < height / 2; i += scale)
{for (int j = 0; j < width / 2; j += scale) { index = width / 2 * i + j; u = v = 0; for (int k = 0; k < scale; k++) { for (int p = 0; p < scale; p++) { tindex = index + (k * width / 2 + p); if (tindex < len / 4) { u = u + out_u[tindex]; v = v + out_v[tindex]; } } } u = u / scale / scale; v = v / scale / scale; for (int k = 0; k < scale; k++) { for (int p = 0; p < scale; p++) { tindex = index + (k * width / 2 + p); if (tindex < len / 4) { out_u[tindex] = u; out_v[tindex] = v; } } } }
}
}