滤镜的诞生,从原理到实践手把手教学。

1,685 阅读8分钟
原文链接: mp.weixin.qq.com

 前言     滤镜几乎是所有相机类图片类的app都必不可免的一个功能。我们今天就来讲讲滤镜是如何实现的,从图像的一些基础概念入手,一步一步解析到实现一个图片滤镜处理的小demo来加固这个知识点,因为笔者是做Android开发,所以就直接用顺手的方式来写这个demo,但是这个知识点不仅仅是在android上适用 ,理论上适用于任何端的图片处理。

  1. 什么是图片,图片在计算机中是如何表示的?

  2. 图片所谓的对比度,亮度,饱和度,色调就就是什么?

  3. 如果让你从0开始,不使用任何的lib,让你实现一个滤镜,你如何下手?

这里笔者提了3个问题,希望读者可以带着问题来阅读文章,这样能加深体会。

图片是什么?

    通俗易懂的讲 彩色图像是由一个个像素点合成的,而每个像素点又是由红(R)绿(G)蓝(B)三原色的三个分量来表示的,每个8个bit(0-255) M、N分别表示图像的行列数,三个M x N的二维矩阵分别表示各个像素的R、G、B三个颜色分量。RGB图像的数据类型一般为8位无符号整形。

    图片亮度:顾名思义,图片看起来亮或者暗,就是由亮度决定的,而决定图片亮度的又是什么?#FFFFFF和#000000 大家应该都很熟悉这2个颜色代码吧 白色和黑色,对于一张图片来说,颜色越接近白色越亮,颜色越接近黑色越暗,所以当你想改变一张图片的亮度时,只需要比原理的值更高或者更低就可以了。     图片对比度:对比 度就是颜色之间的对比,不同颜色之间的差别,对比度越大,颜色的差异越大,即所谓的黑白分明,对比度越大。如何改变对比度?也是从RGB入手,把我们原来浅颜色变更浅,高的颜色变更高,就可以了。     图片饱和度:是指色彩的鲜艳程度,也称色彩的纯度。饱和度取决于该色中含色成分和消色成分(灰色)的比例。含色成分越大,饱和度越大;消色成分越大,饱和度越小。纯的颜色都是高度饱和的,如鲜红,鲜绿等。

色调和色相什么的笔者就不提了,有兴趣的可以自己深入了解一下。

滤镜是什么?

    滤镜其实原理很简单,就是对图片的对比度,亮度,饱和度,色调,色相等进行改变,更进一步的实现,其实就是对图片的RGB值进行修改,从而达到效果。

如何改变RGB值达到效果?(要开始上代码了!!!这里举2个最简单的例子,让大家大概的了解一下。)

我们先从最简单的亮度改变说起吧  就关键代码就4行(如下)

当我们需要改变一张图片的亮度时,只需要取出一个像素点的R G B 值,对其做加减法运算  Value可以为负值,当RGB的数值提高时,图片越亮,RGB的数值降低时,图片越暗。

red += value;  green += value;blue += value;//省略了一些边界判断 每个颜色最多不能超过255,也不能低于0。pixels[i] = (pixels[i] & 0xFF000000) | ((red << 16) & 0x00FF0000) | ((green << 8) & 0x0000FF00) | (blue & 0x000000FF);

接下来我们说一下对比度 代码如下

我们可以看到,我们先把图片的RGB值取出进行运算,上面我们已经说到,对比度其实就是黑白分明,其实就是将RGB的值越大的变越大,越小的变越小,所以我们可以看到,当比值小于0.5的将变得更小,比值大于0.5的变得更大。

red = (((((red / 255.0) - 0.5) * value) + 0.5) * 255.0);green = (((((green / 255.0) - 0.5) * value) + 0.5) * 255.0);blue = (((((blue / 255.0) - 0.5) * value) + 0.5) * 255.0);//省略了一些边界判断 每个颜色最多不能超过255,也不能低于0。pixels[i] = (pixels[i] & 0xFF000000) | ((R << 16) & 0x00FF0000) | ((G << 8) & 0x0000FF00)| (B & 0x000000FF);

其他的比较复杂,篇章有限,我们这里就不讲了,笔者会帖一些连接给大家去深入。

最后来简单实现一下Snapseed这个APP的一个曲线功能

实践

    这个例子很简单,snapseed是可以用N个点来画这个曲线的,我们这里只是用了一个点来而已,原理是一样的,如果大家有兴趣,可以自己再完善一下。我们先来看下Demo效果

我用了一条贝塞尔曲线来控制图片的RGB,初始状态下为一条直线,1:1的状态,说明了和原图的RGB值一致,没有改变,这里先说明一下,我们按照数学中的坐标系来定义这条线,X轴为原始RGB值,Y轴改变后的RGB值,因此可以看到,当我们往上拉得时候,图片会变得更亮,这符合我们上面所说的RGB值越大,亮度就越大。当我们往下拉得时候,图片变暗,也符合我们所说得值越小,亮度也就越小。我们只有一个点的话,就只能实现整体的变化,但是当我们有2个点的话,那就可以实现一条类似正弦函数(忽高忽低/忽低忽高)的曲线,这样可以做到什么效果?就是改变我们的对比度,在RGB值小的地方变得更小,在RGB值大的地方变得更大,对比度就上去了。如下图(这里借用了下snapseed的效果):

接下来我们说下具体怎么去实现,撸码环节;

主要分为3步:

  1. 曲线的绘制

  2. 数据的转换

  3. 对图片进行处理(重点 这里由于效率问题,用native实现的,大家如果对C/C++不熟,用java写也是可以的)

曲线的绘制:

很简单,没有什么好讲的,一条二阶的贝塞尔曲线

Path path = new Path();path.moveTo(0, mHeight); path.quadTo(mTouchX, mTouchY, mWidth, mHeight - mWidth);canvas.drawPath(path, mPaint);

数据的转换:

 

上面我们说了 这条线代表着RGB,当然这条线也可以单独代表R,G,B任何一个,snapseed也有单独处理这个,原理是一样的,我们就不说了,所以这个条线的值是必须在一个范围内的0-255,所以数据必须处理。

int[] allPoints = new int[256];for (int x = 0; x < 256; x++) {   PathInterpolator pathInterpolator = new PathInterpolator(mPath);   allPoints[x] = (int) (255.0f * pathInterpolator.getInterpolation((float) x / 255.0f));}

这应该也不能看懂吧,通俗点讲这个操作是将曲线里面的y值拿出来,我们得到一个256的数组,这个数组我们就要拿去处理图片了。

处理图片:

rgb数组是我们就是我们上面得到的那个256位的数组了,R,G,B数组都是用来存放改变后的值的,最后再将每个像素点的RGB值转换合并。

int R[256];int G[256];int B[256];for (int i = 0; i < 256; i++) {    R[i] = (rgb[i] << 16) & 0x00FF0000;    G[i] = (rgb[i] << 8) & 0x0000FF00;    B[i] = rgb[i] & 0x000000FF; }for (int i = 0; i < width * height; i++) {    pixels[i] =(0xFF000000 & pixels[i]) | (R[(pixels[i] >> 16) & 0xFF]) | (G[(pixels[i] >> 8) & 0xFF]) | (B[pixels[i] & 0xFF]);}

到这里就已经基本上完成了,当然处理图片是耗时的,尽量要放到子线程去执行,如果想要达到Snapseed的效果,那么你只需要在曲线上面下功夫即可,图像处理的代码是不需要改动的,如果你有兴趣,就自己动手撸一遍吧,有哪里不明白的可以留言。如果有哪里不对,也可以留言告诉我。

如果大家觉得文章可以,关注下,点个好看,感谢。

本文参考资料:https://github.com/Zomato/AndroidPhotoFilters