本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