用PIL实现滤镜(一)——素描、铅笔画效果
在计算机图形学发展史中,真实感绘制一直是主旋律。不过从20实际90年代中期开始,非真实感图像绘制(Non-Photorealistic Rendering,NPR)逐渐成为一个研究热点。说白了,真实感绘制目标是像照片般真实地再现客观世界,而非真实感图像绘制专注于图形个性化和艺术化的表达,它主要用来表现图形的艺术特质,以及模拟艺术作品(甚至包括作品中的缺陷)。
在介绍完非真实感图像绘制之后,我们再来提及一下PIL——Python Imaging Library(官方网址)。相信使用python的朋友们都不会陌生,因为在web应用中我们常常用它来生成缩略图。从名字也可以看出,PIL主要用来处理图片,它支持多种图片格式,并提供强大的图片和图像处理能力。详细的关于PIL的内容大家可以参阅手册。
这个系列,我们就主要使用PIL来进行滤镜方面的处理,包括素描、铅笔画、油画等等滤镜效果的实现。在以后我会把代码托管出来。
我们从素描效果开始。在PIL中,基础的类是Image类,首先有必要讲一下图片的mode。它有以下几种:
- 1 (1-bit pixels, black and white, stored with one pixel per byte)
- L (8-bit pixels, black and white):用来表示灰度图
- P (8-bit pixels, mapped to any other mode using a colour palette)
- RGB (3x8-bit pixels, true colour)
- RGBA (4x8-bit pixels, true colour with transparency mask)
- CMYK (4x8-bit pixels, colour separation)
- YCbCr (3x8-bit pixels, colour video format)
- I (32-bit signed integer pixels)
- F (32-bit floating point pixels)
在开始素描效果之前,我们需要首先进行灰度图像预处理。所幸的是,用PIL非常容易实现。设img是Image类的实例,我们只要用convert函数强制转换为L模式即可。
img = img.convert("L")
不过还是有必要讲一下灰度预处理。何谓图像灰度化呢?图像灰度化即是使色彩的三种颜色分量的R,G,B的分量值相等,由于R,G,B的取值范围是[0, 255],所以灰度图像能够表示256种灰度颜色******像灰度法主要有三种算法:
- 最大值法(Maximum):使R、G、B的值等于三个色彩分量中的最大的一个分量值,即:R=G=B=Max(R,G,B)。
- 平均值法(Average):使R、G、B的值等于三个色彩分量的三个色彩分量的平均值,即:R=G=B= (R+G+B)/3。
- 加权平均值法(Weight Average):在这里我给R、G、B三分量分别附上不同的权值,表示为:R=G=B=WR*R+WG*G+WB*B ,其中WR,WG,WB分别是R、G、B的权值。在这里考虑由于人眼对绿色的敏感度最高,红色次之,对蓝色的敏感度最低,因此,当权值 WG > WR > WB时,所产生的灰度图像更符合人眼的视觉感受。PIL库使用ITU-R 601-2 luma transform:
L = R * 299/1000 + G * 587/1000 + B * 114/1000
即 WR=29.9%,WG=58.7%,WB=11.4%。
素描滤镜的处理关键是对边缘的查找。通过对边缘的查找可以得到物体的线条感。在对图像进行灰度化处理后,我们首先定义一个阈值(threshold)。我们知道素描主要强调的是明暗度的变化,绘制时是斜向方向,通过经验,我们将每个像素点的灰度值与其右下角的灰度值进行比较,当大于这个阈值时,就判断其是轮廓并绘制。
以下是素描滤镜的主函数:
from PIL import Image def sketch(img, threshold): ''' 素描 param img: Image实例 param threshold: 介于0到100 ''' if threshold < 0: threshold = 0 if threshold > 100: threshold = 100 width, height = img.size img = img.convert('L') # convert to grayscale mode pix = img.load() # get pixel matrix for w in xrange(width): for h in xrange(height): if w == width-1 or h == height-1: continue src = pix[w, h] dst = pix[w+1, h+1] diff = abs(src - dst) if diff >= threshold: pix[w, h] = 0 else: pix[w, h] = 255 return img
接着,我们写一个测试部分来看看效果:
if __name__ == "__main__": import sys, os path = os.path.dirname(__file__) + os.sep.join(['', 'images', 'lam.jpg']) threshold = 15 if len(sys.argv) == 2: try: threshold = int(sys.argv[1]) except ValueError: path = sys.argv[1] elif len(sys.argv) == 3: path = sys.argv[1] threshold = int(sys.argv[2]) img = Image.open(path) img = sketch(img, threshold) img.save(os.path.splitext(path)[0]+'.sketch.jpg', 'JPEG')
可以在命令行中指定文件名和阈值,或者只指定阈值,或者不带参数。我的测试图片为:
效果图片:
不同的阈值,生成的效果不同。阈值越小,绘制的像素点就越多。
对于铅笔画来说,原理和素描十分相似,但是大家学过画画的就知道,素描强调的是阴影的效果,是斜向作画,而铅笔画主要是勾勒轮廓。因此在对每个像素点的处理上,就和素描产生变化。对于任意一个像素点,求出这个像素点的R、G、B三个分量与周围8个点的相应分量的平均值的差,如果这三个差都大于或者等于某个阈值,就画出线条。最后,铅笔画的作画不是单调的一种颜色,因此加入Alpha分量,大小等于对应点的alpha分量即可。于是,代码如下;
def pencil(img, threshold): ''' 铅笔画 param img: instance of Image param threshold ''' if threshold < 0: threshold = 0 if threshold > 100: threshold = 100 width, height = img.size dst_img = Image.new("RGBA", (width, height)) if img.mode != "RGBA": img = img.convert("RGBA") pix = img.load() dst_pix = dst_img.load() for w in xrange(width): for h in xrange(height): if w == 0 or w == width - 1 \ or h == 0 or h == height - 1: continue # 包括当前像素周围共9个像素点 around_wh_pixels = [pix[i, j][:3] for j in xrange(h-1, h+2) for i in xrange(w-1, w+2)] # 排除当前像素点 exclude_wh_pixels = tuple(around_wh_pixels[:4] + around_wh_pixels[5:]) # 把各个像素点的各个分量求平均值 RGB = map(lambda l: int(sum(l) / len(l)), zip(*exclude_wh_pixels)) cr_p = pix[i, j] # 当前像素点 cr_draw = all([abs(cr_p[i] - RGB[i]) >= threshold for i in range(3)]) if cr_draw: dst_pix[w, h] = 0, 0, 0, cr_p[3] else: dst_pix[w, h] = 255, 255, 255, cr_p[3] return dst_img
效果如图:
特别愿意看你这里,学到很多东西
查找边缘滤镜竟然这么简单质朴!
另外,内置的abs() 可以简化代码.
忘了用python内建的丰富函数了,汗。
已改正。
素描的话,不知道能不能将两个方向的素描图重叠起来。不过这种方法对阙值依赖较大。。
可以计算左上角和右下角的的灰度值的平均数然后比较,不过为了计算简便就没这么做。
额,你是交大的么?
是啊~
童鞋是电院的还是软院的?
我要到9月份才去。
电院。我7,8月也在,9月继续留下来读研。。
那今年才毕业?
我看你的博客好像都是和图像处理有关的,你是学什么的呢?
EE。毕设刚好是做android图像的。就顺手写了点。。
素描滤镜用java语言怎么实现呢?主要是不懂怎么转化为灰度模式和得到像素矩阵。
老秦,突然回忆起过去用过的一个好工具NetBeans,当初是因为无法令其很好的完全自动的支持UTF8格式py文件保存,而放弃了它。我最近在看你文章感受了一些你的编码习惯,突然想起有点像是用的传奇NB,不知是不是哈……想请教你一下:你有办法令NB自动支持所有UTF8格式的问题吗?关注:)~
这个NetBeans编码问题我半年了都没解决,网上的方法顶多是让你可以打开UTF8的文件而已,但NB新生成的py文件依然默认是GB2312格式……你这边有没有使用NB新建任何文件时就可以自动创建默认为UTF8的文件的方法吗?! 老秦你是少壮派,有空时帮我试试呗,没准到你手上能摆平~ 目前没辙,只能先用EmEditor将py文件先转成UTF8格式后,再用NB编辑~ 一点都不自动化 ~~ 有一种骑驴进拉斯维加斯******的感觉~
NetBeans是个不错的工具,不过很可惜,我不是用的NetBeans。我用的实际上是Eclipse+PyDev,其实我觉得这是比较推荐的Py开发环境,在UTF-8编码方面,这个组合没有任何问题。
看你的留言都睡好晚,这样可不好,虽然技术宅都睡得晚:D
哈谢谢~最近有任务就所以熬夜了~人在江湖身不由己了。Eclipse和pydev老秦你有什么好资料么,给我们推荐几个你认为最棒的吧:)~如果可以,期待你以后出版Eclipse开发环境的文章哦,我一定率先捧场哈~
现在才发现评论,汗。
Eclipse方面当时都是自己鼓捣的,也不晓得有什么文章。有机会写个篇把吧,其实用着就熟啦,另外,中国的大牛limodo的Ulipad据说也不错的。
那个公式不是(使用 sRGB 的) Rec601Luma,而是(使用 RGB 的) Rec601Luminance。我是说怎么结果不一样呢。参见 ImageMagick 这里的文档: http://www.imagemagick.org/script/command-line-options.php#intensity
给作者留言
关于作者
残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。
博客分类
搜索
点击排行
标签云
扫描访问
主题
残阳似血的微博
登录