油画效果
先上未经任何处理的原图
原图.png
然后使用油画风格的滤镜OilPaintFilter看看效果,OilPaintFilter的使用方式就一句话:)
RxImageData.bitmap(bitmap).addFilter(new OilPaintFilter()).into(image);
油画效果.png
OilPaintFilter在处理人物图片和风景图片时具有比较好的效果。
OilPaintFilter的源码如下:
import com.cv4j.core.datamodel.ColorProcessor; import com.cv4j.core.datamodel.ImageProcessor; /** * Created by Tony Shen on 2017/5/7. */ public class OilPaintFilter extends BaseFilter { private int radius = 15; // default value private int intensity = 40; // default value public OilPaintFilter() { this(15, 40); } public OilPaintFilter(int radius, int graylevel) { this.radius = radius; this.intensity = graylevel; } public int getRadius() { return radius; } public void setRadius(int radius) { this.radius = radius; } public int getIntensity() { return intensity; } public void setIntensity(int intensity) { this.intensity = intensity; } @Override public ImageProcessor doFilter(ImageProcessor src) { byte[][] output = new byte[3][R.length]; int index = 0; int subradius = this.radius / 2; int[] intensityCount = new int[intensity+1]; int[] ravg = new int[intensity+1]; int[] gavg = new int[intensity+1]; int[] bavg = new int[intensity+1]; for(int i=0; i<=intensity; i++) { intensityCount[i] = 0; ravg[i] = 0; gavg[i] = 0; bavg[i] = 0; } for(int row=0; row<height; row++) { int ta = 0, tr = 0, tg = 0, tb = 0; for(int col=0; col<width; col++) { for(int subRow = -subradius; subRow <= subradius; subRow++) { for(int subCol = -subradius; subCol <= subradius; subCol++) { int nrow = row + subRow; int ncol = col + subCol; if(nrow >=height || nrow < 0) { nrow = 0; } if(ncol >= width || ncol < 0) { ncol = 0; } index = nrow * width + ncol; tr = R[index] & 0xff; tg = G[index] & 0xff; tb = B[index] & 0xff; int curIntensity = (int)(((double)((tr+tg+tb)/3)*intensity)/255.0f); intensityCount[curIntensity]++; ravg[curIntensity] += tr; gavg[curIntensity] += tg; bavg[curIntensity] += tb; } } // find the max number of same gray level pixel int maxCount = 0, maxIndex = 0; for(int m=0; m<intensityCount.length; m++) { if(intensityCount[m] > maxCount) { maxCount = intensityCount[m]; maxIndex = m; } } // get average value of the pixel int nr = ravg[maxIndex] / maxCount; int ng = gavg[maxIndex] / maxCount; int nb = bavg[maxIndex] / maxCount; index = row * width + col; output[0][index] = (byte) nr; output[1][index] = (byte) ng; output[2][index] = (byte) nb; // post clear values for next pixel for(int i=0; i<=intensity; i++) { intensityCount[i] = 0; ravg[i] = 0; gavg[i] = 0; bavg[i] = 0; } } } ((ColorProcessor) src).putRGB(output[0], output[1], output[2]); output = null; return src; } }
其原理是使用边缘保留滤波,边缘保留滤波有很多种,可以参考之前的一篇文章基于边缘保留滤波实现人脸磨皮的算法。这里主要用的是Mean shift算法,修改局部的像素权重从而实现图像的像素模糊,以达到近似油画的效果。
铅笔画效果
我们还开发了另一款滤镜StrokeAreaFilter,用于模拟铅笔画的效果。
RxImageData.bitmap(bitmap).addFilter(new StrokeAreaFilter()).into(image);
看下效果
铅笔画效果.png
对于铅笔画而言可能有点牵强,那再组合一个随机噪声的滤镜试试。
RxImageData.bitmap(bitmap) .addFilter(new StrokeAreaFilter()) .addFilter(new GaussianNoiseFilter()) .into(image);
铅笔画效果2.png
效果也不是特别好,那再换一个USMFilter试试。
RxImageData.bitmap(bitmap) .addFilter(new StrokeAreaFilter()) .addFilter(new USMFilter()) .into(image);
终于,这次效果比前面两幅效果更好了。
铅笔画效果3.png
但是,由于是两个滤镜的叠加,速度会慢很多。再者,USMFilter它是继承高斯滤镜的。所以,在实际使用中只需单独使用StrokeAreaFilter即可,细节多少可以根据参数来调节。
总结
本文所使用的两款滤镜OilPaintFilter和StrokeAreaFilter都在cv4j中。
cv4j 是gloomyfish和我一起开发的图像处理库,纯java实现,目前还处于早期的版本,目前已经更新了滤镜的文档。
上周末我们做了两款滤镜,效果还算是蛮酷的,但是速度在移动端还不够理想,未来会想办法对算法做一些改进,以便更好地满足移动端的体验。