本项目将使用python3去识别图片是否为色情图片,会使用到PIL这个图像处理库,并且编写算法来划分图像的皮肤区域
介绍一下PIL:
PIL(Python Image Library)是一种免费的图像处理工具包,这个软件包提供了基本的图像处理功能,如:改变图像大小,旋转
图像,图像格式转化,色场空间转换(这个我不太懂),图像增强(就是改善清晰度,突出图像有用信息),直方图处理,插值
(利用已知邻近像素点的灰度值来产生未知像素点的灰度值)和滤波等等。虽然这个软件包要实现复杂的图像处理算法并不太适
合,但是python的快速开发能力以及面向对象等等诸多特点使得它非常适合用来进行原型开发。
在 PIL 中,任何一副图像都是用一个 Image 对象表示,而这个类由和它同名的模块导出,因此,要加载一副图像,最简单的形式
是这样的:
- import Image
- img = Image.open(“dip.jpg”)
复制代码 注意:第一行的 Image 是模块名;第二行的 img 是一个 Image 对象; Image 类是在 Image 模块中定义的。关于 Image 模
块和 Image 类,切记不要混淆了。现在,我们就可以对 img 进行各种操作了,所有对 img 的 操作最终都会反映到到 dip.img 图
像上
环境准备
PIL 2009 年之后就没有更新了,也不支持 Python3 ,于是有了 Alex Clark 领导的公益项目 Pillow 。Pillow 是一个对 PIL 友好的分
支,支持 Python3,所以我们这里安装的是 Pillow,这是它的官方文档。
默认已经有python3.0以上和包管理工具pip3。那要执行如下命令升级pip3并安装Pillow 工具包:
- sudo install -U pip3
- sudo install Pillow
复制代码 程序原理
根据颜色(肤色)找出图片中皮肤的区域,然后通过一些条件判断是否为色情图片。
程序的关键步骤如下:
- python学习交流Q群:906715085####
- 1.遍历每个像素,检查像素颜色是否为肤色
- 2.将相邻的肤色像素归为一个皮肤区域,得到若干个皮肤区域
- 3.剔除像素数量极少的皮肤区域
复制代码 我们定义非色情图片的判定规则如下(满足任意一个判断为真):
- 1.皮肤区域的个数小于3个
- 2.皮肤区域的像素与图像所有像素的比值小于15%
- 3.最大皮肤区域小于总皮肤面积的45%
- 4.皮肤区域数量超过60个
复制代码 这些规则你可以尝试更改,直到程序效果让自己满意为止。关于像素肤色判定这方面,公式可以在网上找到很多,但是世界上不
可能有正确率100%的公式。你可以用自己找到的公式,在程序完成后慢慢调试。
•RGB颜色模式
- 第一种:==r > 95 and g > 40 and g < 100 and b > 20 and max([r, g, b]) - min([r, g, b]) > 15 and abs(r - g) > 15 and r > g and r > b==
- 第二种:==nr = r / (r + g + b), ng = g / (r + g + b), nb = b / (r +g + b) ,nr / ng > 1.185 and r * b / (r + g + b) ** 2 > 0.107 and r * g / (r + g + b) ** 2 > 0.112==
复制代码- •HSV颜色模式
- ==h > 0 and h < 35 and s > 0.23 and s < 0.68==
- •YCbCr颜色模式
- ==97.5 <= cb <= 142.5 and 134 <= cr <= 176==
复制代码- import sys
- import os
- import _io
- from collections import namedtuple
- from PIL import Image
复制代码
接着,遍历每个像素,为每个像素创建对应的 Skin 对象,其中 self._classify_skin() 这个方法是检测像素颜色是否为肤色:
若当前像素并不是肤色,那么跳过本次循环,继续遍历:
- Skin = namedtuple("Skin", "id skin region x y")
复制代码 若当前像素是肤色像素,那么就需要处理了,先遍历其相邻像素。
一定要注意相邻像素的索引值,因为像素的 id 值是从 1 开始编起的,而索引是从 0 编起的。变量 _id是存有当前像素的 id 值,
所以当前像素在 self.skin_map 中的索引值为 _id - 1,以此类推,那么其左方的相邻像素在 self.skin_map 中的索引值为 _id - 1 -
1 ,左上方为 _id - 1 - self.width - 1,上方为 _id - 1 - self.width ,右上方为 _id - 1 - self.width + 1 :
- python学习交流Q群:906715085###
- def __init__(self, path_or_image):
- # 若 path_or_image 为 Image.Image 类型的实例,直接赋值
- if isinstance(path_or_image, Image.Image):
- self.image = path_or_image
- # 若 path_or_image 为 str 类型的实例,打开图片
- elif isinstance(path_or_image, str):
- self.image = Image.open(path_or_image)
复制代码
- # 获得图片所有颜色通道
- bands = self.image.getbands()
- # 判断是否为单通道图片(也即灰度图),是则将灰度图转换为 RGB 图
- if len(bands) == 1:
- # 新建相同大小的 RGB 图像
- new_img = Image.new("RGB", self.image.size)
- # 拷贝灰度图 self.image 到 RGB图 new_img.paste (PIL 自动进行颜色通道转换)
- new_img.paste(self.image)
- f = self.image.filename
- # 替换 self.image
- self.image = new_img
- self.image.filename = f
复制代码 self._add_merge() 这个方法接收两个区域号,它将会把两个区域号添加到 self.merge_regions 中的元素中,self.merge_regions
的每一个元素都是一个列表,这些列表中存放了 1 到多个的区域号,区域号代表的区域是连通的,需要合并。
检测的图像里,有些前几行的像素的相邻像素并没有 4 个,所以需要用 try “试错”。
然后相邻像素的若是肤色像素,如果两个像素的皮肤区域号都为有效值且不同,因为两个区域中的像素相邻,那么其实这两个区
域是连通的,说明需要合并这两个区域。记录下此相邻肤色像素的区域号,之后便可以将当前像素归到这个皮肤区域里了。
遍历完所有相邻像素后,分两种情况处理:
1.所有相邻像素都不是肤色像素:发现了新的皮肤区域。
2.存在区域号为有效值的相邻肤色像素:region 的中存储的值有用了,把当前像素归到这个相邻像素所在的区域。
- # 存储对应图像所有像素的全部 Skin 对象
- self.skin_map = []
- # 检测到的皮肤区域,元素的索引即为皮肤区域号,元素都是包含一些 Skin 对象的列表
- self.detected_regions = []
- # 元素都是包含一些 int 对象(区域号)的列表
- # 这些元素中的区域号代表的区域都是待合并的区域
- self.merge_regions = []
- # 整合后的皮肤区域,元素的索引即为皮肤区域号,元素都是包含一些 Skin 对象的列表
- self.skin_regions = []
- # 最近合并的两个皮肤区域的区域号,初始化为 -1
- self.last_from, self.last_to = -1, -1
- # 色情图像判断结果
- self.result = None
- # 处理得到的信息
- self.message = None
- # 图像宽高
- self.width, self.height = self.image.size
- # 图像总像素
- self.total_pixels = self.width * self.height
复制代码 somenamedtuple._replace(kwargs) 返回一个替换指定字段的值为参数的 namedtuple实例
遍历完所有像素之后,图片的皮肤区域划分初步完成了,只是在变量 self.merge_regions 中还有一些连通的皮肤区域号,它们需
要合并,合并之后就可以进行色情图片判定了:
- def resize(self, maxwidth=1000, maxheight=1000):
- """
- 基于最大宽高按比例重设图片大小,
- 注意:这可能影响检测算法的结果
- 如果没有变化返回 0
- 原宽度大于 maxwidth 返回 1
- 原高度大于 maxheight 返回 2
- 原宽高大于 maxwidth, maxheight 返回 3
- maxwidth - 图片最大宽度
- maxheight - 图片最大高度
- 传递参数时都可以设置为 False 来忽略
复制代码 方法 self._merge() 便是用来合并这些连通的皮肤区域的。方法 self._analyse_regions(),运用之前在程序原理一节定义的非色情
图像判定规则,从而得到判定结果。现在编写我们还没写过的调用过的 Nude 类的方法。
首先是 self._classify_skin() 方法,这个方法是检测像素颜色是否为肤色,之前在程序原理一节已经把肤色判定该公式列举了出
来,现在是用的时候了:
- """
- # 存储返回值
- ret = 0
- if maxwidth:
- if self.width > maxwidth:
- wpercent = (maxwidth / self.width)
- hsize = int((self.height * wpercent))
- fname = self.image.filename
- # Image.LANCZOS 是重采样滤波器,用于抗锯齿
- self.image = self.image.resize((maxwidth, hsize), Image.LANCZOS)
- self.image.filename = fname
- self.width, self.height = self.image.size
- self.total_pixels = self.width * self.height
- ret += 1
- if maxheight:
- if self.height > maxheight:
- hpercent = (maxheight / float(self.height))
- wsize = int((float(self.width) * float(hpercent)))
- fname = self.image.filename
- self.image = self.image.resize((wsize, maxheight), Image.LANCZOS)
- self.image.filename = fname
- self.width, self.height = self.image.size
- self.total_pixels = self.width * self.height
- ret += 2
- return ret
复制代码
- Image.resize(size, resample=0)
- size – 包含宽高像素数的元祖 (width, height) resample – 可选的重采样滤波器
- 返回 Image 对象
- 然后便是最关键之一的解析方法了:
- def parse(self):
- # 如果已有结果,返回本对象
- if self.result is not None:
- return self
- # 获得图片所有像素数据
- pixels = self.image.load()
复制代码- python学习交流Q群:906715085###
- for y in range(self.height):
- for x in range(self.width):
- # 得到像素的 RGB 三个通道的值
- # [x, y] 是 [(x,y)] 的简便写法
- r = pixels[x, y][0] # red
- g = pixels[x, y][1] # green
- b = pixels[x, y][2] # blue
- # 判断当前像素是否为肤色像素
- isSkin = True if self._classify_skin(r, g, b) else False
- # 给每个像素分配唯一 id 值(1, 2, 3...height*width)
- # 注意 x, y 的值从零开始
- _id = x + y * self.width + 1
- # 为每个像素创建一个对应的 Skin 对象,并添加到 self.skin_map 中
- self.skin_map.append(self.Skin(_id, isSkin, None, x, y))
复制代码
Nude 类如果就这样完成了,最后运行脚本时只能得到一些真或假的结果,我们需要更直观的感受程序的分析效果,我们可以生成
一张原图的副本,不过这个副本图片中只有黑白色,白色代表皮肤区域,那么这样我们能直观感受到程序分析的效果了。
前面的代码中我们有获得图像的像素的 RGB 值的操作,设置像素的 RGB 值也就是其逆操作,还是很简单的,不过注意设置像素的 RGB 值时不能在原图上操作:
- # 若当前像素不为肤色像素,跳过此次循环
- if not isSkin:
- continue
复制代码 变量 skinIdSet 使用集合而不是列表是有性能上的考量的,Python 中的集合是哈希表实现的,查询效率很高。最后支持一下命令
行参数就大功告成啦!我们使用 argparse 这个模块来实现命令行的支持。argparse 模块使得编写用户友好的命令行接口非常容
易。程序只需定义好它要求的参数,然后 argparse 将负责如何从 sys.argv 中解析出这些参数。argparse 模块还会自动生成帮助
和使用信息并且当用户赋给程序非法的参数时产生错误信息。
具体使用方法请查看argparse的 官方文档
- # 设左上角为原点,相邻像素为符号 *,当前像素为符号 ^,那么相互位置关系通常如下图
- # ***
- # *^
- # 存有相邻像素索引的列表,存放顺序为由大到小,顺序改变有影响
- # 注意 _id 是从 1 开始的,对应的索引则是 _id-1
- check_indexes = [_id - 2, # 当前像素左方的像素
- _id - self.width - 2, # 当前像素左上方的像素
- _id - self.width - 1, # 当前像素的上方的像素
- _id - self.width] # 当前像素右上方的像素
复制代码 测试效果
先来一张很正经的测试图片:
在PyCharm中的终端运行下面的命令执行脚本,注意是python3而不是python:
python3 nude.py -v 1.jpg
运行截图:
这表示1.jpg不是一张色情图片
总结
这个项目就是熟悉了一下PIL的使用,了解了色情图片检查的原理。主要实现难点是在皮肤区域的检测与整合这一方面。项目还有
许多可以改进的地方,比如肤色检测公式,色情判定条件,还有性能问题,我得去学习一下用多线程或多进程提高性能。关于这
篇文章,喜欢的小伙伴记得点赞收藏,关注我才能看更多有意思的,有不懂的地方记得评论留言哟!!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |