口罩检测:人工自动生成戴口罩数据集、检测人脸是否戴口罩

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


口罩,已经成为某些场合出门的标配 , 面对人员流动带来的疫情传播压力,车站、机场、地铁站等场合都严格检查出入人员体温、口罩佩戴情况等。百度在近日开源了一个口罩人脸检测及分类模型,除了识别人脸外,还可判断出他们是否佩戴口罩,识别及分类准确率分别达到 98% 和 96.5%。

功能:

1,在图像中检测是否佩戴口罩

2,实时视频流中的口罩检测

此数据集包含1376个图像,属于两个类别: 带口罩:690张图片 无口罩:686张图片

为了创建这个数据集,有一个巧妙的解决方案: 1,拍摄人脸的正常图像 2,然后创建一个计算机视觉Python脚本来向它们添加口罩,从而创建一个戴口罩的数据集 3,面部 landmarks允许我们自动推断面部结构的位置(包括:眼睛   眉毛   鼻子   嘴   下颌线)因此我们可以使用它来帮我们构建数据集

注意:如果使用一组图像创建戴了口罩的人的数据集,则这批原来无口罩的图像不能再作为“无口罩”样本使用,否则,模型拟合效果会很差。


 

1,同一张图片不能同时出现在戴口罩和不戴口罩两类样本里 
2,注意目录结构,口罩检测功能的实现,需要数据集构建脚本,训练及测试程序 
3,面部 landmarks可以自动推断面部结构的位置(包括:眼睛   眉毛   鼻子   嘴   下颌线)因此我们可以使用它来帮我们构建数据集
mouth [49, 68].
left eyebrow [18, 22].
right eyebrow  [23, 27].
right eye [37, 42].
left eye  [43, 48].
nose  [28, 36].
jaw [1, 17].

为了训练一个口罩检测器,我们需要将我们的实践分成两个不同的阶段,每个阶段都有各自的子步骤(如右图所示):
训练:在这里,我们将从磁盘加载我们的口罩检测数据集,在此数据集上训练模型(使用Keras/TensorFlow),然后将面罩检测器写入到磁盘
部署:一旦面罩检测仪经过训练,我们就可以继续加载面罩检测模型,执行面部检测,然后将每个面部分为带口罩和不带口罩。

1,同一张图片不能同时出现在戴口罩和不戴口罩两类样本里
2,注意目录结构,口罩检测功能的实现,需要数据集构建脚本,训练及测试程序
3,面部 landmarks可以自动推断面部结构的位置(包括:眼睛   眉毛   鼻子   嘴   下颌线)因此我们可以使用它来帮我们构建数据集
import numpy as np
x = np.array([[0, 3, 4],
              [1, 6, 4]]
             )
# 默认参数ord=None,axis=None,keepdims=False
print("默认参数(矩阵整体元素平方和开根号,不保留矩阵二维特性:", np.linalg.norm(x)) #8.831760866327848
print("矩阵整体元素平方和开根号,保留矩阵二维特性:", np.linalg.norm(x, keepdims=True)) # [[8.83176087]]
print("矩阵每个行向量求向量的2范数:", np.linalg.norm(x, axis=1, keepdims=True)) #[[5.] [7.28010989]]
print("矩阵每个列向量求向量的2范数:", np.linalg.norm(x, axis=0, keepdims=True)) #[[1 6.70820393 5.65685425]]
print("矩阵1范数:", np.linalg.norm(x, ord=1, keepdims=True)) #[[9.]] 即第2列的3+6=9
print("矩阵2范数:", np.linalg.norm(x, ord=2, keepdims=True)) #[[8.70457079]]
print("矩阵∞范数:", np.linalg.norm(x, ord=np.inf, keepdims=True)) #[[11.]] 即第2行的1+6+4=11
print("矩阵每个行向量求向量的1范数:", np.linalg.norm(x, ord=1, axis=1, keepdims=True)) #[[ 7.] [11.]]

"""  
x_norm = np.linalg.norm(x, ord=None, axis=None, keepdims=False) 求范数
    x: 表示矩阵
    ord:范数类型    
        ord=None(默认2范数):矩阵中所有元素值的平方和(先对每个元素自身平方运算后再所有结果值求和),再整体开根号
        ord=1(1范数):每一列中所有列值求总和,获取出最大的某一列列值总和,即哪一列的列值总和是最大的
        ord=2(2范数):矩阵中所有元素值的平方和,再整体开根号
        ord=np.inf(无穷范数):每一行中所有行值求总和,获取出最大的某一行行值总和,即哪一行的行值总和是最大的
    axis:处理类型    
        axis=1表示按行向量处理,求多个行向量的范数
        axis=0表示按列向量处理,求多个列向量的范数
        axis=None表示矩阵范数。
"""
import cv2
import os
from mask import create_mask

folder_path =r"./Downloads"

# folder_path = "/home/preeth/Downloads"
#dist_path = "/home/preeth/Downloads"
#c = 0

images = [os.path.join(folder_path, f)
          for f in os.listdir(folder_path)
            #判断是否是文件
            if os.path.isfile(os.path.join(folder_path, f))]
for i in range(len(images)):
    print("the path of the image is", images[i])
    #image = cv2.imread(images[i])
    #c = c + 1
    create_mask(images[i]) 
import os
import sys
import random
import argparse
import numpy as np
from PIL import Image, ImageFile
import face_recognition

#口罩图片文件
BLUE_IMAGE_PATH = r"./images/blue-mask.png"

def create_mask(image_path):
    #人脸图片路径
    pic_path = image_path
    #口罩图片路径
    mask_path = BLUE_IMAGE_PATH
    # mask_path = "/media/preeth/Data/prajna_files/mask_creator/face_mask/images/blue-mask.png"
    show = False
    model = "hog"
    FaceMasker(pic_path, mask_path, show, model).mask()

class FaceMasker:
    #鼻梁nose_bridge,脸颊chin 的特征点
    KEY_FACIAL_FEATURES = ('nose_bridge', 'chin')

    def __init__(self, face_path, mask_path, show=False, model='hog'):
        self.face_path = face_path
        self.mask_path = mask_path
        self.show = show
        self.model = model
    #     self._face_img: ImageFile = None
    #     self._mask_img: ImageFile = None

    def mask(self):
        """
        图像载入函数 load_image_file load_image_file(file, mode='RGB')
            加载一个图像文件到一个numpy array类型的对象上。
            参数:
                file:待加载的图像文件名字
                mode:转换图像的格式 只支持“RGB”(8位RGB, 3通道)和“L”(黑白)
            返回值: 一个包含图像数据的numpy array类型的对象
        """
        face_image_np = face_recognition.load_image_file(self.face_path)
        """
        face_locations(img, number_of_times_to_upsample=1, model='hog') 
        给定一个图像,返回图像中每个人脸的面部特征位置(眼睛、鼻子等),也即是获取每个人脸所在的边界框/人脸的位置(top, right, bottom, left)。
        参数: 
            img:一个image(numpy array类型)
            number_of_times_to_upsample:从images的样本中查找多少次人脸,该参数值越高的话越能发现更小的人脸。 
            model:使用哪种人脸检测模型。“hog” 准确率不高,但是在CPUs上运行更快,
                   “cnn” 更准确更深度(且 GPU/CUDA加速,如果有GPU支持的话),默认是“hog” 
            返回值: 一个元组列表,列表中的每个元组包含人脸的位置(top, right, bottom, left)
        """
        face_locations = face_recognition.face_locations(face_image_np, model=self.model)
        """
        人脸特征提取函数 face_landmarks(face_image, face_locations=None,model="large") 
            给定一个图像,提取图像中每个人脸的脸部特征位置。
            人脸特征提取函数face_landmarks 提取后的脸部特征包括:
                鼻梁nose_bridge、鼻尖nose_tip、 下巴chin、左眼left_eye、右眼right_eye、左眉 left_eyebrow、
                右眉right_eyebrow、上唇top_lip、下 唇bottom_lip
            参数: 
                face_image:输入的人脸图片 
                face_locations=None: 
                    可选参数,默认值为None,代表默认解码图片中的每一个人脸。 
                    若输入face_locations()[i]可指定人脸进行解码 
                model="large"/"small":
                    输出的特征模型,默认为“large”,可选“small”。 
                    当选择为"small"时,只提取左眼、右眼、鼻尖这三种脸部特征。
        """
        face_landmarks = face_recognition.face_landmarks(face_image_np, face_locations)
        #实现array到image的转换,返回image
        self._face_img = Image.fromarray(face_image_np)#人脸图片
        self._mask_img = Image.open(self.mask_path) #口罩图片

        found_face = False #是否找到脸的标志符
        #遍历检测到的每个人脸所属的整个脸部特征
        for face_landmark in face_landmarks:
            #   {'chin': [ (1108, 361)], 'left_eyebrow':[...], 'right_eyebrow': [...],
            #   'nose_bridge': [...], 'nose_tip': [...], 'left_eye': [...], 'right_eye': [...],
            #   'top_lip': [...], 'bottom_lip':[...] }
            # print(face_landmark)
            # print(type(face_landmark)) #<class 'dict'>

            #鼻梁nose_bridge、脸颊chin 是否在 所检测出来的人脸特征字典中的标志
            skip = False
            #遍历 鼻梁nose_bridge、脸颊chin 的特征点
            for facial_feature in self.KEY_FACIAL_FEATURES:
                #判断 鼻梁nose_bridge、脸颊chin 是否在 所检测出来的人脸特征字典中
                if facial_feature not in face_landmark:
                    # 鼻梁nose_bridge、脸颊chin 不在 所检测出来的人脸特征字典中,则认为跳过不处理当前该检测出来的人脸特征
                    skip = True
                    break
            if skip:
                # 鼻梁nose_bridge、脸颊chin 不在 所检测出来的人脸特征字典中,则认为跳过不处理当前该检测出来的人脸特征
                continue

            # 找到脸
            found_face = True
            #传入当前所检测出来的人脸的整个脸部特征字典数据,进行对人脸中的口部位置贴上口罩
            self._mask_face(face_landmark)

        if found_face:
            if self.show:
                self._face_img.show()
            # save
            self._save()
        else:
            print('Found no face.')

    """
    鼻梁nose_bridge中的第2个特征点到脸颊chin中的第9个特征点(即下巴)构成一条竖直线;
    计算左脸颊chin中的第1个特征点到竖直线的距离乘以width_ratio作为左脸中口罩的宽,竖直线两端点坐标之间的距离计算2范数的值作为左脸中口罩的高;
    计算右脸颊chin中的第17个特征点到竖直线的距离乘以width_ratio作为右脸中口罩的宽,竖直线两端点坐标之间的距离计算2范数的值作为右脸中口罩的高;
    """
    # 传入当前所检测出来的人脸的整个脸部特征字典数据,进行对人脸中的口部位置贴上口罩
    def _mask_face(self, face_landmark: dict):
        """
        获取鼻梁nose_bridge中的第2个特征点,即获取整个人脸68个特征点中的第29个特征点。
        获取脸颊chin中的第1个特征点,即获取整个人脸68个特征点中的第1个特征点。
        获取脸颊chin中的第9个特征点,即获取整个人脸68个特征点中的第9个特征点。
        获取脸颊chin中的第17个特征点,即获取整个人脸68个特征点中的第17个特征点。

        整个人脸68个特征点中 第29个特征点 和 第1个特征点 和 第17个特征点 三个点一起构成了一条线段(直线)
        """
        #获取字典中鼻梁nose_bridge的特征数据:'nose_bridge': [...]。鼻梁有4个特征点。
        nose_bridge = face_landmark['nose_bridge']
        # print("nose_bridge",nose_bridge) # 比如 [(118, 63), (118, 75), (117, 88), (117, 101)]
        # 因为鼻梁有4个特征点,因此'nose_bridge': [...] 中的列表有4个元祖的特征值
        # nose_bridge[4 * 1 // 4] 即等于 nose_bridge[1] 取第2个元祖的特征值,
        # 也即是取出鼻梁nose_bridge中的第2个特征点的值,该特征点位于整个人脸68个特征点中的第29个
        nose_point = nose_bridge[len(nose_bridge) * 1 // 4]
        # print("nose_point",nose_point) # 比如 (118, 75)
        nose_v = np.array(nose_point) #把元祖封装的类型 转换为 array类型
        # print("nose_v", nose_v) # 比如 [118 75]

        #获取字典中脸颊chin的特征数据:'chin': [...]。脸颊chin有17个特征点。
        chin = face_landmark['chin']
        # chin_len = len(chin)
        # print(chin_len) #17
        #获取 脸颊chin中的 第9个特征点的值,该特征点位于整个人脸68个特征点中的第9个,即下巴底部位置
        chin_bottom_point = chin[8]
        # print("chin_bottom_point",chin_bottom_point) # 比如 (155, 341)
        # chin_bottom_point = chin[chin_len // 2]
        chin_bottom_v = np.array(chin_bottom_point) #把元祖封装的类型 转换为 array类型
        # print("chin_bottom_v",chin_bottom_v) # 比如 [155 341]
        # chin_left_point = chin[chin_len // 8]
        # chin_right_point = chin[chin_len * 7 // 8]
        # 获取 脸颊chin中的 第1个特征点的值,该特征点位于整个人脸68个特征点中的第1个
        chin_left_point = chin[0]
        # 获取 脸颊chin中的 第17个特征点的值,该特征点位于整个人脸68个特征点中的第17个
        chin_right_point = chin[16]

        # 分割口罩图片并调整大小
        width = self._mask_img.width  #口罩图片的width
        height = self._mask_img.height #口罩图片的height
        width_ratio = 1.2 #宽度比

        """
        1.nose_v - chin_bottom_v
            nose_v:鼻梁nose_bridge中的第2个特征点的值,该特征点位于整个人脸68个特征点中的第29个。
            chin_bottom_v:脸颊chin中的 第9个特征点的值,该特征点位于整个人脸68个特征点中的第9个,即下巴底部位置。
            nose_v - chin_bottom_v:(鼻梁)第29个特征点的值  - (下巴)第9个特征点的值
        2.x_norm = np.linalg.norm(x, ord=None, axis=None, keepdims=False) 求范数
            x: 表示矩阵
            ord:范数类型    
                ord=None(默认2范数):矩阵中所有元素值的平方和(先对每个元素自身平方运算后再所有结果值求和),再整体开根号
                ord=1(1范数):每一列中所有列值求总和,获取出最大的某一列列值总和,即哪一列的列值总和是最大的
                ord=2(2范数):矩阵中所有元素值的平方和,再整体开根号
                ord=np.inf(无穷范数):每一行中所有行值求总和,获取出最大的某一行行值总和,即哪一行的行值总和是最大的
            axis:处理类型    
                axis=1表示按行向量处理,求多个行向量的范数
                axis=0表示按列向量处理,求多个列向量的范数
                axis=None表示矩阵范数。
        3.比如 nose_v - chin_bottom_v 的值为 [6 -147] 
          np.linalg.norm(nose_v - chin_bottom_v)
                ord=None(默认2范数):矩阵中所有元素值的平方和(先对每个元素自身平方运算后再所有结果值求和),再整体开根号
                因此 sqrt(6^2 + (-147)^2) = 147.1223980228707
        4.最终把 147.1223980228707 作为 切割出来的左边一半的口罩图片的高
        """
        # print("nose_v - chin_bottom_v",nose_v - chin_bottom_v) #比如 [6 -147] 147.1223980228707
        # print("int(np.linalg.norm(nose_v - chin_bottom_v))",int(np.linalg.norm(nose_v - chin_bottom_v))) #比如 147
        # 最终把 147.1223980228707 作为 切割出来的左边一半的口罩图片的高
        new_height = int(np.linalg.norm(nose_v - chin_bottom_v))

        # 计算左半边脸的口罩
        # 切割出口罩图片中左边的一半:(234 // 2, 146) 得到 (117, 146)
        # (0, 0, width // 2, height):(0, 0) 代表 左上角的x/y,(width // 2, height) 代表 右下角的x/y
        mask_left_img = self._mask_img.crop((0, 0, width // 2, height))
        # print("_mask_img.size",self._mask_img.size) #(234, 146)
        # print("mask_left_img.size",mask_left_img.size) #(117, 146)

        """
        第29个特征点(x1,y1) 到 第9个特征点(x2,y2) 构成一条直线,计算 第1个特征点(x,y) 到 这条直线 的距离 作为 切割出来的左边一半的口罩图片的宽。
            第1个特征点(x,y):chin_left_point,即脸颊chin中的 第1个特征点的值,该特征点位于整个人脸68个特征点中的第1个
            第9个特征点(x2,y2):chin_bottom_point,即脸颊chin中的 第9个特征点的值,该特征点位于整个人脸68个特征点中的第9个,即下巴底部位置
            第29个特征点(x1,y1):nose_point,即鼻梁nose_bridge中的第2个特征点的值,该特征点位于整个人脸68个特征点中的第29个
        """
        # 将 第1个特征点(x,y) 到 这条直线 的距离 作为 切割出来的左边一半的口罩图片的宽。
        mask_left_width = self.get_distance_from_point_to_line(chin_left_point, nose_point, chin_bottom_point)

        #mask_left_width(第1个特征点(x,y) 到 这条直线 的距离) * width_ratio(宽度比1.2)
        mask_left_width = int(mask_left_width * width_ratio)
        # print("mask_left_width",mask_left_width) #比如 156

        # 将切割出来的左边一半的口罩图片 从 (117, 146) resize为 举例的 (156, 147)
        # 也即是把口罩左半边宽和高 按比例缩放到 左脸面颊的宽 和 鼻梁到下巴的高,将口罩左半边按比例缩放进行适配 左脸面颊的宽 和 鼻梁到下巴的高,
        # 为的就是能在人脸左脸为侧脸的时候仍能把口罩左半边按比例缩放进行适配给为侧脸的左脸。
        mask_left_img = mask_left_img.resize((mask_left_width, new_height))

        #  切割出口罩图片中右边的一半:(234 // 2, 146) 得到 (117, 146)
        # (width // 2, 0, width, height):(width // 2, 0) 代表 左上角的x/y,(width, height) 代表 右下角的x/y
        mask_right_img = self._mask_img.crop((width // 2, 0, width, height))
        # print("mask_right_img.size",mask_right_img.size) #(117, 146)

        """
        第29个特征点(x1,y1) 到 第9个特征点(x2,y2) 构成一条直线,计算 第17个特征点(x,y) 到 这条直线 的距离 作为 切割出来的右边一半的口罩图片的宽。
            第17个特征点(x,y):chin_right_point,即脸颊chin中的 第17个特征点的值,该特征点位于整个人脸68个特征点中的第17个
            第9个特征点(x2,y2):chin_bottom_point,即脸颊chin中的 第9个特征点的值,该特征点位于整个人脸68个特征点中的第9个,即下巴底部位置
            第29个特征点(x1,y1):nose_point,即鼻梁nose_bridge中的第2个特征点的值,该特征点位于整个人脸68个特征点中的第29个
        """
        mask_right_width = self.get_distance_from_point_to_line(chin_right_point, nose_point, chin_bottom_point)

        # mask_right_width(第17个特征点(x,y) 到 这条直线 的距离) * width_ratio(宽度比1.2)
        mask_right_width = int(mask_right_width * width_ratio)
        # print("mask_right_width",mask_right_width) #比如 132

        # 将切割出来的右边一半的口罩图片 从 (117, 146) resize为 举例的 (132, 147)
        # 也即是把口罩右半边宽和高 按比例缩放到 右脸面颊的宽 和 鼻梁到下巴的高,将口罩右半边按比例缩放进行适配 右脸面颊的宽 和 鼻梁到下巴的高,
        # 为的就是能在人脸右脸为侧脸的时候仍能把口罩右半边按比例缩放进行适配给为侧脸的右脸。
        mask_right_img = mask_right_img.resize((mask_right_width, new_height))

        # 合并 左脸的左半边口罩 和 右脸的右半边口罩:(左半边口罩的宽width + 右半边口罩的宽width, 口罩的高new_height)
        size = (mask_left_img.width + mask_right_img.width, new_height)
        print("size",size) #比如 (548, 307)
        """
        paste(透明图层的图, (粘贴的起始位置)):粘贴后的图像的背景图层会变成黑色,一般不使用
        paste(透明图层的图, (粘贴的起始位置), 透明图层的图):粘贴后的图像的背景图层依旧为透明,一般使用该方式
        """
        #RGBA:A代表透明图层通道
        mask_img = Image.new('RGBA', size)
        #paste(透明图层的左半边口罩的图, (0, 0), 透明图层的左半边口罩的图):粘贴后的左半边口罩的图的背景图层依旧为透明
        mask_img.paste(mask_left_img, (0, 0), mask_left_img)
        #paste(透明图层的右半边口罩的图, (左半边口罩的宽width, 0), 透明图层的右半边口罩的图):粘贴后的右半边口罩的图的背景图层依旧为透明
        mask_img.paste(mask_right_img, (mask_left_img.width, 0), mask_right_img)

        """
        1.chin_bottom_point:脸颊chin中的第9个特征点(x2,y2)的值,该特征点位于整个人脸68个特征点中的第9个,即下巴底部位置
          nose_point:鼻梁nose_bridge中的第2个特征点的值,该特征点位于整个人脸68个特征点中的第29个特征点(x1,y1)
        2.np.arctan2(chin_bottom_point[1] - nose_point[1], chin_bottom_point[0] - nose_point[0])
          => np.arctan2(脸颊chin中的第9个特征点(下巴底部位置)的y - 鼻梁nose_bridge中的第2个特征点的y, 
                        脸颊chin中的第9个特征点(下巴底部位置)的x - 鼻梁nose_bridge中的第2个特征点的x)
        3.numpy.arctan2(x1, x2, /, out=None, *, where=True, casting='same_kind', order='K', dtype=None, subok=True[, signature, extobj]) = <ufunc 'arctan2'>
            x1 :类似array, 实值。填入y坐标。
            x2 :类似array, 实值。填入x坐标。x2必须可广播以匹配x1的形状,反之亦然。
            
            arctan2与基础C库的atan2函数相同。C标准中定义了以下特殊值:
                x1	x2	arctan2(x1,x2)
                +/- 0	+0	+/- 0
                +/- 0	-0	+/- pi
                > 0	+/-inf	+0 / +pi
                < 0	+/-inf	-0 / -pi
                +/-inf	+inf	+/- (pi/4)
                +/-inf	-inf	+/- (3*pi/4)
            请注意,+ 0和-0是不同的浮点数,+inf和-inf也是如此。
            
            例子,考虑不同象限中的四个点:
                >>> x = np.array([-1, +1, +1, -1])
                >>> y = np.array([-1, -1, +1, +1])
                >>> np.arctan2(y, x) * 180 / np.pi
                array([-135.,  -45.,   45.,  135.])
                注意参数的顺序。arctan2在x2 = 0时以及在其他几个特殊点处也定义,获得以下范围内的值[-pi, pi]
                >>> np.arctan2([1., -1.], [0., 0.])
                array([ 1.57079633, -1.57079633])
                >>> np.arctan2([0., 0., np.inf], [+0., -0., np.inf])
                array([ 0.        ,  3.14159265,  0.78539816])
        """
        # print("chin_bottom_point[1]",chin_bottom_point[1]) #比如 107
        # print("nose_point[1]",nose_point[1])#比如 66
        # print("chin_bottom_point[0]",chin_bottom_point[0])#比如 84
        # print("nose_point[0]",nose_point[0]) #比如 80
        # print("chin_bottom_point[1] - nose_point[1]",chin_bottom_point[1] - nose_point[1])#比如 41
        # print("chin_bottom_point[0] - nose_point[0]",chin_bottom_point[0] - nose_point[0])#比如 4

        """
        radian = np.arctan2(y值,x值) 计算两点之间的弧度
            参数:
                y值:两个点y值之间差值。
                x值:两个点x值之间差值。
        把两点之间计算出来的弧度转换为相对于y轴倾斜的角度:        
            计算出来的是radian弧度,通过 radian / np.pi * 180 得出的是相对于x轴倾斜(逆时针旋转)的角度,
            但我们的目的是计算相对于y轴倾斜的角度,因此通过((radian / np.pi * 180) - 90) 即基础上减去90度,
            那么角度变成从相对于x轴倾斜变成相对于y轴倾斜,最后通过((radian / np.pi * 180) - 90) * i 计算出相对于y轴倾斜的角度。
        角度转弧度:π/180×角度
        弧度变角度:180/π×弧度
        """
        # 旋转口罩:np.arctan2 计算出弧度
        radian = np.arctan2(chin_bottom_point[1] - nose_point[1], chin_bottom_point[0] - nose_point[0])
        # print("radian",radian) #比如弧度为 1.473543128543331
        # print("两点差值",chin_bottom_point[1] - nose_point[1], chin_bottom_point[0] - nose_point[0])  #287  -109

        """ 
        1.chin_bottom_point:脸颊chin中的第9个特征点(下巴底部位置)
          nose_point:鼻梁nose_bridge中的第2个特征点
          左上角的坐标为(0,0),脸颊chin中的第9个特征点和鼻梁nose_bridge中的第2个特征点构成一条直线line。
                鼻梁特征点的x 等于 下巴特征点的x,代表两点都位于y轴上,直线line和y轴重合。
                鼻梁特征点的x 大于 下巴特征点的x,直线line在y轴的左边,直线line相对于y轴倾斜 负数值的角度数。
                鼻梁特征点的x 小于 下巴特征点的x,直线line在y轴的右边,直线line相对于y轴倾斜 正数值的角度数。
        """
        angle = ((radian / np.pi * 180) - 90) * -1
        # print("angle",angle)

        """
        API官网 rotate解析:https://pillow.readthedocs.io/en/stable/reference/Image.html
        rotate(self, angle, resample=NEAREST, expand=0, center=None, translate=None, fillcolor=None,)
            返回一个按照给定角度顺时钟围绕图像中心旋转后的图像拷贝。
            expand=0:如果为false/0或者缺省,则使输出图像的大小与输入图像的大小相同。 请注意,展开标记假定围绕中心旋转且没有平移。
            expand=1:如果为true/1,则扩展输出图像以使其足够大以容纳整个旋转的图像。
 
        expand=True时,从 输入图像的尺寸(548, 307) 变成 输出图像的尺寸(622, 483)。
        expand=False(缺省值)时,输出图像与输入图像尺寸一样大,均为(548, 307)。
        """
        rotated_mask_img = mask_img.rotate(angle, expand=True)
        print("rotated_mask_img.size",rotated_mask_img.size) #(622, 483)
        print("mask_img.size",mask_img.size) #(548, 307)

        # rotated_mask_img = mask_img.rotate(angle)
        # print("rotated_mask_img.size",rotated_mask_img.size) #(548, 307)
        # print("mask_img.size",mask_img.size) #(548, 307)

        # 计算口罩位置
        #计算 鼻梁nose_bridge中的第2个特征点的x 与 脸颊chin中的第9个特征点(下巴底部位置)的x 的平均值
        center_x = (nose_point[0] + chin_bottom_point[0]) // 2
        print("center_x",center_x) # 比如 833
        #计算 鼻梁nose_bridge中的第2个特征点的y 与 脸颊chin中的第9个特征点(下巴底部位置)的y 的平均值
        center_y = (nose_point[1] + chin_bottom_point[1]) // 2
        print("center_y",center_y) # 比如 486

        # 口罩的宽// 2 - 左半边口罩的宽width:即一半口罩的宽 减去 左脸半边口罩的宽,差值如果为正值代表 左脸半边口罩的宽 小于 右脸半边口罩的宽,
        # 差值如果为负值代表 左脸半边口罩的宽 大于 右脸半边口罩的宽,不管是正值还是负值,
        # 差值实际就是 左脸半边口罩的宽 和 右脸半边口罩的宽 之间的差值。
        # offset = mask_img.width // 2 - mask_left_img.width
        # print("offset",offset) # 比如 -20,负值代表 左半边口罩的宽 大于 右半边口罩的宽。
        # print("mask_img.width",mask_img.width) #548。那么有 548 // 2 = 274 即一半口罩的宽
        # print("rotated_mask_img.width",rotated_mask_img.width) #622
        # print("mask_left_img.width",mask_left_img.width) #294 即 左脸半边口罩的宽,那么有 左脸半边口罩的宽 大于 右脸半边口罩的宽

        """
        1.(center_x,center_y):
            鼻梁nose_bridge中的第2个特征点坐标位置 与 脸颊chin中的第9个特征点(下巴底部位置)坐标位置 的平均值,
            (center_x,center_y) 便大概落在鼻尖与上嘴唇之间的位置。
        2.rotated_mask_img.width // 2:旋转后的口罩的宽的一半
          rotated_mask_img.height // 2:旋转后的口罩的高的一半
        3.RAD表示弧度,Cos(1.47 rad)= Cos(84°13'29")= 0.100625733386932    
          角度转弧度:π/180×角度
          弧度变角度:180/π×弧度
        """

        # (center_x,center_y) 大概落在鼻尖与上嘴唇之间的位置,
        # 那么center_x - 旋转后的口罩图片的宽的一半 相当于把 旋转后的口罩图片的左上角起点x值 贴到 左脸颊处
        box_x = center_x - rotated_mask_img.width // 2
        # box_x = center_x + int(offset * np.cos(angle)) - rotated_mask_img.width // 2
        # print("int(offset * np.cos(radian))",int(offset * np.cos(radian))) # 7
        # print("rotated_mask_img.width // 2",rotated_mask_img.width // 2) # 311

        # (center_x,center_y) 大概落在鼻尖与上嘴唇之间的位置,
        # 那么center_x - 旋转后的口罩图片的高的一半 相当于把 旋转后的口罩图片的左上角起点y值 贴到 左脸颊处
        box_y = center_y - rotated_mask_img.height // 2
        # box_y = center_y + int(offset * np.sin(angle)) - rotated_mask_img.height // 2
        # print("int(offset * np.sin(radian))",int(offset * np.sin(radian))) # -18
        # print("rotated_mask_img.height // 2",rotated_mask_img.height // 2) # 241

        # 往人脸上添加口罩
        # (box_x,box_y) 即为 (center_x - rotated_mask_img.width // 2 ,center_y - rotated_mask_img.height // 2),
        # 代表  旋转后的口罩图片的左上角起点(x,y) 从 (center_x,center_y) 移动到 左脸颊处
        self._face_img.paste(rotated_mask_img, (box_x,box_y), rotated_mask_img)

        from PIL import ImageDraw
        d = ImageDraw.Draw(self._face_img)
        d.point([(center_x,center_y)], fill=(0,0,255))
        d.point([chin_bottom_point, nose_point], fill=(255,255,255))
        d.line([chin_bottom_point, nose_point], fill=(255,0,0), width=2)
        d.line([nose_point, (nose_point[0], chin_bottom_point[1])], fill=(0,0,255), width=2)

    def _save(self):
        # os.path.splitext(“文件路径”)    分离文件名与扩展名;默认返回(fname,fextension)元组,可做分片操作
        path_splits = os.path.splitext(self.face_path)
        new_face_path = path_splits[0] + '-with-mask' + path_splits[1]
        self._face_img.save(new_face_path)
        print(f'Save to {new_face_path}')

    """
    1.point:
        1.在计算左半边脸上的口罩时,point为第1个特征点(x,y):chin_left_point,即脸颊chin中的 第1个特征点的值,该特征点位于整个人脸68个特征点中的第1个
        2.在计算右半边脸上的口罩时,point为第17个特征点(x,y):chin_right_point,即脸颊chin中的 第17个特征点的值,该特征点位于整个人脸68个特征点中的第17个
      line_point1:
        第9个特征点(x2,y2):chin_bottom_point,即脸颊chin中的 第9个特征点的值,该特征点位于整个人脸68个特征点中的第9个,即下巴底部位置
      line_point2:
        第29个特征点(x1,y1):nose_point,即鼻梁nose_bridge中的第2个特征点的值,该特征点位于整个人脸68个特征点中的第29个
    2.点(x0,y0) 到直线 ax+by+c=0 的距离d 的表达式为 |ax0+by0+c| / sqrt(a^2+b^2)
    3.根据已知两点坐标(两点式)的直线表达式为 (x-x1)/(x2-x1) = (y-y1)/(y2-y1)
            可以得知 点(x1,y1)到点(x2,y2) 构成一条直线,那么 点(x,y)为 点(x1,y1)到点(x2,y2) 这条直线上的点,或者说是在这条直线方向上的点,
            因为这条直线可以是所在的方向上的无限延伸的,所以可以说点(x,y) 为 点(x1,y1)到点(x2,y2) 这条直线(方向)上的点。
       (x-x1)/(x2-x1) = (y-y1)/(y2-y1) 表达式 简化为 x(y2-y1)+y(x1-x2)+y1(x2-x1)+x1(y1-y2)=0
       那么可以根据上式得知 点(x,y)中的x代表x0,y代表y0。a代表(y2-y1),b代表(x1-x2),c代表y1(x2-x1)+x1(y1-y2)。
       最终得 |ax0+by0+c| / sqrt(a^2+b^2) 
                = |(y2-y1)x + (x1-x2)y + y1(x2-x1)+x1(y1-y2)| / sqrt((y2-y1)^2 + (x1-x2)^2)
                = (x,y) 即为 point[0]和point[1],也即 整个人脸68个特征点中的 第1个特征点(x,y) 或 第17个特征点(x,y)
                  (x1,y1) 即为 line_point1[0]和line_point1[1],也即 整个人脸68个特征点中的 第9个特征点(x,y)
                  (x2,y2) 即为 line_point2[0]和line_point2[1],也即 整个人脸68个特征点中的 第29个特征点(x,y)
                = |(line_point2[1]-line_point1[1])*point[0] + (line_point1[0]-line_point2[0])*point[1] 
                    + line_point1[1]*(line_point2[0]-line_point1[0]) + line_point1[0]*(line_point1[1]-line_point2[1])| 
                    / sqrt((line_point2[1]-line_point1[1])^2 + (line_point1[0]-line_point2[0])^2)
    """
    @staticmethod
    def get_distance_from_point_to_line(point, line_point1, line_point2):
        distance = np.abs((line_point2[1] - line_point1[1]) * point[0] +
                          (line_point1[0] - line_point2[0]) * point[1] +
                          (line_point2[0] - line_point1[0]) * line_point1[1] +
                          (line_point1[1] - line_point2[1]) * line_point1[0]) / \
                   np.sqrt((line_point2[1] - line_point1[1]) * (line_point2[1] - line_point1[1]) +
                           (line_point1[0] - line_point2[0]) * (line_point1[0] - line_point2[0]))
        return int(distance)


# if __name__ == '__main__':
#     #cli()
#     create_mask(image_path)
"""
命令行用法:python train_mask_detector.py --dataset dataset

1,同一张图片不能同时出现在戴口罩和不戴口罩两类样本里
2,注意目录结构,口罩检测功能的实现,需要数据集构建脚本,训练及测试程序
3,面部 landmarks可以自动推断面部结构的位置(包括:眼睛   眉毛   鼻子   嘴   下颌线)因此我们可以使用它来帮我们构建数据集
"""

from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.layers import AveragePooling2D
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Input
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.preprocessing.image import load_img
from tensorflow.keras.utils import to_categorical
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import os

# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
#--数据集:带口罩和不戴口罩的数据集的输入路径
ap.add_argument("-d", "--dataset", required=False,  default="./dataset",help="path to input dataset")
#--plot:输出训练过程信息的图片的路径,将使用matplotlib生成
ap.add_argument("-p", "--plot", type=str, default="plot.png", help="path to output loss/accuracy plot")
#--模型:生成的口罩检测器模型的存储路径
ap.add_argument("-m", "--model", type=str, default="mask_detector.model", help="path to output face mask detector model")
args = vars(ap.parse_args())

INIT_LR = 1e-4 #初始化的学习率
EPOCHS = 20
BS = 32 #批量大小

# 在我们的数据集目录中获取图像列表,然后初始化数据列表(即图像)和类图像
print("[INFO] loading images...")
#获取数据集中的所有图片的path
imagePaths = list(paths.list_images(args["dataset"]))
data = [] #初始化图片数据的列表
labels = [] #初始化图片标签的列表

# 遍历图像路径
for imagePath in imagePaths:
	# print(imagePath) #比如 ./dataset\without_mask\0.jpg
	# 从文件名中提取类标签
	label = imagePath.split(os.path.sep)[-2]
	# print("os.path.sep",os.path.sep) # 路径分隔符:\
	# print("label",label) # 比如为 dataset目录下的 with_mask/without_mask的文件夹名
	# 加载输入图像调整到调整到224×224像素:因为MobileNetV2预训练网络需要input_tensor=Input(shape=(224, 224, 3))的输入
	image = load_img(imagePath, target_size=(224, 224))
	#转换为数组格式
	image = img_to_array(image)
	#preprocess_input(预处理功能):
	# tensorflow下keras自带的类似于一个归一化的函数;其对传入的图像做了归一化处理,能够加快图像的处理速度。
	# 将输入图像中的像素强度缩放到范围[-1,1]
	image = preprocess_input(image)
	# 分别更新数据和标签列表,将预处理图像和相关标签分别附加到数据和标签列表
	data.append(image)
	labels.append(label)

# 将数据和标签转换为NumPy数组
data = np.array(data, dtype="float32")
labels = np.array(labels)
# print("labels",labels) #labels ['without_mask' 'with_mask'  ...  ]

#对标签labels 执行 one-hot编码
lb = LabelBinarizer()
labels = lb.fit_transform(labels)
# print("labels",labels) #labels [[1] ... [0]]
labels = to_categorical(labels)
# print("labels",labels) # [[0. 1.] ... [1. 0.]]

"""
stratify=labels 的作用:
	保持测试集与整个数据集里的labels中标签分类的比例一致。
	例子:
		整个数据集有1000条样本,也有1000个labels,并且labels分两类(即0和1),其中labels为0有300条样本,labels为1有700条样本,
		即labels分两类的比例为3:7。那么现在把整个数据集进行split切分,因为test_size = 0.2,所以训练集分到800条样本,测试集分到200条样本。
		因为stratify=labels,则训练集和测试集中的labels分两类的比例均为3:7,结果就是在训练集中labels有240个0和560个1,
		测试集中labels有60个0和140个1。
	同理,若将训练集进一步分出一个验证集:
		(trainX, valX, trainY, valY) = train_test_split(trainX, trainY, test_size=0.20, stratify=trainY, random_state=42)
		则训练集和验证集中的样本数分别为640和160,且由于stratify=trainY,验证集与训练集中的标签分类0和1的比例均为3:7,
		则验证集中将被分到48个0和112个1。
"""
#使用80%的数据进行训练,其余20%进行测试,将数据划分为训练和测试分组
(trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.20, stratify=labels, random_state=42)
"""
ImageDataGenerator()
	keras.preprocessing.image模块中的图片生成器,同时也可以在batch中对数据进行增强,扩充数据集大小,增强模型的泛化能力。
	比如进行旋转,变形,归一化等等。
		rotation_range(): 旋转范围
		width_shift_range(): 水平平移范围
		height_shift_range(): 垂直平移范围
		zoom_range(): 缩放范围
		fill_mode: 填充模式, constant, nearest, reflect
		horizontal_flip(): 水平反转
		vertical_flip(): 垂直翻转
"""
# 构建用于数据增强的训练图像生成器。在训练过程中,我们将对图像应用数据增扩,以提高泛化能力。
# 建立随机旋转、缩放、剪切、偏移和翻转参数。我们将在训练时使用aug对象。
aug = ImageDataGenerator(rotation_range=20, zoom_range=0.15, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.15,
						 horizontal_flip=True, fill_mode="nearest")

"""
微调是一种策略,我几乎总是建议在节省大量时间的同时建立一个基线模型。
我们的数据准备和模型结构的微调已经就绪,我们现在准备训练我们的口罩检测网络。
"""
# 加载MobileNetV2网络,并加载预先通过ImageNet训练的MobileNet权重。
# include_top=False 表示不保留FC层,自己构造一个新的FC层,并将其附加到Baseline上以代替旧的FC层。
# MobileNetV2预训练网络需要输入图像调整到调整到224×224像素:target_size=(224, 224)
baseModel = MobileNetV2(weights="imagenet", include_top=False, input_tensor=Input(shape=(224, 224, 3)))

# 新构造的FC层将放置在基础模型顶部的模型头部
headModel = baseModel.output
headModel = AveragePooling2D(pool_size=(7, 7))(headModel)
headModel = Flatten(name="flatten")(headModel)
headModel = Dense(128, activation="relu")(headModel)
headModel = Dropout(0.5)(headModel)
headModel = Dense(2, activation="softmax")(headModel)

# 将新构造的头部FC层放置在基本模型的顶部(这将成为我们将训练的实际模型)
model = Model(inputs=baseModel.input, outputs=headModel)

# 循环遍历基本模型中的所有层并冻结它们,以便在第一次训练过程中不会更新它们
for layer in baseModel.layers:
	layer.trainable = False

# 编译我们的模型
print("[INFO] compiling model...")
# 衰变率:初始化的学习率 / EPOCHS
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
#使用Adam优化器、学习率衰减计划和二进制交叉熵编译我们的模型。
model.compile(loss="binary_crossentropy", optimizer=opt, metrics=["accuracy"])

# 训练网络
print("[INFO] training head...")
H = model.fit(
	#数据增强对象(aug)将提供一批经过修改的图像数据
	aug.flow(trainX, trainY, batch_size=BS),
	steps_per_epoch=len(trainX) // BS, # 一个epoch需要多少步
	validation_data=(testX, testY),
	validation_steps=len(testX) // BS,
	epochs=EPOCHS)

# 对测试集做出预测,获取最高概率的类标签索引。
print("[INFO] evaluating network...")
predIdxs = model.predict(testX, batch_size=BS)

# 对于测试集中的每个图像,我们需要找到具有最大预测概率的标签索引
predIdxs = np.argmax(predIdxs, axis=1)

# 显示格式正确的分类报告
print(classification_report(testY.argmax(axis=1), predIdxs, target_names=lb.classes_))

# 将模型序列化到磁盘
print("[INFO] saving mask detector model...")
model.save(args["model"], save_format="h5")

# 绘制训练损失和准确性
N = EPOCHS
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, N), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, N), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, N), H.history["accuracy"], label="train_acc")
plt.plot(np.arange(0, N), H.history["val_accuracy"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")
plt.savefig(args["plot"])


请记住,为了分类一个人是否戴着口罩,我们首先需要执行面部检测-如果找不到一张脸(这是在这幅图像中发生的事情),那么就不能应用口罩检测器!
我们无法检测前景中的人脸的原因是:
它被面具遮住了
用于训练面部检测器的数据集不包含戴口罩的人的示例图像因此,如果大部分人脸被遮挡,我们的人脸检测器很可能无法检测出人脸。
从上面的结果部分可以看出,我们的面罩检测器在数据集有限的情况下做的还不错,尽管:
1,为了进一步改进我们的面罩检测模型,可以收集更多戴口罩的人的图像,不仅仅局限于人工生成。
虽然人工数据集在这种情况下工作得很好,但没有什么可以替代真实的数据集。
2,你还应该收集面部图像,尤其是有可能会“混淆”我们分类器的图像,使其认为此人戴着口罩,而事实上他们并没有,例如穿拉高领口的衣服,戴着围巾等等,所有这些都是可能被我们的面罩检测器混淆为戴口罩的样本。

我们目前检测一个人是否戴口罩的方法分为两步:
步骤1:进行面部检测
步骤2:将我们的面罩检测仪应用于每个面部
这种方法的问题在于,根据定义,面罩会遮住面部的一部分。如果有足够的人脸被遮挡,则无法检测到该人脸,因此,将不应用口罩检测器。
为了避免这个问题,应该训练一个两类对象检测器,该检测器由戴口罩类和不戴口罩类组成。
将对象检测器与专用的with_mask类结合将允许在两个方面改进模型。
首先,目标探测器将能够自然地探测到戴着口罩的人,否则,由于太多的脸部被遮挡,面部探测器将无法探测到这些人。
其次,这种方法将我们的计算机视觉流程简化为一个步骤-而不是应用人脸检测和我们的面罩检测器模型,这样的方法不仅计算效率更高,而且更“优雅”和端到端。
1,虽然人工数据集在很多情况下工作得很好,但更推荐加入部分真实的数据集。
2,不同的训练流程,数据增扩,超参数设置等都会产生不同的训练效果
3,某些图像本身带有“歧义”很容易造成分类器的误判
# 命令行用法:python detect_mask_image.py --image examples/example_01.png

from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.models import load_model
import numpy as np
import argparse
import cv2
import os

# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
# --image 包含用于推断的脸部输入图像的路径
ap.add_argument("-i", "--image", required=False, default="./examples/example_01.png", help="path to input image")
# --face 人脸检测器模型目录的路径(需要在对人脸进行分类之前对其进行本地化)
ap.add_argument("-f", "--face", type=str, default="./face_detector", help="path to face detector model directory")
# --model 训练的面罩检测器模型文件的路径
ap.add_argument("-m", "--model", type=str, default="./mask_detector.model", help="path to trained face mask detector model")
# --confidence 可选的概率阈值可以设置为超过50%以上的过滤弱人脸检测
ap.add_argument("-c", "--confidence", type=float, default=0.5, help="minimum probability to filter weak detections")
args = vars(ap.parse_args())

"""
dnn.readNet
	作用:加载深度学习网络及其模型参数
	原型:readNet(model, config=None, framework=None)
	参数:
		model: 训练的权重参数的模型二值文件,支持的格式有:*.caffemodel(Caffe)、*.pb(TensorFlow)、*.t7 或 *.net(Torch)、 
				*.weights(Darknet)、*.bin(DLDT).
		config: 包含网络配置的文本文件,支持的格式有:*.prototxt (Caffe)、*.pbtxt (TensorFlow)、*.cfg (Darknet)、*.xml (DLDT).
		framework: 所支持格式的框架名
	该函数自动检测训练模型所采用的深度框架,然后调用 readNetFromCaffe、readNetFromTensorflow、readNetFromTorch 或 readNetFromDarknet 
	中的某个函数完成深度学习网络模型及模型参数的加载。
 	下面我们看下对应于特定框架的API:
		1.Caffe
			readNetFromCaffe(prototxt, caffeModel=None)
				作用:加载采用Caffe的配置网络和训练的权重参数
		2.Darknet
			readNetFromDarknet(cfgFile, darknetModel=None)
				作用:加载采用Darknet的配置网络和训练的权重参数
		3.Tensorflow
			readNetFromTensorflow(model, config=None)
				作用:加载采用Tensorflow 的配置网络和训练的权重参数
				参数:
					model: .pb 文件
					config: .pbtxt 文件
		4.Torch
			readNetFromTorch(model, isBinary=None)
				作用:加载采用 Torch 的配置网络和训练的权重参数
				参数:
				model: 采用 torch.save()函数保存的文件
		5.ONNX
			readNetFromONNX(onnxFile)
				作用:加载 .onnx 模型网络配置参数和权重参数
"""
# 从磁盘加载序列化的面部检测器模型
print("[INFO] loading face detector model...")
#网络模型文件:Caffe中deploy.prototxt
prototxtPath = os.path.sep.join([args["face"], "deploy.prototxt"])
#预训练的权重参数文件:res10_300x300_ssd_iter_140000.caffemodel(300x300代表该检测人脸的模型的输入应为300x300)
weightsPath = os.path.sep.join([args["face"], "res10_300x300_ssd_iter_140000.caffemodel"])
# 所加载的预训练模型用于人脸检测
net = cv2.dnn.readNet(prototxtPath, weightsPath)

#从磁盘加载面罩检测器模型文件
print("[INFO] loading face mask detector model...")
#所加载的预训练模型用于口罩检测
model = load_model(args["model"])

# 从磁盘加载输入图像,对其进行克隆,然后获取图像的空间尺寸
image = cv2.imread(args["image"])
#复制
orig = image.copy()
#获取帧尺寸,以便将来缩放和显示
(h, w) = image.shape[:2]

"""
dnn.blobFromImage
	作用:根据输入图像,创建维度N(图片的个数),通道数C,高H和宽W次序的blobs
	原型:blobFromImage(image, scalefactor=None, size=None, mean=None, swapRB=None, crop=None, ddepth=None)
	参数:
		image:cv2.imread 读取的图片数据
		scalefactor: 缩放像素值,如 [0, 255] - [0, 1]
		size: 输出blob(图像)的尺寸,如 (netInWidth, netInHeight)
		mean: 从各通道减均值. 如果输入 image 为 BGR 次序,且swapRB=True,则通道次序为 (mean-R, mean-G, mean-B).
		swapRB: 交换 3 通道图片的第一个和最后一个通道,如 BGR - RGB
		crop: 图像尺寸 resize 后是否裁剪. 如果crop=True,则,输入图片的尺寸调整resize后,一个边对应与 size 的一个维度,
			   而另一个边的值大于等于 size 的另一个维度;然后从 resize 后的图片中心进行 crop. 
			   如果crop=False,则无需 crop,只需保持图片的长宽比
		ddepth: 输出 blob 的 Depth. 可选: CV_32F 或 CV_8U
		
blob = cv2.dnn.blobFromImage(image, scalefactor=1.0, size, mean, swapRB=True)
	在进行深度学习或者图片分类时,blobFromImage主要是用来对图片进行预处理。包含两个主要过程:
		1,整体像素值减去平均值(mean)
		2,通过缩放系数(scalefactor)对图片像素值进行缩放
	参数:	
		image:这个就是我们将要输入神经网络进行处理或者分类的图片。
		scalefactor:当我们将图片减去平均值之后,还可以对剩下的像素值进行一定的尺度缩放,它的默认值是1,如果希望减去平均像素之后的值,
					  全部缩小一半,那么可以将scalefactor设为1/2。
		size:这个参数是我们神经网络在训练的时候要求输入的图片尺寸。
		mean:需要将图片整体减去的平均值,如果我们需要对RGB图片的三个通道分别减去不同的值,那么可以使用3组平均值,
			   如果只使用一组例如(B=106.13, G=115.97, R=124.96),那么就默认对三个通道减去一样的值。
			   减去平均值(mean):为了消除同一场景下不同光照的图片,对我们最终的分类或者神经网络的影响,
			   我们常常对图片的R、G、B通道的像素求一个平均值,然后将每个像素值减去我们的平均值,这样就可以得到像素之间的相对值,
			   就可以排除光照的影响。
		swapRB:OpenCV中认为我们的图片通道顺序是BGR,但是我平均值假设的顺序是RGB,所以如果需要交换R和G,那么就要使swapRB=true
"""
# 预处理由OpenCV的blobFromImage函数:
# 	1.将输入图片大小调整为300×300像素,并执行均值减去。
#	  因为预训练的权重参数文件使用的为:res10_300x300_ssd_iter_140000.caffemodel(其中300x300代表该检测人脸的模型的输入应为300x300)
# 	2.图片RGB三个通道分别减去(B=104.0, G=177.0, R=123.0),即每个通道减去指定的均值
# 	3.缩放系数1.0 可以对图片像素值进行缩放,此处使用1.0代表了并没有做真正的缩放,像素值除以1.0结果值仍不变
blob = cv2.dnn.blobFromImage(image, 1.0, (300, 300), (104.0, 177.0, 123.0))

# 通过网络传递blob图片数据 并获得面部检测
print("[INFO] computing face detections...")
net.setInput(blob)
# 执行面部检测以定位图像中所有面部的位置
detections = net.forward()
# print("detections.shape",detections.shape) #(1, 1, 200, 7)
# print("detections[0, 0, 0, 0]",detections[0, 0, 0, 0]) #0.0
# print("detections[0, 0, 0, 1]",detections[0, 0, 0, 1]) #1.0 目标标签
# print("detections[0, 0, 0, 2]",detections[0, 0, 0, 2]) #0.9984427 目标是否为人脸的置信度
# print("detections[0, 0, 0, 3:7]",detections[0, 0, 0, 3:7]) #[0.5462329  0.12488028 0.6709176  0.3542412 ]
"""
for i in range(0, detections.shape[2]) 循环遍历每个检测出来的可能目标的人脸
	detections.shape:(1, 1, 200, 7)
		detections.shape[2]:表示有200个可能为人脸的目标
		detections.shape[3]:每个人脸目标对应的7个值,第2个值为目标标签,第3个值为目标是否为人脸的置信度,
							   第4到第7个值一共4个值为人脸坐标位置信息
		
	idx = int(detections[0, 0, 0, 1])  #1.0 提取第1个可疑人脸的目标标签
	confidence = detections[0, 0, 0, 2] #0.9984427 提取第1个可疑人脸的置信度
	box = detections[0, 0, 0, 3:7] #[0.5462329  0.12488028 0.6709176  0.3542412 ] 提取第1个可疑人脸的4个位置信息值
"""
for i in range(0, detections.shape[2]):
	#提取与检测相关的置信度(即概率)
	confidence = detections[0, 0, i, 2] #目标是否为人脸的置信度
	# print("confidence",confidence) #0.9984427
	# 通过确保置信度大于最小置信度来滤除弱检测。
	if confidence > args["confidence"]:
		# 计算对象边界框的(x,y)坐标:可疑人脸的4个位置信息值 还需要乘以 原图的宽和高组成的np.array([w, h, w, h])
		box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
		(startX, startY, endX, endY) = box.astype("int")

		# 确保边界框在框架尺寸之内。计算特定面的边界框值,并确保该框落在图像的边界内
		(startX, startY) = (max(0, startX), max(0, startY))
		(endX, endY) = (min(w - 1, endX), min(h - 1, endY))

		# 通过NumPy切片提取面部ROI区域
		face = image[startY:endY, startX:endX]
		# 将其从BGR转换为RGB通道顺序
		face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
		# 将其调整为224x224(代表该检测口罩的模型的输入为224x224),然后进行预处理
		face = cv2.resize(face, (224, 224))
		# 转换为数组格式
		face = img_to_array(face)
		# preprocess_input(预处理功能):
		# 	tensorflow下keras自带的类似于一个归一化的函数;其对传入的图像做了归一化处理,能够加快图像的处理速度。
		# 	将输入图像中的像素强度缩放到范围[-1,1]
		face = preprocess_input(face)
		face = np.expand_dims(face, axis=0)

		# 将脸部通过模型以预测脸部是否有遮罩
		(mask, withoutMask) = model.predict(face)[0]

		# 确定我们将用来绘制边界框和文本的类标签和颜色
		# 根据口罩检测器模型返回的概率确定类标签,如果 mask的概率值大于withoutMask的概率值的话,则label = "Mask" 反之 label = "No Mask"
		label = "Mask" if mask > withoutMask else "No Mask"
		# 指定相关颜色。有口罩的颜色是绿色,没有口罩的颜色是红色。
		color = (0, 255, 0) if label == "Mask" else (0, 0, 255)
		# 包括 标签、概率
		label = "{}: {:.2f}%".format(label, max(mask, withoutMask) * 100)

		# 使用OpenCV绘图函数 绘制标签文本(包括类和概率)以及面的边框矩形。
		# 在输出图片上显示标签和边框矩形
		cv2.putText(image, label, (startX, startY - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 2)
		cv2.rectangle(image, (startX, startY), (endX, endY), color, 2)

# 显示输出图像
cv2.imshow("Output", image)
cv2.waitKey(0)
# 命令行用法:python detect_mask_video.py

from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.models import load_model
from imutils.video import VideoStream
import numpy as np
import argparse
import imutils
import time
import cv2
import os

"""
此函数检测人脸,然后将我们的口罩分类器应用于每个人脸ROI。
	detect_and_predict_mask函数接受三个参数:
	frame:来自我们的流的帧
	faceNet:用于检测人脸在图像中的位置的模型
	maskNet:口罩识别模型
	在内部,我们构造一个blob、检测面和初始化列表,这些列表包括我们的faces(即ROI)、loc(面位置)和pred(有口罩/无口罩预测列表)。
"""
def detect_and_predict_mask(frame, faceNet, maskNet):
	#获取帧尺寸,以便将来缩放和显示
	(h, w) = frame.shape[:2]
	# 预处理由OpenCV的blobFromImage函数:
	# 	1.将输入图片大小调整为300×300像素,并执行均值减去。
	#	  因为预训练的权重参数文件使用的为:res10_300x300_ssd_iter_140000.caffemodel(其中300x300代表该检测人脸的模型的输入应为300x300)
	# 	2.图片RGB三个通道分别减去(B=104.0, G=177.0, R=123.0),即每个通道减去指定的均值
	# 	3.缩放系数1.0 可以对图片像素值进行缩放,此处使用1.0代表了并没有做真正的缩放,像素值除以1.0结果值仍不变
	blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0))
	#帧图传入faceNet 进行人脸检测
	faceNet.setInput(blob)
	# 执行面部检测以定位图像中所有面部的位置
	detections = faceNet.forward()

	#初始化我们的面部列表,它们的对应位置以及来自我们的面罩网络的预测列表
	faces = []
	locs = []
	preds = []

	# 循环检测
	for i in range(0, detections.shape[2]):
		#提取与检测相关的置信度(即概率)
		confidence = detections[0, 0, i, 2] #目标是否为人脸的置信度

		# 通过确保置信度大于最小置信度来滤除弱检测
		if confidence > args["confidence"]:
			# 计算对象边界框的(x,y)坐标:可疑人脸的4个位置信息值 还需要乘以 原图的宽和高组成的np.array([w, h, w, h])
			box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
			(startX, startY, endX, endY) = box.astype("int")

			# 确保边界框在框架尺寸之内。计算特定面的边界框值,并确保该框落在图像的边界内
			(startX, startY) = (max(0, startX), max(0, startY))
			(endX, endY) = (min(w - 1, endX), min(h - 1, endY))

			# 通过NumPy切片提取面部ROI区域
			face = frame[startY:endY, startX:endX]
			# 将其从BGR转换为RGB通道顺序
			face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
			# 将其调整为224x224(代表该检测口罩的模型的输入为224x224),然后进行预处理
			face = cv2.resize(face, (224, 224))
			# 转换为数组格式
			face = img_to_array(face)
			# preprocess_input(预处理功能):
			# 	tensorflow下keras自带的类似于一个归一化的函数;其对传入的图像做了归一化处理,能够加快图像的处理速度。
			# 	将输入图像中的像素强度缩放到范围[-1,1]
			face = preprocess_input(face)
			face = np.expand_dims(face, axis=0)

			# 将脸部和边界框添加到它们各自的列表中
			faces.append(face)
			locs.append((startX, startY, endX, endY))

	# 仅在检测到至少一张脸的情况下做出预测
	if len(faces) > 0:
		#我们确保至少检测到一个人脸(第64行)-如果没有,我们将返回空的pred。其次,我们对框架中的整批面部执行推断,以便流程更快。
		#为了更快地进行推断,我们将同时对所有面孔进行批量预测,而不是上面的“ for”循环中的一对一预测
		preds = maskNet.predict(faces)

	# 将我们的面部边界框位置和相应的有口罩/非口罩预测返回给调用函数。
	# 返回两元组的脸部位置及其对应的位置
	return (locs, preds)

# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
# --face 人脸检测器模型目录的路径(需要在对人脸进行分类之前对其进行本地化)
ap.add_argument("-f", "--face", type=str, default="face_detector", help="path to face detector model directory")
# --model 训练的面罩检测器模型文件的路径
ap.add_argument("-m", "--model", type=str, default="mask_detector.model", help="path to trained face mask detector model")
# --confidence 可选的概率阈值可以设置为超过50%以上的过滤弱人脸检测
ap.add_argument("-c", "--confidence", type=float, default=0.5, help="minimum probability to filter weak detections")
args = vars(ap.parse_args())

# 从磁盘加载序列化的面部检测器模型
print("[INFO] loading face detector model...")
#网络模型文件:Caffe中deploy.prototxt
prototxtPath = os.path.sep.join([args["face"], "deploy.prototxt"])
#预训练的权重参数文件:res10_300x300_ssd_iter_140000.caffemodel(300x300代表该检测人脸的模型的输入应为300x300)
weightsPath = os.path.sep.join([args["face"], "res10_300x300_ssd_iter_140000.caffemodel"])
# 所加载的预训练模型用于人脸检测
faceNet = cv2.dnn.readNet(prototxtPath, weightsPath)

print("[INFO] loading face mask detector model...")
#所加载的预训练模型用于口罩检测
maskNet = load_model(args["model"])

#初始化视频流,并允许摄像头传感器预热
print("[INFO] starting video stream...")
#开启摄像头
# vs = VideoStream(src=0).start()
vs = cv2.VideoCapture(0)
time.sleep(2.0)

# 循环播放摄像头视频流中的帧
while True:
	# 从视频流中抓取帧
	# frame = vs.read()
	sucess, frame = vs.read()
	#将帧调整为最大宽度为400像素
	frame = imutils.resize(frame, width=400)

	# 检测框架中的脸并确定他们是否戴着口罩
	(locs, preds) = detect_and_predict_mask(frame, faceNet, maskNet)

	# 循环检测到的面部位置及其对应位置
	for (box, pred) in zip(locs, preds):
		# 解包 边界框和预测
		(startX, startY, endX, endY) = box
		(mask, withoutMask) = pred

		# 确定我们将用来绘制边界框和文本的类标签和颜色
		# 根据口罩检测器模型返回的概率确定类标签,如果 mask的概率值大于withoutMask的概率值的话,则label = "Mask" 反之 label = "No Mask"
		label = "Mask" if mask > withoutMask else "No Mask"
		# 指定相关颜色。有口罩的颜色是绿色,没有口罩的颜色是红色。
		color = (0, 255, 0) if label == "Mask" else (0, 0, 255)
		# 包括 标签、概率
		label = "{}: {:.2f}%".format(label, max(mask, withoutMask) * 100)

		# 使用OpenCV绘图函数 绘制标签文本(包括类和概率)以及面的边框矩形。
		# 在输出图片上显示标签和边框矩形
		cv2.putText(frame, label, (startX, startY - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 2)
		cv2.rectangle(frame, (startX, startY), (endX, endY), color, 2)

	# 显示输出图像
	cv2.imshow("Frame", frame)
	key = cv2.waitKey(1) & 0xFF

	# 如果按下“ q”键,则退出循环
	if key == ord("q"):
		break

# 做一点清理
cv2.destroyAllWindows()
vs.stop()
"""  python-使用arctan / arctan2绘制从0到2π的a  """
# import pylab
# import numpy as np
# e = np.arange(0.0, 1.0, 0.15).reshape(-1, 1)
# nu = np.linspace(0, 2*np.pi, 50000)
# x =  ((1-e)/(1+e))**0.5 * np.tan(nu/2.)
# x2 = e*(1-e**2)**0.5 * np.sin(nu)/(1 + e*np.cos(nu))
# using_arctan = True
# using_OP_arctan2 = False
#
# if using_arctan:
#     M2evals = 2*np.arctan(x) - x2
#     M2evals[ M2evals<0 ] += 2*np.pi
# elif using_OP_arctan2:
#     M2evals = 2 * np.arctan2(1,1/x) - x2
#
# fig2 = pylab.figure()
# ax2 = fig2.add_subplot(111)
# for M2e, _e in zip(M2evals, e.ravel()):
#     ax2.plot(nu.ravel(), M2e, label = str(_e))
# pylab.legend(loc='upper left')
# pylab.show()

"""
atan 和 atan2 都是反正切函数,返回的都是弧度
对于两点形成的直线,两点分别是 point(x1,y1) 和 point(x2,y2),其斜率对应角度的计算方法可以是:
angle = atan( (y2-y1)/(x2-x1) ) 或 angle = atan2( y2-y1, x2-x1 )

因此可以看出 atan 和 atan2 的区别:
    1、参数的个数不同;atan 为单个参数,atan2为两个参数
    2、atan2 的优点在于: 如果 x2-x1等于0 ,角度依然可以计算,但是atan函数则需要提前判断,否则就会导致程序出错;

结论: atan 和 atan2函数,建议用 atan2函数;
注意:1、两者返回值都是弧度;2、atan2函数参数是y在前,x在后;3、需要先导入math库
下面再根据几个实例来看一下它们的用法和区别:
"""
import math
#倾斜45度时,弧度转角度
a=math.atan(1)
#倾斜45度时,弧度转角度
b=math.atan2(1,1)
print(a,b) # 0.7853981633974483 0.7853981633974483

#垂直90度时,弧度转角度
b=math.atan2(60,0)
print(b) #1.5707963267948966
print(b/math.pi*180) #90.0

#倾斜45度时,弧度转角度
b=math.atan2(100-90,100-90)
print(b) # 0.7853981633974483
print(b/math.pi*180) # 45.0

b=math.atan2(-10,-10)
print(b) # -2.356194490192345
print(b/math.pi*180) # -135.0

#水平时,弧度转角度
b=math.atan2(0,100)
print(b) #0.0
print(b/math.pi*180) #0.0

# b=math.atan2(287, -109)
# print(b) #1.9337606438498591
# print(b/math.pi*180) #110.79632348109763



"""
返回一个按照给定角度顺时钟围绕图像中心旋转后的图像拷贝。变量filter是NEAREST、BILINEAR或者BICUBIC之一。
如果省略该变量,或者图像模式为“1”或者“P”,则默认为NEAREST。
变量expand,如果为true,表示输出图像足够大,可以装载旋转后的图像。如果为false或者缺省,则输出图像与输入图像尺寸一样大。
"""
from PIL import Image, ImageFile
im = Image.open("1.jpg")  ##文件存在的路径,如果没有路径就是当前目录下文件
im_45 = im.rotate(45, expand=1)
# im_45 = im.rotate(-45, expand=1)
im_90 = im.rotate(90, Image.NEAREST,1)
print(im_45.size, im_90.size)
im_45.show()
im_90.show()

# # 读取图片
# pilim = Image.open('1.jpg')
# # 转换为有alpha层
# im2 = pilim.convert('RGBA')
# # 旋转
# rot = im2.rotate(-45, expand=1)
# # 创建一个与旋转图像大小相同的白色图像
# fff = Image.new('RGBA', rot.size, (255,)*4)
# # 使用alpha层的rot作为掩码创建一个复合图像
# out = Image.composite(rot, fff, rot)
# out.show()
# 保存
# out.convert(pilim.mode).save('./res/out.jpg')

"""
  im.transpose(method)⇒ image
返回当前图像的翻转或者旋转的拷贝。变量方法的取值为:FLIP_LEFT_RIGHT,FLIP_TOP_BOTTOM,ROTATE_90,ROTATE_180,或ROTATE_270。
"""
# from PIL import Image
# im = Image.open("1.jpg")
# im.show()
# im1=im.rotate(45)
# im1.show()#逆时针旋转 45 度角。
# im2=im.transpose(Image.FLIP_LEFT_RIGHT)#左右对换
# im2.show()
# im3=im.transpose(Image.FLIP_TOP_BOTTOM)#上下对换。
# im3.show()
# im4=im.transpose(Image.ROTATE_90)#旋转 90 度角。
# im4.show()
# im5=im.transpose(Image.ROTATE_180)#旋转 180 度角。
# im5.show()
# im6=im.transpose(Image.ROTATE_270)#旋转 270 度角。
# im6.show()

  • 19
    点赞
  • 84
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值