肤块识别Demo_Python实现【详】

本Demo旨在利用Python识别图片中的肤块区域,主要思想简述如下:

一、遍历像素点,判断是否为肤色;
1.若非肤色,则遍历下个像素点;
 若为肤色,则进行所处肤块的判断合并处理
2.肤色像素点所处肤块判断:
 若该像素点周围存在相邻肤色像素元素,则将该点并入到相邻肤色元素所属的肤块区域中(像素点并入对应肤块区);
 若该点周围不存在相邻肤色元素,说明新的肤块区域产生,则创建一个新的肤块区域(新建对应肤块区域);

二、遍历结束,对各个肤块区域进行整合处理:
1.对可以合并的肤块区域进行再次合并;
2.将肤块区域及所含像素点进行统计,得出统计结果;
3.将肤块区域所含像素点设置为黑色,其余像素点设为白色,直观显示用户肤块分布情况;

主体思想阐述结束,下面进行具体实现,设计为SkinRecognition类。

SkinRecognition类主体框架:

class SkinRecognition(object):
    #定义Skin类型: 记录各个像素点的信息,包含的字段:id skin region x y
    #其中skin:像素点是否为肤色;region:像素点所属肤块区
    Skin = namedtuple("Skin","id skin region x y")

    #初始化函数,参数path_image为待分析图片或其路径,函数内定义类的各属性信息
    def __init__(self,path_image)

    #图片缩小函数:按比例缩小过大图片,减少图片分析代价
    def reduceSize(self,maxwidth=1000,maxheight=1000)

    #【重要】分析函数:
    #遍历每个像素,依次各像素点创建对应Skin对象
    def parsePixels(self)

    #合并两区域到同一肤块区域中:
    def mergeRegion(self,_from,_to)

    #遍历结束后,整合部分未合并相邻肤块区域:
    def merge(self)

    #肤块区域清理函数:剔除元素数量未达到标准的肤块区域列表
    def clearRegion(self,detectedRegions)

    #肤块区域统计分析:
    def analyseRegion(self)

    #肤色判别函数:基于像素
    def detectSkin(self,r,g,b)

    def normalize(self,r,g,b)
    def toYCbCr(self,r,g,b)
    def toHSV(self,r,g,b)
    def summary(self)

    #生成肤色识别结果图片:
    def showSkin(self)


Tip1:
 namedtuple(): 参数传递类型名和所需字段,返回标准元组类型子类
Tip2:
 isInstance(object,classInfo): 若object是参数classInfo的实例,返回真,否则返回假


类的初始化函数:

class SkinRecognition(object):

     def __init__(self, path_image):
         # 若 path_image 为 Image.Image 类的实例,直接赋值给变量image
         if isinstance(path_image, Image.Image):
             self.image = path_image
         # 若path_image为str型的实例,则为路径,打开路径对应图片
         elif isinstance(path_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
             # 新RGB图替换原灰度图self.image
             self.image = new_img
             self.image.filename = f

         # 存储对应图像所有像素的全部 Skin 对象(id skin region x y)
         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



Tip3:Image.resize(size,resample=0):
   size包含宽高像素的元组(width,height), resample可选的重采样滤波器;
   返回新的Image对象


图片缩放函数:

  #越大的图片消耗的资源越多,所以需要缩小图片大小,定义缩小函数
   def resize(self, maxwidth=1000, maxheight=1000):

       """
       基于最大宽高按比例重设图片大小,但可能影响识别算法最终结果
       参数:maxwidth,图片最大宽度;maxheight, 图片最大高度;
            可以设置为 False 来忽略

       如果图片无需缩放,则返回 0
       原宽度大于 maxwidth,则返回 1
       原高度大于 maxheight,则返回 2
       原宽高大于 maxwidth, maxheight,则返回 3
       """
       # 存储返回值
       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



Tip4:相邻元素的定义:
   设左上角为原点,相邻像素为符号 !,当前像素为符号 ^,那么相互位置关系通常如下图:
    !!!
    !!^
Tip5:像素点对应Skin对象_id 是从 1 开始的,而像素点索引则是从0开始的,表示为 _id-1

TIP:
列表的基本操作,见…

像素点解析函数:为每个像素点创建对应的Skin对象

    # 重点!!!【解析函数】
    # 遍历每个像素点,为每个像素创建对应Skin(id skin region x y)对象
    def parse(self):
        # 如果已有结果,返回本对象
        if self.result is not None:
            return self
        # 获得图片所有像素数据
        pixels = self.image.load()
        # 遍历每个像素
        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
                # 判断当前像素是否为肤色像素,结果作为相应Skin对象的skin字段值
                isSkin = True if self.detectSkin(r, g, b) else False
                # 给每个像素分配唯一 id 值(1, 2, 3...height*width), id从1开始
                # 注意 x, y 的值从零开始,所以需要将像素索引值加1
                _id = x + y * self.width + 1
                # 为每个像素创建一个对应的 Skin 对象(id skin region x y),并添加到 self.skin_map 中
                self.skin_map.append(self.Skin(_id, isSkin, None, x, y))
                # 若当前像素不为肤色像素,跳过此次循环
                if not isSkin:
                    continue

                # 存有该像素点相邻像素索引的列表,存放顺序固定
                # 该像素点id=_id,对应索引则是 _id-1
                check_indexes = [_id - 2, # 当前像素左方的像素
                                 _id - self.width,  # 当前像素右上方的像素
                                 _id - self.width - 1,  # 当前像素的上方的像素
                                 _id - self.width - 2]  # 当前像素左上方的像素
                # 用来记录相邻像素中肤色像素所在的区域号,初始化为 -1
                region = -1
                # 遍历每一个相邻像素的索引
                for index in check_indexes:
                    # 尝试索引相邻像素的 Skin 对象,没有则跳出循环
                    try:
                        self.skin_map[index]
                    except IndexError:
                        #检测图像里,部分前面的元素没有全部四个相邻元素
                        break
                    # 相邻像素若为肤色像素:
                    if self.skin_map[index].skin:
                        # 若相邻像素与当前像素的 region 均为有效值,且二者不同,且尚未添加相同的合并任务
                        if (self.skin_map[index].region != None and
                                region != None and region != -1 and
                                self.skin_map[index].region != region and
                                self.last_from != region and
                                self.last_to != self.skin_map[index].region) :
                            # 那么添加这两个区域的合并任务
                            #   添加到 self.merge_regions元素中:
                            #          它的每个元素是个列表,这些列表中存放了一个或多个区域号,这些元素是连通的,需要合并:该像素所属肤块区域 合并到 该像素相邻像素所属肤块区域
                            self.mergeRegion(region, self.skin_map[index].region)
                        # 记录此相邻像素所在的区域号
                        region = self.skin_map[index].region
                # 遍历完所有相邻像素后,若 region 仍等于 -1,说明所有相邻像素都不是肤色像素
                if region == -1:
                    # 更改region属性为新的区域号,注意元组是不可变类型,不能直接更改属性
                    # somenamedtuple._replace(kwargs) 返回一个新的替换指定字段值的namedtuple实例
                    _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])
        # 遍历所有像素结束后,图片皮肤区域的划分初步完成
        # merge_regions中元素为一些连通的皮肤区域(这些连通肤块区是相邻的肤块区域索引构成的列表),
        # 利用self.merge()继续合并这些索引代表的区域,合并整理后的区域存储到 self.skin_regions
        self.merge(self.detected_regions, self.merge_regions)
        # 分析皮肤区域,得到判定结果
        self.analyseRegions()
        return self



合并连通的肤块区域

    # self.merge_regions 的元素为一个个连通肤块区域,其元素是由肤块区域的区域号构成的列表,这些肤块区域是相邻的
    # 这个方法便是将两个待合并的相邻的肤块区域号添加到 self.merge_regions 中
    def mergeRegion(self, _from, _to):
        # 两个区域号赋值给类属性
        self.last_from = _from
        self.last_to = _to

        # 记录 self.merge_regions 的某个索引值,初始化为 -1
        from_index = -1
        # 记录 self.merge_regions 的某个索引值,初始化为 -1
        to_index = -1


        # 遍历每个 self.merge_regions 的元素
        for index, region in enumerate(self.merge_regions):
            # 遍历连通肤块区域元素中的每个区域号
            # 判断待合并俩肤块区域的区域号是否已经存在于连通肤块的列表中,若属于则记录所在连通肤块列表的索引
            for r_index in region:
                if r_index == _from:
                    from_index = index
                if r_index == _to:
                    to_index = index

        # 若两个区域号都存在于 self.merge_regions 中
        if from_index != -1 and to_index != -1:
            # 如果这两个区域号分别存在于两个列表中(所属不同的小连通肤块区域)
            # 那么合并这两个列表(继续合并俩小连通肤块,构成新的大连通肤块)
            if from_index != to_index:
                self.merge_regions[from_index].extend(self.merge_regions[to_index])
                del(self.merge_regions[to_index])
            return

        # 若两个区域号都不存在于 self.merge_regions 中(两个肤块区域都不属于连通区域)
        if from_index == -1 and to_index == -1:
            # 创建新的区域号列表(两个肤块区域构成新的最小连通肤块区域)
            self.merge_regions.append([_from, _to])
            return
        # 若两个待合并区域号中有一个存在于 self.merge_regions 中
        #(两个肤块区域中,有一个已经属于连通肤块区域,一个是最小肤块区域只有一个像素)
        if from_index != -1 and to_index == -1:
            # 将不存在于 self.merge_regions 中的那个区域号(只有一个像素的肤块区域)
            # 添加到另一个区域号所在的列表(另一个肤块区域所属的连通肤块区域中)
            self.merge_regions[from_index].append(_to)
            return
        # 若两个待合并的区域号中有一个存在于 self.merge_regions 中
        if from_index == -1 and to_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 中的连通肤块区域里的各个相邻的肤块区域的区域号,
        # 这些区域号代表的肤块区域region中的元素:Skin对象,
        # 放入一个新的肤块区域列表,索引为连通肤块索引index
        for index, region in enumerate(merge_regions):
            try:
                new_detected_regions[index]
            except IndexError:
                new_detected_regions.append([])
            for r_index in region:
                new_detected_regions[index].extend(detected_regions[r_index])
                detected_regions[r_index] = []  #合并后的肤块区域清空

        # 添加剩下无需合并的其余皮肤区域到 new_detected_regions
        for region in detected_regions:
            if len(region) > 0:
                new_detected_regions.append(region)

        # 清理 new_detected_regions
        self.clearRegions(new_detected_regions)



皮肤区域清理函数

    # 只保存像素数大于指定数量(eg:30)的皮肤区域
    def clearRegions(self, detected_regions):
        for region in detected_regions:
            if len(region) > 30:
                self.skin_regions.append(region)



肤块统计分析函数

    def analyseRegions(self):
        # 如果皮肤区域块总数量统计
        self.message = "The Number of Pic's Skin Regions :  ({_skin_regions_size})".format(
            _skin_regions_size=len(self.skin_regions))

        # 为皮肤区域排序
        self.skin_regions = sorted(self.skin_regions, key=lambda s: len(s),
                                   reverse=True)

        # 计算皮肤总像素数量
        total_skin = float(sum([len(skin_region) for skin_region in self.skin_regions]))

        # 皮肤总像素与整个图像的比值
        self.message += "Total skin percentage ({:.2f})".format(total_skin / self.total_pixels * 100)

        self.result = True
        return self.result



基于像素的肤色检测

# 肤色检测技术
    def detectSkin(self, r, g, b):
        # 根据RGB值判定
        rgb_classifier = 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
        # 根据处理后的 RGB 值判定
        nr, ng, nb = self.normalize(r, g, b)
        norm_rgb_classifier = nr / ng > 1.185 and \
            float(r * b) / ((r + g + b) ** 2) > 0.107 and \
            float(r * g) / ((r + g + b) ** 2) > 0.112

        # HSV 颜色模式下的判定
        h, s, v = self.toHsv(r, g, b)
        hsv_classifier = h > 0 and \
            h < 35 and \
            s > 0.23 and \
            s < 0.68

        # YCbCr 颜色模式下的判定
        y, cb, cr = self.toYCbCr(r, g,  b)
        ycbcr_classifier = 97.5 <= cb <= 142.5 and 134 <= cr <= 176

        # return rgb_classifier or norm_rgb_classifier or hsv_classifier or ycbcr_classifier
        return ycbcr_classifier

    def normalized(self, r, g, b):
        if r == 0:
            r = 0.0001
        if g == 0:
            g = 0.0001
        if b == 0:
            b = 0.0001
        _sum = float(r + g + b)
        return [r / _sum, g / _sum, b / _sum]

    def toYCbCr(self, r, g, b):
        # 公式来源:
        # http://stackoverflow.com/questions/19459831/rgb-to-ycbcr-conversion-problems
        y = .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*b
        return y, cb, cr

    def toHsv(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.0001

        if _max == r:
            if diff == 0:
                h = sys.maxsize
            else:
                h = (g - b) / diff
        elif _max == g:
            h = 2 + ((g - r) / diff)
        else:
            h = 4 + ((r - g) / diff)

        h *= 60
        if h < 0:
            h += 360

        return [h, 1.0 - (3.0 * (_min / _sum)), (1.0 / 3.0) * _max]

    def inspect(self):
        _image = 'FileName--{}  Format--{}  Size--{}×{}  '.format(self.image.filename, self.image.format, self.width, self.height)
        return "{_image}: Detected?: result={_result} Outcome: message='{_message}'".format(_image=_image, _result=self.result, _message=self.message)



生成可视化肤块区域图片函数

# 将在源文件目录生成图片文件,将皮肤区域可视化
    def showSkin(self):
        # 未得出结果时方法返回
        if self.result is None:
            return
        # 皮肤像素的 ID 的集合
        skinIdSet = set()
        # 将原图做一份拷贝
        simage = self.image
        # 加载数据
        simageData = simage.load()

        # 将皮肤像素的 id 存入 skinIdSet
        for sr in self.skin_regions:
            for pixel in sr:
                skinIdSet.add(pixel.id)
        # 将图像中的皮肤像素设为白色,其余设为黑色
        for pixel in self.skin_map:
            if pixel.id not in skinIdSet:
                simageData[pixel.x, pixel.y] = 0, 0, 0
            else:
                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' if self.result else 'Normal', fileExtName))


至此,类SkinRecognition定义完毕


TIP:
argparse用法,见…

实现

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description='Detect Skin Region 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()

    for fname in args.files:
        if os.path.isfile(fname):
            n = Nude(fname)
            if args.resize:
                n.resize(maxheight=800, maxwidth=600)
            n.parse()
            if args.visualization:
                n.showSkinRegions()
            print(n.result, n.inspect())
        else:
            print(fname, "is not a file")

实现命令示例:
python 脚本路径.py -v 图片路径1.jpg 图片路径2.jpg

总结:
pixel -> Skin对象 -> 肤块Region (存相邻肤色像素对应Skin对象)
=》合并相邻肤块区域至同一列表
->连通肤块(存相邻肤块区域号)
=》合并连通肤块区域列表中的各肤块区域号对应肤块区域里的各像素
->新的完全整合的肤块区域
识别划分肤块操作结束

The End

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值