2.2 程序原理
本程序根据颜色(肤色)找出图片中皮肤的区域,然后通过一些条件判断是否为色情图片。
程序的关键步骤如下:
HSV 颜色模式
h > 0 and h < 35 and s > 0.23 and s < 0.68
YCbCr 颜色模式
97.5 <= cb <= 142.5 and 134 <= cr <= 176
一幅图像有零个到多个的皮肤区域,程序按发现顺序给它们编号,第一个发现的区域编号为 0,第 n 个发现的区域编号为 n-1
我们用一种类型来表示像素,我们给这个类型取名为 Skin ,包含了像素的一些信息:唯一的 编号( id ),是/否肤色( skin ),皮肤区域号( region ),横坐标( x ),纵坐标( y)
接下来实现细节部分
2.3 实现脚本
在 /home/shiyanlou/ 目录下新建 nude.py 文件,我们将在这个文件中进行代码的编写:
导入所需要的模块
importsysimportosimport_iofromcollectionsimportnamedtuplefromPILimportImage
我们将设计一个 Nude 类:
classNude(object):
这个类里面我们首先使用 collections.namedtuple() 定义一个 Skin 类型
Skin= namedtuple("Skin","id skin region x y")
def__init__(self, path_or_image):# 若 path_or_image 为 Image.Image 类型的实例,直接赋值ifisinstance(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 图iflen(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.imageself.image = new_imgself.image.filename = f# 存储对应图像所有像素的全部 Skin 对象self.skin_map = []# 检测到的皮肤区域,元素的索引即为皮肤区域号,元素都是包含一些 Skin 对象的列表self.detected_regions = []# 元素都是包含一些 int 对象(区域号)的列表# 这些元素中的区域号代表的区域都是待合并的区域self.merge_regions = []# 整合后的皮肤区域,元素的索引即为皮肤区域号,元素都是包含一些 Skin 对象的列表self.skin_regions = []# 最近合并的两个皮肤区域的区域号,初始化为 -1self.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
本实验代码中使用到的模块中的函数均可以在其模块的文档中找到,一定要培养查阅文档的习惯
isinstane(object, classinfo)
如果参数 object 是参数 classinfo 的实例,返回真,否则假;参数 classinfo 可以是一个包含若干 type 对象的元祖,如果参数 object 是其中任意一个类型的实例,返回真,否则假
涉及到效率问题,越大的图片所需要消耗的资源与时间越大,因此有时候可能需要对图片进行缩小
所以需要有图片缩小方法
若当前像素并不是肤色,那么跳过本次循环,继续遍历
# 若当前像素不为肤色像素,跳过此次循环ifnotisSkin:continue
若当前像素是肤色像素,那么就需要处理了,先遍历其相邻像素
检测的图像里,有些前几行的像素的相邻像素并没有 4 个,所以需要用 try “试错”
然后相邻像素的若是肤色像素,如果两个像素的皮肤区域号都为有效值且不同,因为两个区域中的像素相邻,那么其实这两个区域是连通的,说明需要合并这两个区域。记录下此相邻肤色像素的区域号,之后便可以将当前像素归到这个皮肤区域里了。
遍历完所有相邻像素后,分两种情况处理
方法 self._merge() 便是用来合并这些连通的皮肤区域的
方法 self._analyse_regions() ,运用之前在程序原理一节定义的非色情图像判定规则,从而得到判定结果
现在编写我们还没写过的调用过的 Nude 类的方法
首先是 self._classify_skin() 方法,这个方法是检测像素颜色是否为肤色,之前在程序原理一节已经把肤色判定该公式列举了出来,现在是用的时候了
颜色模式的转换并不是本实验的重点,转换公式可以在网上找到,这里我们直接拿来用就行
def _to_normalized(self, r, g, b):ifr==0:r=0.0001ifg==0:g=0.0001ifb==0:b=0.0001_sum= float(r + g + b) return [r / _sum, g / _sum, b / _sum]def _to_ycbcr(self, r, g, b):# 公式来源:# http://stackoverflow.com/questions/19459831/rgb-to-ycbcr-conversion-problemsy= .299*r + .587*g + .114*bcb=128-0.168736*r -0.331364*g +0.5*bcr=128+0.5*r -0.418688*g -0.081312*b return y, cb, crdef _to_hsv(self, r, g, b):h=0_sum= float(r + g + b)_max= float(max([r, g, b]))_min= float(min([r, g, b]))diff= float(_max - _min)if_sum==0:_sum=0.0001if_max== r:ifdiff==0:h= sys.maxsizeelse:h= (g - b) / diff elif_max== g:h=2+ ((g - r) / diff)else:h=4+ ((r - g) / diff) h *=60ifh <0: h +=360return [h,1.0- (3.0* (_min / _sum)), (1.0/3.0) * _max]
def_add_merge(self, _from, _to):# 两个区域号赋值给类属性self.last_from = _fromself.last_to = _to# 记录 self.merge_regions 的某个索引值,初始化为 -1from_index = -1# 记录 self.merge_regions 的某个索引值,初始化为 -1to_index = -1# 遍历每个 self.merge_regions 的元素forindex, regioninenumerate(self.merge_regions):# 遍历元素中的每个区域号forr_indexinregion:ifr_index ==_from:from_index = indexifr_index ==_to:to_index = index# 若两个区域号都存在于 self.merge_regions 中iffrom_index != -1andto_index != -1:# 如果这两个区域号分别存在于两个列表中# 那么合并这两个列表iffrom_index !=to_index:self.merge_regions[from_index].extend(self.merge_regions[to_index]) del(self.merge_regions[to_index])return# 若两个区域号都不存在于 self.merge_regions 中iffrom_index == -1andto_index == -1:# 创建新的区域号列表self.merge_regions.append([_from, _to])return# 若两个区域号中有一个存在于 self.merge_regions 中iffrom_index != -1andto_index == -1:# 将不存在于 self.merge_regions 中的那个区域号# 添加到另一个区域号所在的列表self.merge_regions[from_index].append(_to)return# 若两个待合并的区域号中有一个存在于 self.merge_regions 中iffrom_index == -1andto_index != -1:# 将不存在于 self.merge_regions 中的那个区域号# 添加到另一个区域号所在的列表self.merge_regions[to_index].append(_from)return
def _merge(self, detected_regions, merge_regions):# 新建列表 new_detected_regions # 其元素将是包含一些代表像素的 Skin 对象的列表# new_detected_regions 的元素即代表皮肤区域,元素索引为区域号new_detected_regions= []# 将 merge_regions 中的元素中的区域号代表的所有区域合并forindex, regioninenumerate(merge_regions):try:new_detected_regions[index] except IndexError:new_detected_regions.append([])forr_indexinregion:new_detected_regions[index].extend(detected_regions[r_index]) detected_regions[r_index] = []# 添加剩下的其余皮肤区域到 new_detected_regionsforregionindetected_regions:iflen(region) >0:new_detected_regions.append(region)# 清理 new_detected_regionsself._clear_regions(new_detected_regions)# 添加剩下的其余皮肤区域到 new_detected_regionsforregionindetected_regions:iflen(region) >0:new_detected_regions.append(region)# 清理 new_detected_regionsself._clear_regions(new_detected_regions)
然后可以组织下分析得出的信息
definspect(self): _image ='{} {} {}×{}'.format(self.image.filename,self.image.format,self.width,self.height)return"{_image}: result={_result} message='{_message}'".format(_image=_image, _result=self.result, _message=self.message)
Nude 类如果就这样完成了,最后运行脚本时只能得到一些真或假的结果,我们需要更直观的感受程序的分析效果,我们可以生成一张原图的副本,不过这个副本图片中只有黑白色,白色代表皮肤区域,那么这样我们能直观感受到程序分析的效果了
if__name__ =="__main__": import argparse parser = argparse.ArgumentParser(description='Detect nudity in images.') parser.add_argument('files', metavar='image', nargs='+', help='Images you wish to test') parser.add_argument('-r','--resize', action='store_true', help='Reduce image size to increase speed of scanning') parser.add_argument('-v','--visualization', action='store_true', help='Generating areas of skin image') args = parser.parse_args()forfnameinargs.files:ifos.path.isfile(fname): n = Nude(fname)ifargs.resize: n.resize(maxheight=800, maxwidth=600) n.parse()ifargs.visualization: n.showSkinRegions() print(n.result, n.inspect())else: print(fname,"is not a file")
2.4. 测试效果
使用 wget 把测试用图片下载下来
$ wgethttp://labfile.oss.aliyuncs.com/courses/589/1.jpg
运行下面的命令执行脚本,注意是 python3 而不是 python
$ python3 nude.py-v1.jpg
现在你可以等待程序结果,结果出来后,你还可以查看 -v 选项生成的效果展示图片
三、实验总结
本次实验熟悉了下 PIL 的使用,了解了色情图片检测的原理,整个实验难点是在皮肤区域的检测与整合这一方面,这方面不是很清楚的同学多多阅读思考,如果有什么疑问或者建议的话可以留言,实验楼会尽力解答你的问题,建议写下实验报告,将自己的思考过程记录下来是很有好处的
本实验还有许多可以改进的地方,比如肤色检测的公式,色情判定条件,还有性能问题,同学可以自己尝试改进,比如性能问题可以尝试多线程或多进程
四、完整代码
代码也可以下载下来
$ wgethttp://labfile.oss.aliyuncs.com/courses/589/nude.py
代码:
import sysimport osimport _iofrom collections import namedtuplefrom PIL import ImageclassNude(object):Skin = namedtuple("Skin","id skin region x y")def__init__(self, path_or_image):# 若 path_or_image 为 Image.Image 类型的实例,直接赋值ifisinstance(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 图iflen(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.imageself.image = new_imgself.image.filename = f# 存储对应图像所有像素的全部 Skin 对象self.skin_map = []# 检测到的皮肤区域,元素的索引即为皮肤区域号,元素都是包含一些 Skin 对象的列表self.detected_regions = []# 元素都是包含一些 int 对象(区域号)的列表# 这些元素中的区域号代表的区域都是待合并的区域self.merge_regions = []# 整合后的皮肤区域,元素的索引即为皮肤区域号,元素都是包含一些 Skin 对象的列表self.skin_regions = []# 最近合并的两个皮肤区域的区域号,初始化为 -1self.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.heightdefresize(self, maxwidth=1000, maxheight=1000):"""
基于最大宽高按比例重设图片大小,
注意:这可能影响检测算法的结果
如果没有变化返回 0
原宽度大于 maxwidth 返回 1
原高度大于 maxheight 返回 2
原宽高大于 maxwidth, maxheight 返回 3
maxwidth - 图片最大宽度
maxheight - 图片最大高度
传递参数时都可以设置为 False 来忽略
"""# 存储返回值ret =0ifmaxwidth:ifself.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 = fnameself.width,self.height =self.image.sizeself.total_pixels =self.width *self.height ret +=1ifmaxheight:ifself.height >maxheight:hpercent = (maxheight / float(self.height)) wsize = int((float(self.width) * float(hpercent))) fname =self.image.filenameself.image =self.image.resize((wsize, maxheight), Image.LANCZOS)self.image.filename = fnameself.width,self.height =self.image.sizeself.total_pixels =self.width *self.height ret +=2returnret# 分析函数defparse(self):# 如果已有结果,返回本对象ifself.result isnotNone:returnself# 获得图片所有像素数据pixels =self.image.load()# 遍历每个像素foryinrange(self.height):forxinrange(self.width):# 得到像素的 RGB 三个通道的值# [x, y] 是 [(x,y)] 的简便写法r = pixels[x, y][0]# redg = pixels[x, y][1]# greenb = pixels[x, y][2]# blue# 判断当前像素是否为肤色像素isSkin = Trueifself._classify_skin(r, g, b)elseFalse# 给每个像素分配唯一 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))# 若当前像素不为肤色像素,跳过此次循环ifnotisSkin:continue# 设左上角为原点,相邻像素为符号 *,当前像素为符号 ^,那么相互位置关系通常如下图# ***# *^# 存有相邻像素索引的列表,存放顺序为由大到小,顺序改变有影响# 注意 _id 是从 1 开始的,对应的索引则是 _id-1check_indexes = [_id -2,# 当前像素左方的像素_id -self.width -2,# 当前像素左上方的像素_id -self.width -1,# 当前像素的上方的像素_id -self.width]# 当前像素右上方的像素# 用来记录相邻像素中肤色像素所在的区域号,初始化为 -1region = -1# 遍历每一个相邻像素的索引forindexincheck_indexes:# 尝试索引相邻像素的 Skin 对象,没有则跳出循环try:self.skin_map[index] exceptIndexError:break# 相邻像素若为肤色像素:ifself.skin_map[index].skin:# 若相邻像素与当前像素的 region 均为有效值,且二者不同,且尚未添加相同的合并任务if(self.skin_map[index].region != Noneandregion != Noneandregion != -1andself.skin_map[index].region != regionandself.last_from != regionandself.last_to !=self.skin_map[index].region) :# 那么这添加这两个区域的合并任务self._add_merge(region,self.skin_map[index].region)# 记录此相邻像素所在的区域号region =self.skin_map[index].region# 遍历完所有相邻像素后,若 region 仍等于 -1,说明所有相邻像素都不是肤色像素ifregion == -1:# 更改属性为新的区域号,注意元祖是不可变类型,不能直接更改属性_skin =self.skin_map[_id -1]._replace(region=len(self.detected_regions))self.skin_map[_id -1] = _skin# 将此肤色像素所在区域创建为新区域self.detected_regions.append([self.skin_map[_id -1]])# region 不等于 -1 的同时不等于 None,说明有区域号为有效值的相邻肤色像素elif region !=None:# 将此像素的区域号更改为与相邻像素相同_skin =self.skin_map[_id -1]._replace(region=region)self.skin_map[_id -1] = _skin# 向这个区域的像素列表中添加此像素self.detected_regions[region].append(self.skin_map[_id -1])# 完成所有区域合并任务,合并整理后的区域存储到 self.skin_regionsself._merge(self.detected_regions,self.merge_regions)# 分析皮肤区域,得到判定结果self._analyse_regions()returnself# self.merge_regions 的元素都是包含一些 int 对象(区域号)的列表# self.merge_regions 的元素中的区域号代表的区域都是待合并的区域# 这个方法便是将两个待合并的区域号添加到 self.merge_regions 中def_add_merge(self, _from, _to):# 两个区域号赋值给类属性self.last_from = _fromself.last_to = _to# 记录 self.merge_regions 的某个索引值,初始化为 -1from_index = -1# 记录 self.merge_regions 的某个索引值,初始化为 -1to_index = -1# 遍历每个 self.merge_regions 的元素forindex, regioninenumerate(self.merge_regions):# 遍历元素中的每个区域号forr_indexinregion:ifr_index ==_from:from_index = indexifr_index ==_to:to_index = index# 若两个区域号都存在于 self.merge_regions 中iffrom_index != -1andto_index != -1:# 如果这两个区域号分别存在于两个列表中# 那么合并这两个列表iffrom_index !=to_index:self.merge_regions[from_index].extend(self.merge_regions[to_index]) del(self.merge_regions[to_index])return# 若两个区域号都不存在于 self.merge_regions 中iffrom_index == -1andto_index == -1:# 创建新的区域号列表self.merge_regions.append([_from, _to])return# 若两个区域号中有一个存在于 self.merge_regions 中iffrom_index != -1andto_index == -1:# 将不存在于 self.merge_regions 中的那个区域号# 添加到另一个区域号所在的列表self.merge_regions[from_index].append(_to)return# 若两个待合并的区域号中有一个存在于 self.merge_regions 中iffrom_index == -1andto_index != -1:# 将不存在于 self.merge_regions 中的那个区域号# 添加到另一个区域号所在的列表self.merge_regions[to_index].append(_from)return# 合并该合并的皮肤区域def_merge(self, detected_regions, merge_regions):# 新建列表 new_detected_regions # 其元素将是包含一些代表像素的 Skin 对象的列表# new_detected_regions 的元素即代表皮肤区域,元素索引为区域号new_detected_regions = []# 将 merge_regions 中的元素中的区域号代表的所有区域合并forindex, regioninenumerate(merge_regions):try:new_detected_regions[index] exceptIndexError:new_detected_regions.append([])forr_indexinregion:new_detected_regions[index].extend(detected_regions[r_index]) detected_regions[r_index] = []# 添加剩下的其余皮肤区域到 new_detected_regionsforregionindetected_regions:iflen(region) >0: new_detected_regions.append(region)# 清理 new_detected_regionsself._clear_regions(new_detected_regions)# 皮肤区域清理函数# 只保存像素数大于指定数量的皮肤区域def_clear_regions(self, detected_regions):forregionindetected_regions:iflen(region) >30:self.skin_regions.append(region)# 分析区域def_analyse_regions(self):# 如果皮肤区域小于 3 个,不是色情iflen(self.skin_regions) <3:self.message ="Less than 3 skin regions ({_skin_regions_size})".format( _skin_regions_size=len(self.skin_regions))self.result = Falsereturnself.result# 为皮肤区域排序self.skin_regions = sorted(self.skin_regions, key=lambdas:len(s), reverse=True)# 计算皮肤总像素数total_skin = float(sum([len(skin_region)forskin_regioninself.skin_regions]))# 如果皮肤区域与整个图像的比值小于 15%,那么不是色情图片iftotal_skin /self.total_pixels *100<15:self.message ="Total skin percentage lower than 15 ({:.2f})".format(total_skin /self.total_pixels *100)self.result = Falsereturnself.result# 如果最大皮肤区域小于总皮肤面积的 45%,不是色情图片iflen(self.skin_regions[0]) / total_skin *100<45:self.message ="The biggest region contains less than 45 ({:.2f})".format(len(self.skin_regions[0]) / total_skin *100)self.result = Falsereturnself.result# 皮肤区域数量超过 60个,不是色情图片iflen(self.skin_regions) >60:self.message ="More than 60 skin regions ({})".format(len(self.skin_regions))self.result = Falsereturnself.result# 其它情况为色情图片self.message ="Nude!!"self.result = Truereturnself.result# 基于像素的肤色检测技术def_classify_skin(self, r, g, b):# 根据RGB值判定rgb_classifier = r >95and\ g >40andg <100and\ b >20and\ max([r, g, b]) - min([r, g, b]) >15and\ abs(r - g) >15and\ r > gand\ r > b# 根据处理后的 RGB 值判定nr, ng, nb =self._to_normalized(r, g, b) norm_rgb_classifier = nr / ng >1.185and\ float(r * b) / ((r + g + b) **2) >0.107and\ float(r * g) / ((r + g + b) **2) >0.112# HSV 颜色模式下的判定h, s, v =self._to_hsv(r, g, b) hsv_classifier = h >0and\ h <35and\ s >0.23and\ s <0.68# YCbCr 颜色模式下的判定y, cb, cr =self._to_ycbcr(r, g, b) ycbcr_classifier =97.5<= cb <=142.5and134<= cr <=176# 效果不是很好,还需改公式# return rgb_classifier or norm_rgb_classifier or hsv_classifier or ycbcr_classifierreturnycbcr_classifierdef_to_normalized(self, r, g, b):ifr ==0: r =0.0001ifg ==0: g =0.0001ifb ==0: b =0.0001_sum = float(r + g + b)return[r / _sum, g / _sum, b / _sum]def_to_ycbcr(self, r, g, b):# 公式来源:# http://stackoverflow.com/questions/19459831/rgb-to-ycbcr-conversion-problemsy = .299*r + .587*g + .114*b cb =128-0.168736*r -0.331364*g +0.5*b cr =128+0.5*r -0.418688*g -0.081312*breturny, cb, crdef_to_hsv(self, r, g, b): h =0_sum = float(r + g + b) _max = float(max([r, g, b])) _min = float(min([r, g, b])) diff = float(_max - _min)if_sum ==0: _sum =0.0001if_max ==r:ifdiff ==0: h = sys.maxsizeelse:h = (g - b) / diff elif _max ==g:h =2+ ((g - r) / diff)else:h =4+ ((r - g) / diff) h *=60ifh <0: h +=360return[h,1.0- (3.0* (_min / _sum)), (1.0/3.0) * _max]definspect(self): _image ='{} {} {}×{}'.format(self.image.filename,self.image.format,self.width,self.height)return"{_image}: result={_result} message='{_message}'".format(_image=_image, _result=self.result, _message=self.message)# 将在源文件目录生成图片文件,将皮肤区域可视化defshowSkinRegions(self):# 未得出结果时方法返回ifself.result isNone:return# 皮肤像素的 ID 的集合skinIdSet = set()# 将原图做一份拷贝simage =self.image# 加载数据simageData = simage.load()# 将皮肤像素的 id 存入 skinIdSetforsrinself.skin_regions:forpixelinsr:skinIdSet.add(pixel.id)# 将图像中的皮肤像素设为白色,其余设为黑色forpixelinself.skin_map:ifpixel.idnotinskinIdSet:simageData[pixel.x, pixel.y] =0,0,0else:simageData[pixel.x, pixel.y] =255,255,255# 源文件绝对路径filePath = os.path.abspath(self.image.filename)# 源文件所在目录fileDirectory = os.path.dirname(filePath) +'/'# 源文件的完整文件名fileFullName = os.path.basename(filePath)# 分离源文件的完整文件名得到文件名和扩展名fileName, fileExtName = os.path.splitext(fileFullName)# 保存图片simage.save('{}{}_{}{}'.format(fileDirectory, fileName,'Nude'ifself.resultelse'Normal', fileExtName))if__name__=="__main__": import argparse parser = argparse.ArgumentParser(description='Detect nudity in images.') parser.add_argument('files', metavar='image', nargs='+', help='Images you wish to test') parser.add_argument('-r','--resize', action='store_true', help='Reduce image size to increase speed of scanning') parser.add_argument('-v','--visualization', action='store_true', help='Generating areas of skin image') args = parser.parse_args()forfnameinargs.files:ifos.path.isfile(fname): n = Nude(fname)ifargs.resize:n.resize(maxheight=800, maxwidth=600) n.parse()ifargs.visualization:n.showSkinRegions() print(n.result, n.inspect())else:print(fname,"is not a file")
私信小编01即可获取源码!
网友评论