深度学习——基于MTCNN算法实现人脸侦测

news2024/11/14 18:52:33

这里写目录标题

      • 先看效果
  • MTCNN
      • 主体思想
        • 级联网络
        • 图像金字塔
        • IOU算法
          • iou 公式
        • nms 算法
        • 数据生成celeba
      • 数据代码
      • 训练代码
      • 侦测代码
      • 总结

先看效果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

MTCNN

从2016年,MTCNN算法出来之后,属实在工业上火了一把,最近尝试着把论文代码复现了一下。

主体思想

级联网络

**在这里插入图片描述
这篇论文属于一篇多任务级联卷积神经网络,如图,利用P、R、O 三个网络来进行检测。
算法步骤:

  1. 将传入p网络中,预选一些框,生成特征图
  2. 将p网络中的预选框传入R网络中进行进一步筛选
  3. 将R网络中的预选框传入O网络中进行进一步筛选
  4. O网络进行最终判定,输出人脸框。
    在这里插入图片描述

上述步骤在图像金字塔的总框架下实现,为实现不同尺度下的人脸侦测,采用图像金字塔算法,将不同尺度的人脸传入网络中,虽然这样有可能让模型准确率更高,但是这样也导致模型运算的次数增加。
P网络:
由于P网络未知一张图片含有多少人脸,所以使用全卷积网络接收任意大小的图片,将通道数作为输出特征数,使用图像金字塔缩放图片(最短边长大于12),并设卷积核步长为1不放过任何一个可能性。
采用12x12的大核决定网络侦测到的最小人脸。以固定大小框切取同样大小图片(输出固定)。
使用卷积网络实现切割12x12图片,通过多层小卷积核神经网络的特征提取等价于大核单层神经网络(感受野相同),抽象能力更强,特征提取更精细,参数更少,网络运行更快。使用偏移量的大小作为训练损失,使得网络计算更方便;。
对不同任务设置不同的损失函数,增强学习的针对性。增加人脸五官特征任务,增加网络学习任务的复杂性,清晰网络学习的方向。每增加一个损失,会提升神经网络特征提取的能力,促进网络模型优化。
R网络:
通过多层小卷积核神经网络的特征提取等价于大核单层神经网络(感受野相同)。
由于输入为24 x 24固定大小的图片,输出则可使用全连接提取特征(在此处使用卷积核提取特征相同)。
O网络:
通过多层小卷积核神经网络的特征提取等价于大核单层神经网络(感受野相同)。
由于输入为48 x 28固定大小的图片,输出则可使用全连接提取特征(在此处使用卷积核提取特征相同)。

图像金字塔
一张图片上会存在着多个目标,且目标的大小不一样,模型对于一些过小或者过大的目标都无法检测,需要强调一些多尺度信息。MTCNN中对于图像的缩放因子为0.703左右。

另外不得不提的两个算法,IOU 和NMS.

IOU算法

MTCNN制作数据集样本要求:

IOU范围 样本类型 标签
0 ~ 0.3 非人脸 置信度为0(不参与计算偏移量损失)
0.3 ~ 0.4 地标(缓冲区,防止误判)
0.4 ~ 0.65 Part人脸 置信度为2(不参与计算置信度损失)
0.65 ~ 1 人脸 置信度为1

IoU是一种测量在特定数据集中检测相应物体准确度的一个标准。IoU是一个简单的测量标准,只要是在输出中得出一个预测范围(bounding boxex)的任务都可以用IoU来进行测量。为了可以使IoU用于测量任意大小形状的物体检测,我们需要:人为在训练集图像中标出要检测物体的大概范围。也就是先验框的说法。
也就是说,这个标准用于测量真实和预测之间的相关度,相关度越高,该值越高。
在FasterRcnn也提出,相近的框近似于线性变换关系,利用于此,学习先验框与实际框之间的映射关系,比直接学习目标的框更为容易。

iou 公式

在这里插入图片描述
在这里插入图片描述

反应两个框之间的交并比关系,IOU越大,代表重合程度更大.

def iou(box,boxes,isMin=False):

    box_area = (box[3]-box[1])*(box[2]-box[0])
    boxes_area = (boxes[:,3]-boxes[:,1])*(boxes[:,2]-boxes[:,0])


    xx1 = np.maximum(box[0],boxes[:,0])
    yy1 = np.maximum(box[1],boxes[:,1])
    xx2 = np.minimum(box[2],boxes[:,2])
    yy2 = np.minimum(box[3],boxes[:,3])

    w = np.maximum(0, (xx2-xx1))
    h = np.maximum(0,(yy2-yy1))

    area =w*h

    if isMin:
        return np.true_divide(area,np.minimum(box_area,boxes_area))
    else:
        return np.true_divide(area,box_area+boxes_area-area)
nms 算法

在这里插入图片描述
-直接问题就是为了解决多个输出框的问题

  • 选取最大置信度的框
  • 剩余的框与最大置信度框进行iou 交并比计算
  • 把iou大于某个阈值的框进行过滤,这样可以去除掉大量的重复框
    代码实现如下:
def nms(boxes, thresh=0.3, isMin = False):
    #框的长度为0时(防止程序有缺陷报错)
    if boxes.shape[0] == 0:
        return np.array([])

    #框的长度不为0时
    #根据置信度排序:[x1,y1,x2,y2,C]
    _boxes = boxes[(-boxes[:, 4]).argsort()] # #根据置信度“由大到小”,默认有小到大(加符号可反向排序)
    #创建空列表,存放保留剩余的框
    r_boxes = []
    # 用1st个框,与其余的框进行比较,当长度小于等于1时停止(比len(_boxes)-1次)
    while _boxes.shape[0] > 1: #shape[0]等价于shape(0),代表0轴上框的个数(维数)
        #取出第1个框
        a_box = _boxes[0]
        #取出剩余的框
        b_boxes = _boxes[1:]

        #将1st个框加入列表
        r_boxes.append(a_box) ##每循环一次往,添加一个框
        # print(iou(a_box, b_boxes))

        #比较IOU,将符合阈值条件的的框保留下来
        index = np.where(iou(a_box, b_boxes,isMin) < thresh) #将阈值小于0.3的建议框保留下来,返回保留框的索引
        _boxes = b_boxes[index] #循环控制条件;取出阈值小于0.3的建议框

    if _boxes.shape[0] > 0: ##最后一次,结果只用1st个符合或只有一个符合,若框的个数大于1;★此处_boxes调用的是whilex循环里的,此判断条件放在循环里和外都可以(只有在函数类外才可产生局部作用于)
        r_boxes.append(_boxes[0]) #将此框添加到列表中
    #stack组装为矩阵::将列表中的数据在0轴上堆叠(行方向)
    return np.stack(r_boxes)

上述讲完,基本知识已经完毕,剩下实操代码:
网络模型代码实现:
这里我实现了修改:

  1. 采用了BN,方便训练模型更快的收敛。
  2. 将每个模型中的最大池化全部转化为卷积步长=2的方式(数据量基本足够,所以不用担心过拟合,但是这样会导致计算量的增大),改了效果确实好了不少。
from torch import nn
import torch

class PNet(nn.Module):
    def __init__(self):
        super(PNet,self).__init__()
        self.name = "pNet"
        self.pre_layer = nn.Sequential(
            nn.Conv2d(in_channels=3,out_channels=10,kernel_size=(3,3),stride=(1,1),padding=(1,1)), # conv1
            nn.PReLU(),
            # prelu1
            nn.Conv2d(in_channels=10,out_channels=10,kernel_size=(3,3),stride=(2,2)),    
            nn.Conv2d(10,16,kernel_size=(3,3),stride=(1,1)), # conv2
            nn.PReLU(),                              # prelu2
            nn.Conv2d(16,32,kernel_size=(3,3),stride=(1,1)), # conv3
            nn.PReLU()                               # prelu3
        )

        self.conv4_1 = nn.Conv2d(32,1,kernel_size=(1,1),stride=(1,1))
        self.conv4_2 = nn.Conv2d(32,4,kernel_size=(1,1),stride=(1,1))



    def forward(self, x):
        x = self.pre_layer(x)
        cond = torch.sigmoid(self.conv4_1(x)) # 置信度用sigmoid激活(用BCEloos时先要用sigmoid激活)
        offset = self.conv4_2(x)         # 偏移量不需要激活,原样输出
        return cond,offset

R网络

# R网路
class RNet(nn.Module):
    def __init__(self):
        super(RNet,self).__init__()
        self.name = "RNet"
        self.pre_layer = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=28, kernel_size=(3,3),stride=(1,1),padding=(1,1)), # conv1
            nn.BatchNorm2d(28),
            nn.PReLU(),                                                                  # prelu1
            nn.Conv2d(in_channels=28, out_channels=28, kernel_size=(3, 3), stride=(2, 2)), 
            nn.BatchNorm2d(28),
            nn.PReLU(),
            # pool1
            nn.Conv2d(28, 48, kernel_size=(3,3),stride=(1,1)),  # conv2
            nn.BatchNorm2d(48),
            nn.PReLU(),                                  # prelu2
            nn.Conv2d(in_channels=48, out_channels=48, kernel_size=(3, 3), stride=(2, 2)), 
            nn.BatchNorm2d(48),
            nn.PReLU(),
            nn.Conv2d(48, 64, kernel_size=(2,2), stride=(1,1)),          # conv3
            nn.BatchNorm2d(64),
            nn.PReLU()                                           # prelu3
        )
        self.conv4 = nn.Linear(64*3*3,128) # conv4
        self.prelu4 = nn.PReLU()           # prelu4
        #detetion
        self.conv5_1 = nn.Linear(128,1)
        #bounding box regression
        self.conv5_2 = nn.Linear(128, 4)

    def forward(self, x):
        #backend
        x = self.pre_layer(x)
        x = x.view(x.size(0),-1)
        x = self.conv4(x)
        x = self.prelu4(x)
        #detection
        label = torch.sigmoid(self.conv5_1(x)) # 置信度
        offset = self.conv5_2(x) # 偏移量
        return label,offset


数据转化代码:这里采用的是论文中的数据celeba 和widerface
先把celeba的数据中的box和landmarks集中一起


 landmarks_path = r"F:\CelebA\Anno\list_landmarks_align_celeba.txt"
    bbox_path = r"F:\CelebA\Anno\list_bbox_celeba.txt"

    save_path = "anno.txt"
    with open(landmarks_path,"r") as f:
        landmarks = f.readlines()
    with open(bbox_path,"r") as f:
        bbox = f.readlines()
    with open(save_path,"w") as f:
        for i,(line1,line2) in enumerate(zip(bbox,landmarks)):
            if i<1:
                f.write(line1)
            elif i==1:
                strs = line1.strip()+" "+ line2
                f.write(strs)
                # f.write(line2)
            else:
                strs = line1.strip().split()+line2.strip().split()[1:]
                strs = " ".join(strs)+"\n"
                f.write(strs)

O网路的实现:

# O网路
class ONet(nn.Module):
    def __init__(self):
        super(ONet,self).__init__()
        self.name = "oNet"
        # backend
        self.pre_layer = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=(3,3), stride=(1,1),padding=(1,1)),  # conv1
            nn.BatchNorm2d(32),
            nn.PReLU(),                                          # prelu1
            nn.Conv2d(in_channels=32, out_channels=32, kernel_size=(3, 3), stride=(2, 2)),  
            nn.BatchNorm2d(32),
            nn.PReLU(),
            nn.Conv2d(32, 64,kernel_size=(3,3), stride=(1,1)),  # conv2
            nn.BatchNorm2d(64),
            nn.PReLU(),  # prelu2
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(3, 3), stride=(2, 2)),  
            nn.BatchNorm2d(64),
            nn.PReLU(),  # prelu2
            nn.Conv2d(64, 64, kernel_size=(3,3), stride=(1,1)),        # conv3
            nn.BatchNorm2d(64),
            nn.PReLU(),  # prelu3
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(2, 2), stride=(2, 2)),  
            nn.BatchNorm2d(64),
            nn.PReLU(),  # prelu3
            nn.Conv2d(64, 128,kernel_size=(2,2), stride=(1,1)),  # conv4
            nn.PReLU(),  # prelu4
        )
        self.conv5 = nn.Linear(128 * 3 * 3, 256)  # conv5
        self.prelu5 = nn.PReLU()                 # prelu5
        # detection
        self.conv6_1 = nn.Linear(256, 1)
        # bounding box regression
        self.conv6_2 = nn.Linear(256, 4)

    def forward(self, x):
        # backend
        x = self.pre_layer(x)
        x = x.reshape(x.size(0), -1)
        x = self.conv5(x)
        x = self.prelu5(x)
        # detection
        label = torch.sigmoid(self.conv6_1(x)) # 置信度
        offset = self.conv6_2(x)          # 偏移量

        return label, offset


if __name__ == '__main__':

    x = torch.randn(2,3,12,12)
    x2 = torch.randn(2,3,24,24)
    x3 = torch.randn(2,3,48,48)
    model1 = PNet()
    print(model1(x)[0].shape)
    print(model1(x)[1].shape)
    model2 = RNet()
    print(model2(x2)[0].shape)
    print(model2(x2)[1].shape)
    model3 = ONet()
    print(model3(x3)[0].shape)
    print(model3(x3)[1].shape)
    print(model1)
    print(model2)
    print(model3)
    print(list(model1.pre_layer[0].weight))

数据生成celeba
import os
import traceback

import numpy as np
from PIL import Image, ImageDraw

from tools import utils


class GenerateData():

    def __init__(self, anno_src=r"anno.txt",
                 imgs_path=r"F:\CelebA\Img\img_celeba\img_celeba",
                 save_path="D:\DataSet"
                 ):

        self.anno_src = anno_src
        self.imgs_path = imgs_path
        self.save_path = save_path

        if not os.path.exists(self.save_path):
            os.makedirs(self.save_path)

    def run(self, size=12):
        print("gen %i image" % size)  # %i:十进制数占位符
        for face_size in [size]:
            # “样本图片”存储路径--image
            positive_image_dir = os.path.join(self.save_path, str(face_size), "positive")  # 三级文件路径
            negative_image_dir = os.path.join(self.save_path, str(face_size), "negative")
            part_image_dir = os.path.join(self.save_path, str(face_size), "part")

            print(positive_image_dir, negative_image_dir, part_image_dir)
            for dir_path in [positive_image_dir, negative_image_dir, part_image_dir]:
                if not os.path.exists(dir_path):  # 如果文件不存在则创建文件路径
                    os.makedirs(dir_path)

            # “样本标签”存储路径--text
            positive_anno_filename = os.path.join(self.save_path, str(face_size), "positive.txt")  # 创建正样本txt文件
            negative_anno_filename = os.path.join(self.save_path, str(face_size), "negative.txt")
            part_anno_filename = os.path.join(self.save_path, str(face_size), "part.txt")

            # 计数初始值:给文件命名
            positive_count = 0  # 计数器初始值
            negative_count = 0
            part_count = 0
            # 凡是文件操作,最好try一下,防止程序出错奔溃
            try:
                positive_anno_file = open(positive_anno_filename, "w")  # 以写入的模式打开txt文档
                negative_anno_file = open(negative_anno_filename, "w")
                part_anno_file = open(part_anno_filename, "w")
                for i, line in enumerate(open(self.anno_src)):  # 枚举出所有信息
                    if i < 2:
                        continue  # i小于2时继续读文件readlines
                    # print(i,line)
                    try:
                        # print(line)
                        strs = line.strip().split(" ")  # strip删除两边的空格
                        # print(strs)
                        # print(strs)
                        image_filename = strs[0].strip()
                        # print(image_filename)
                        image_file = os.path.join(self.imgs_path, image_filename)  # 创建文件绝对路径

                        with Image.open(image_file) as img:


                            img_w, img_h = img.size
                            x1 = float(strs[1].strip())  # 取2nd个值去除两边的空格,再转车float型
                            y1 = float(strs[2].strip())
                            w = float(strs[3].strip())
                            h = float(strs[4].strip())
                            x2 = float(x1 + w)
                            y2 = float(y1 + h)
                            px1 = float(strs[5].strip())  # 人的五官
                            py1 = float(strs[6].strip())
                            px2 = float(strs[7].strip())
                            py2 = float(strs[8].strip())
                            px3 = float(strs[9].strip())
                            py3 = float(strs[10].strip())
                            px4 = float(strs[11].strip())
                            py4 = float(strs[12].strip())
                            px5 = float(strs[13].strip())
                            py5 = float(strs[14].strip())

                            # 过滤字段,去除不符合条件的坐标
                            if max(w, h) < 40 or x1 < 0 or y1 < 0 or w < 0 or h < 0:
                                continue
                            # 标注不太标准:给人脸框与适当的偏移★
                            x1 = int(x1 + w * 0.12)  # 原来的坐标给与适当的偏移:偏移人脸框的0.15倍
                            y1 = int(y1 + h * 0.1)
                            x2 = int(x1 + w * 0.9)
                            y2 = int(y1 + h * 0.85)
                            w = int(x2 - x1)  # 偏移后框的实际宽度
                            h = int(y2 - y1)
                            boxes = [[x1, y1, x2, y2]]  # 左上角和右下角四个坐标点;二维的框有批次概念

                            # draw = ImageDraw.Draw(img)
                            # draw.rectangle(boxes[0],)
                            # img.show()

                            # 计算出人脸中心点位置:框的中心位置
                            cx = x1 + w / 2
                            cy = y1 + h / 2
                            # 使正样本和部分样本数量翻倍以图片中心点随机偏移
                            for _ in range(2):  # 每个循环5次,画五个框框、抠出来
                                # 让人脸中心点有少许的偏移
                                w_ = np.random.randint(-w * 0.1, w * 0.1)  # 框的横向偏移范围:向左、向右移动了20%
                                h_ = np.random.randint(-h * 0.1, h * 0.1)
                                cx_ = cx + w_
                                cy_ = cy + h_

                                # 让人脸形成正方形(12*12,24*24,48*48),并且让坐标也有少许的偏离
                                side_len = np.random.randint(int(min(w, h) * 0.8), np.ceil(1.25 * max(w, h)))
                                # 边长偏移的随机数的范围;ceil大于等于该值的最小整数(向上取整);原0.8


                                x1_ = np.max(cx_ - side_len / 2, 0)  # 坐标点随机偏移
                                y1_ = np.max(cy_ - side_len / 2, 0)
                                x2_ = x1_ + side_len
                                y2_ = y1_ + side_len

                                crop_box = np.array([x1_, y1_, x2_, y2_])  # 偏移后的新框
                                # draw.rectangle(list(crop_box))


                                # 计算坐标的偏移值
                                offset_x1 = (x1 - x1_) / side_len  # 偏移量△δ=(x1-x1_)/side_len;新框的宽度;
                                offset_y1 = (y1 - y1_) / side_len
                                offset_x2 = (x2 - x2_) / side_len
                                offset_y2 = (y2 - y2_) / side_len

                                offset_px1 = (px1 - x1_) / side_len  # 人的五官特征的偏移值
                                offset_py1 = (py1 - y1_) / side_len
                                offset_px2 = (px2 - x1_) / side_len
                                offset_py2 = (py2 - y1_) / side_len
                                offset_px3 = (px3 - x1_) / side_len
                                offset_py3 = (py3 - y1_) / side_len
                                offset_px4 = (px4 - x1_) / side_len
                                offset_py4 = (py4 - y1_) / side_len
                                offset_px5 = (px5 - x1_) / side_len
                                offset_py5 = (py5 - y1_) / side_len

                                # 剪切下图片,并进行大小缩放
                                face_crop = img.crop(crop_box)  # “抠图”,crop剪下框出的图像
                                face_resize = face_crop.resize((face_size, face_size),
                                                               Image.ANTIALIAS)  # ★按照人脸尺寸(“像素矩阵大小”)进行缩放:12/24/48;坐标没放缩

                                iou = utils.iou(crop_box, np.array(boxes))[0]  # 抠出来的框和原来的框计算IOU
                                if iou > 0.65:  # 正样本;原为0.65
                                    positive_anno_file.write(
                                        "positive/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
                                            positive_count, 1, offset_x1, offset_y1,
                                            offset_x2, offset_y2, offset_px1, offset_py1, offset_px2, offset_py2,
                                            offset_px3,
                                            offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))
                                    positive_anno_file.flush()  # flush:将缓存区的数据写入文件
                                    face_resize.save(
                                        os.path.join(positive_image_dir, "{0}.jpg".format(positive_count)))  # 保存
                                    positive_count += 1
                                elif iou > 0.4:  # 部分样本;原为0.4
                                    part_anno_file.write(
                                        "part/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
                                            part_count, 2, offset_x1, offset_y1, offset_x2,
                                            offset_y2, offset_px1, offset_py1, offset_px2, offset_py2, offset_px3,
                                            offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))  # 写入txt文件
                                    part_anno_file.flush()
                                    face_resize.save(os.path.join(part_image_dir, "{0}.jpg".format(part_count)))
                                    part_count += 1
                                elif iou < 0.29:  # ★这样生成的负样本很少;原为0.3
                                    negative_anno_file.write(
                                        "negative/{0}.jpg {1} 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n".format(negative_count, 0))
                                    negative_anno_file.flush()
                                    face_resize.save(os.path.join(negative_image_dir, "{0}.jpg".format(negative_count)))
                                    negative_count += 1

                            # 生成负样本
                            _boxes = np.array(boxes)
                            for i in range(2):  # 数量一般和前面保持一样
                                side_len = np.random.randint(face_size, min(img_w, img_h) / 2)
                                x_ = np.random.randint(0, img_w - side_len)
                                y_ = np.random.randint(0, img_h - side_len)
                                crop_box = np.array([x_, y_, x_ + side_len, y_ + side_len])

                                if np.max(utils.iou(crop_box, _boxes)) < 0.29:  # 在加IOU进行判断:保留小于0.3的那一部分;原为0.3
                                    face_crop = img.crop(crop_box)  # 抠图
                                    face_resize = face_crop.resize((face_size, face_size),
                                                                   Image.ANTIALIAS)  # ANTIALIAS:平滑,抗锯齿

                                    negative_anno_file.write(
                                        "negative/{0}.jpg {1} 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n".format(negative_count, 0))
                                    negative_anno_file.flush()
                                    face_resize.save(os.path.join(negative_image_dir, "{0}.jpg".format(negative_count)))
                                    negative_count += 1

                    except Exception as e:
                        print(e)
                        traceback.print_exc()

            except Exception as e:
                print(e)
                # 关闭写入文件
            finally:

                positive_anno_file.close()  # 关闭正样本txt件

                negative_anno_file.close()

                part_anno_file.close()


if __name__ == '__main__':
    data = GenerateData()
    data.run(size=12)
    data.run(size=24)
    data.run(size=48)

widerface 数据代码

import os
import traceback

import numpy as np
from PIL import Image, ImageDraw

from tools import utils

def gentxt():
    imgs_path = r"F:\widerface\WIDER_train\images"
    bbox_txt = r"F:\widerface\wider_face_split\wider_face_train_bbx_gt.txt"
    with open(bbox_txt,"r") as f:
        data = f.readlines()

    empty_dict = {}
    temp_name = None
    for i,line in enumerate(data):
        # i = i.strip()
        if line.strip().endswith("jpg"):
            line = line.strip()
            empty_dict[line] = []
            temp_name = line
        else:
            line = line.strip()
            if len(line)>10:
                # print(line.split()[:4])
                empty_dict[temp_name].append(line.split()[:4])

    with open("wider_anno.txt","w") as f:
        for key in empty_dict.keys():
            values = empty_dict[key]
            f.write(f"{key} ")
            for value in values:
                # print(value)
                # print(" ".join(value))
                f.write(" ".join(value))
                f.write(" ")
            f.write("\n")
            # exit()
            # f.write(f"{key}",)


class GenerateData():

    def __init__(self, anno_src=r"wider_anno.txt",
                 imgs_path=r"F:\widerface\WIDER_train\images",
                 save_path="D:\DataSet\wider"
                 ):

        self.anno_src = anno_src
        self.imgs_path = imgs_path
        self.save_path = save_path

        if not os.path.exists(self.save_path):
            os.makedirs(self.save_path)

    def run(self, size=12):
        print("gen %i image" % size)  # %i:十进制数占位符
        for face_size in [size]:
            # “样本图片”存储路径--image
            positive_image_dir = os.path.join(self.save_path, str(face_size), "positive")  # 三级文件路径
            negative_image_dir = os.path.join(self.save_path, str(face_size), "negative")
            part_image_dir = os.path.join(self.save_path, str(face_size), "part")

            print(positive_image_dir, negative_image_dir, part_image_dir)
            for dir_path in [positive_image_dir, negative_image_dir, part_image_dir]:
                if not os.path.exists(dir_path):  # 如果文件不存在则创建文件路径
                    os.makedirs(dir_path)

            # “样本标签”存储路径--text
            positive_anno_filename = os.path.join(self.save_path, str(face_size), "positive.txt")  # 创建正样本txt文件
            negative_anno_filename = os.path.join(self.save_path, str(face_size), "negative.txt")
            part_anno_filename = os.path.join(self.save_path, str(face_size), "part.txt")

            # 计数初始值:给文件命名
            positive_count = 0  # 计数器初始值
            negative_count = 0
            part_count = 0
            # 凡是文件操作,最好try一下,防止程序出错奔溃
            try:
                positive_anno_file = open(positive_anno_filename, "w")  # 以写入的模式打开txt文档
                negative_anno_file = open(negative_anno_filename, "w")
                part_anno_file = open(part_anno_filename, "w")
                for i, line in enumerate(open(self.anno_src)):  # 枚举出所有信息

                    try:
                        # print(line)
                        strs = line.strip().split(" ")  # strip删除两边的空格
                        # print(strs)
                        # print(strs)
                        image_filename = strs[0].strip()
                        # print(image_filename)
                        image_file = os.path.join(self.imgs_path, image_filename)  # 创建文件绝对路径
                        values = list(map(float, strs[1:]))
                        all_boxes = []

                        for index in range(0, len(values), 4):
                            all_boxes.append(values[index:index + 4])

                        with Image.open(image_file) as img:
                            for one_box in all_boxes:
                                img_w, img_h = img.size
                                x1 = one_box[0]# float(strs[1].strip())  # 取2nd个值去除两边的空格,再转车float型
                                y1 = one_box[1] # float(strs[2].strip())
                                w =one_box[2]#  float(strs[3].strip())
                                h =one_box[3]# float(strs[4].strip())
                                x2 = float(x1 + w)
                                y2 = float(y1 + h)

                                # draw = ImageDraw.Draw(img)
                                # draw.rectangle([x1,y1,x2,y2])
                                # img.show()
                                # exit()
                                px1 = 0#float(strs[5].strip())  # 人的五官
                                py1 = 0#float(strs[6].strip())
                                px2 =0# float(strs[7].strip())
                                py2 =0# float(strs[8].strip())
                                px3 =0# float(strs[9].strip())
                                py3 =0# float(strs[10].strip())
                                px4 =0# float(strs[11].strip())
                                py4 =0# float(strs[12].strip())
                                px5 =0# float(strs[13].strip())
                                py5 =0# float(strs[14].strip())

                                # 过滤字段,去除不符合条件的坐标
                                if max(w, h) < 40 or x1 < 0 or y1 < 0 or w < 0 or h < 0:
                                    continue

                                # x1 = int(x1 + w)  # 原来的坐标给与适当的偏移:偏移人脸框的0.15倍
                                # y1 = int(y1 + h)
                                # x2 = int(x1 + w)
                                # y2 = int(y1 + h)
                                # w = int(x2 - x1)  # 偏移后框的实际宽度
                                # h = int(y2 - y1)
                                boxes = [[x1, y1, x2, y2]]  # 左上角和右下角四个坐标点;二维的框有批次概念
                                # print(boxes)
                                # # exit()

                                # 计算出人脸中心点位置:框的中心位置
                                cx = x1 + w / 2
                                cy = y1 + h / 2
                                # 使正样本和部分样本数量翻倍以图片中心点随机偏移
                                for _ in range(1):  # 每个循环5次,画五个框框、抠出来
                                    # 让人脸中心点有少许的偏移
                                    # print(-w * 0.2, w * 0.2)
                                    w_ = np.random.randint(-w * 0.2, w * 0.2)  # 框的横向偏移范围:向左、向右移动了20%
                                    h_ = np.random.randint(-h * 0.2, h * 0.2)
                                    cx_ = cx + w_
                                    cy_ = cy + h_

                                    # 让人脸形成正方形(12*12,24*24,48*48),并且让坐标也有少许的偏离
                                    side_len = np.random.randint(int(min(w, h) * 0.8), np.ceil(1.25 * max(w, h)))
                                    # 边长偏移的随机数的范围;ceil大于等于该值的最小整数(向上取整);原0.8


                                    x1_ = np.max(cx_ - side_len / 2, 0)  # 坐标点随机偏移
                                    y1_ = np.max(cy_ - side_len / 2, 0)
                                    x2_ = x1_ + side_len
                                    y2_ = y1_ + side_len

                                    crop_box = np.array([x1_, y1_, x2_, y2_])  # 偏移后的新框
                                    # draw.rectangle(list(crop_box))
                                    # img.show()
                                    # exit()


                                    # 计算坐标的偏移值
                                    offset_x1 = (x1 - x1_) / side_len  # 偏移量△δ=(x1-x1_)/side_len;新框的宽度;
                                    offset_y1 = (y1 - y1_) / side_len
                                    offset_x2 = (x2 - x2_) / side_len
                                    offset_y2 = (y2 - y2_) / side_len

                                    offset_px1 =0#  (px1 - x1_) / side_len  # 人的五官特征的偏移值
                                    offset_py1 =0 #  (py1 - y1_) / side_len
                                    offset_px2 =0 #(px2 - x1_) / side_len
                                    offset_py2 =0# (py2 - y1_) / side_len
                                    offset_px3 =0# (px3 - x1_) / side_len
                                    offset_py3 = 0#(py3 - y1_) / side_len
                                    offset_px4 =0# (px4 - x1_) / side_len
                                    offset_py4 = 0#(py4 - y1_) / side_len
                                    offset_px5 = 0#(px5 - x1_) / side_len
                                    offset_py5 = 0#(py5 - y1_) / side_len

                                    # 剪切下图片,并进行大小缩放
                                    face_crop = img.crop(crop_box)  # “抠图”,crop剪下框出的图像
                                    face_resize = face_crop.resize((face_size, face_size),
                                                                   Image.ANTIALIAS)  # ★按照人脸尺寸(“像素矩阵大小”)进行缩放:12/24/48;坐标没放缩

                                    iou = utils.iou(crop_box, np.array(boxes))[0]  # 抠出来的框和原来的框计算IOU
                                    if iou > 0.65:  # 正样本;原为0.65
                                        positive_anno_file.write(
                                            "positive/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
                                                positive_count, 1, offset_x1, offset_y1,
                                                offset_x2, offset_y2, offset_px1, offset_py1, offset_px2, offset_py2,
                                                offset_px3,
                                                offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))
                                        positive_anno_file.flush()  # flush:将缓存区的数据写入文件
                                        face_resize.save(
                                            os.path.join(positive_image_dir, "{0}.jpg".format(positive_count)))  # 保存
                                        positive_count += 1
                                    elif iou > 0.4:  # 部分样本;原为0.4
                                        part_anno_file.write(
                                            "part/{0}.jpg {1} {2} {3} {4} {5} {6} {7} {8} {9} {10} {11} {12} {13} {14} {15}\n".format(
                                                part_count, 2, offset_x1, offset_y1, offset_x2,
                                                offset_y2, offset_px1, offset_py1, offset_px2, offset_py2, offset_px3,
                                                offset_py3, offset_px4, offset_py4, offset_px5, offset_py5))  # 写入txt文件
                                        part_anno_file.flush()
                                        face_resize.save(os.path.join(part_image_dir, "{0}.jpg".format(part_count)))
                                        part_count += 1
                                    elif iou < 0.29:  # ★这样生成的负样本很少;原为0.3
                                        negative_anno_file.write(
                                            "negative/{0}.jpg {1} 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n".format(negative_count, 0))
                                        negative_anno_file.flush()
                                        face_resize.save(os.path.join(negative_image_dir, "{0}.jpg".format(negative_count)))
                                        negative_count += 1

                                # # 生成负样本
                                # _boxes = np.array(boxes)
                                # for i in range(2):  # 数量一般和前面保持一样
                                #     side_len = np.random.randint(face_size, min(img_w, img_h) / 2)
                                #     x_ = np.random.randint(0, img_w - side_len)
                                #     y_ = np.random.randint(0, img_h - side_len)
                                #     crop_box = np.array([x_, y_, x_ + side_len, y_ + side_len])
                                #
                                #     if np.max(utils.iou(crop_box, _boxes)) < 0.29:  # 在加IOU进行判断:保留小于0.3的那一部分;原为0.3
                                #         face_crop = img.crop(crop_box)  # 抠图
                                #         face_resize = face_crop.resize((face_size, face_size),
                                #                                        Image.ANTIALIAS)  # ANTIALIAS:平滑,抗锯齿
                                #
                                #         negative_anno_file.write(
                                #             "negative/{0}.jpg {1} 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n".format(negative_count, 0))
                                #         negative_anno_file.flush()
                                #         face_resize.save(os.path.join(negative_image_dir, "{0}.jpg".format(negative_count)))
                                #         negative_count += 1

                    except Exception as e:
                        print(e)
                        traceback.print_exc()

            except Exception as e:
                print(e)
                # 关闭写入文件
            finally:

                positive_anno_file.close()  # 关闭正样本txt件

                negative_anno_file.close()

                part_anno_file.close()


if __name__ == '__main__':
    data = GenerateData()
    data.run(size=12)
    data.run(size=24)
    data.run(size=48)


数据代码

  • 采用了数据增强,
  • 在训练过程中发现,图像会把一些手或者菜当成人头,着实可怕,这是因为celeba数据中人物手把头挡住的问题,且黄皮肤居多,部分人物光暗。因此采用颜色变换增强,对比度,颜色。
  • 在训练过程中发现,侧脸不容易识别,,因为对不少的数据集采用图像镜像实现侧脸翻转,效果不错。解决问题
# 创建数据集
from torch.utils.data import Dataset
import os
import numpy as np
import torch
from PIL import Image
from torchvision import transforms
tf1 = transforms.Compose(
    [transforms.ColorJitter(brightness=0.5),
     transforms.RandomHorizontalFlip(p=0.5)
     ]
)
tf2 = transforms.Compose(
    [transforms.ColorJitter(contrast=0.5),
     transforms.RandomHorizontalFlip(p=0.5)
     ]
)
tf3 = transforms.Compose(
    [transforms.ColorJitter(saturation=0.5),
     transforms.RandomHorizontalFlip(p=0.5)
     ]
)
tf4 = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5)
])

tf = transforms.RandomChoice(
    [
        tf1,tf2,tf3,tf4
    ]
)
# 数据集
class FaceDataset(Dataset):
    def __init__(self,path_1=r"D:\DataSet",path_2="D:\DataSet\wider",size=12,tf=tf):
        super(FaceDataset, self).__init__()
        self.dataset = []
        self.size = size
        for path in [path_1,path_2]:
            self.base_path_1 = path
            self.path = os.path.join(self.base_path_1,str(self.size))
            for txt in ["positive.txt","negative.txt","part.txt"]:
                with open(os.path.join(self.path,txt),"r") as f:
                    data = f.readlines()
                for line in data:
                    line = line.strip().split()
                    img_path = os.path.join(self.path,line[0])
                    benkdata = " ".join(line[1:])
                    self.dataset.append([img_path,benkdata])

        self.tf = tf

    def __len__(self):
        return len(self.dataset) # 数据集长度

    def __getitem__(self, index): # 获取数据


        img_path,strs = self.dataset[index]
        strs = strs.strip().split(" ") # 取一条数据,去掉前后字符串,再按空格分割
        #标签:置信度+偏移量
        cond = torch.Tensor([int(strs[0])]) # []莫丢,否则指定的是shape
        offset = torch.Tensor([float(strs[1]),float(strs[2]),float(strs[3]),float(strs[4])])
        #样本:img_data
        # img_path = os.path.join(self.path,strs[0]) # 图片绝对路径
        img = Image.open(img_path)

        img = self.tf(img)
        # img.show()
        img = np.array(img) / 255. - 0.5
        img_data = torch.tensor(img,dtype=torch.float32)  # 打开-->array-->归一化去均值化-->转成tensor
        img_data = img_data.permute(2,0,1) # CWH

        # print(img_data.shape) # WHC
        # a = img_data.permute(2,0,1) #轴变换
        # print(a.shape) #[3, 48, 48]:CWH
        return img_data,cond,offset

# 测试
if __name__ == '__main__':


    dataset = FaceDataset(size=12)
    print(dataset[0])
    print(len(dataset))

###网络训练

训练代码

增加了余弦退火法的训练方式,采用smoothL1回归坐标点,BCELoss 分类损失
p 网络训练

# 创建训练器----以训练三个网络
import os
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
from torch.utils.data import DataLoader
import torch
from torch import nn
import torch.optim as optim
from sampling import FaceDataset # 导入数据集
from models import models
from torch.utils.tensorboard import SummaryWriter
# 创建训练器

class Trainer:
    def __init__(self, net, save_path, dataset_size, isCuda=True,SummaryWriter_path=r"run"): # 网络,参数保存路径,训练数据路径,cuda加速为True
        self.net = net
        self.save_path = save_path

        self.dataset_path = dataset_size
        self.isCuda = isCuda
        # print(self.net.name)
        # self.net.name
        summaryWriter_path = os.path.join(SummaryWriter_path,self.net.name)

        if not os.path.exists(summaryWriter_path):
            os.makedirs(summaryWriter_path)

        length = len(os.listdir(summaryWriter_path))
        path_name = os.path.join(summaryWriter_path, "exp" + str(length))
        os.makedirs(path_name)

        self.summaryWriter = SummaryWriter(path_name)

        if self.isCuda:      # 默认后面有个else
            self.net.cuda() # 给网络加速

        # 创建损失函数
        # 置信度损失
        self.cls_loss_fn = nn.BCELoss() # ★二分类交叉熵损失函数,是多分类交叉熵(CrossEntropyLoss)的一个特例;用BCELoss前面必须用sigmoid激活,用CrossEntropyLoss前面必须用softmax函数
        # 偏移量损失
        self.offset_loss_fn = nn.SmoothL1Loss()

        # 创建优化器
        self.optimizer = optim.SGD(self.net.parameters(),lr=0.0001,momentum=0.9)

        # 恢复网络训练---加载模型参数,继续训练
        if os.path.exists(self.save_path): # 如果文件存在,接着继续训练
            net.load_state_dict(torch.load(self.save_path),strict=False)

    # 训练方法
    def train(self,epochs=1000):

        faceDataset = FaceDataset(size=self.dataset_path) # 数据集
        dataloader = DataLoader(faceDataset, batch_size=256, shuffle=True, num_workers=4,drop_last=True) # 数据加载器
        #num_workers=4:有4个线程在加载数据(加载数据需要时间,以防空置);drop_last:为True时表示,防止批次不足报错。
        scheduler = CosineAnnealingWarmRestarts(self.optimizer, T_0=5, T_mult=1)

        self.best_loss = 1

        for epoch in range(epochs):
            for i, (img_data_, category_, offset_) in enumerate(dataloader): # 样本,置信度,偏移量
                if self.isCuda:                    # cuda把数据读到显存里去了(先经过内存);没有cuda在内存,有cuda在显存
                    img_data_ = img_data_.cuda()  # [512, 3, 12, 12]
                    category_ = category_.cuda() # 512, 1]
                    offset_ = offset_.cuda()    # [512, 4]

                # 网络输出
                _output_category, _output_offset = self.net(img_data_) # 输出置信度,偏移量

                # print(_output_category.shape)     # [512, 1, 1, 1]
                # print(_output_offset.shape)       # [512, 4, 1, 1]
                output_category = _output_category.reshape(-1, 1) # [512,1]
                output_offset = _output_offset.reshape(-1, 4)     # [512,4]
                # output_landmark = _output_landmark.view(-1, 10)

                # 计算分类的损失----置信度
                category_mask = torch.lt(category_, 2)  # 对置信度小于2的正样本(1)和负样本(0)进行掩码; ★部分样本(2)不参与损失计算;符合条件的返回1,不符合条件的返回0
                category = torch.masked_select(category_, category_mask)              # 对“标签”中置信度小于2的选择掩码,返回符合条件的结果
                output_category = torch.masked_select(output_category, category_mask) # 预测的“标签”进掩码,返回符合条件的结果
                cls_loss = self.cls_loss_fn(output_category, category)                # 对置信度做损失

                # 计算bound回归的损失----偏移量
                offset_mask = torch.gt(category_, 0)  # 对置信度大于0的标签,进行掩码;★负样本不参与计算,负样本没偏移量;[512,1]
                offset_index = torch.nonzero(offset_mask)[:, 0]  # 选出非负样本的索引;[244]
                offset = offset_[offset_index]                   # 标签里饿偏移量;[244,4]
                output_offset = output_offset[offset_index]      # 输出的偏移量;[244,4]
                offset_loss = self.offset_loss_fn(output_offset, offset)  # 偏移量损失

                #总损失
                loss = cls_loss + offset_loss
                # 反向传播,优化网络
                self.optimizer.zero_grad() # 清空之前的梯度
                loss.backward()           # 计算梯度
                self.optimizer.step()    # 优化网络

                #输出损失:loss-->gpu-->cup(变量)-->tensor-->array
                print("epoch=",epoch ,"loss:", loss.cpu().data.numpy(), " cls_loss:", cls_loss.cpu().data.numpy(), " offset_loss",
                      offset_loss.cpu().data.numpy())


                self.summaryWriter.add_scalars("loss", {"loss": loss.cpu().data.numpy(),
                                                        "cls_loss": cls_loss.cpu().data.numpy(),
                                                        "offser_loss": offset_loss.cpu().data.numpy()},epoch)
                # self.summaryWriter.add_histogram("pre_conv_layer1",self.net.pre_layer[0].weight,epoch)
                # self.summaryWriter.add_histogram("pre_conv_layer2",self.net.pre_layer[3].weight,epoch)
                # self.summaryWriter.add_histogram("pre_conv_layer3",self.net.pre_layer[5].weight,epoch)


                # 保存
                if i%5==0:
                    if i%500==0:
                        torch.save(self.net.state_dict(), self.save_path)  # state_dict保存网络参数,save_path参数保存路径
                        print("save success")  # 每轮次保存一次;最好做一判断:损失下降时保存一次
                    if loss.cpu().data.numpy()<self.best_loss:
                        self.best_loss = loss.cpu().data.numpy()
                        torch.save(self.net.state_dict(), self.save_path) # state_dict保存网络参数,save_path参数保存路径
                        print("save success")# 每轮次保存一次;最好做一判断:损失下降时保存一次

            scheduler.step()

if __name__ == '__main__':
    
    net = models.PNet()
    trainer = Trainer(net, 'pnet.pt', dataset_size=12) # 网络,保存参数,训练数据;创建训器
    trainer.train()                                                     # 调用训练器中的train方法
    # net = models.RNet()
    # trainer = Trainer(net, 'rnet.pt', r"D:\DataSet\24") # 网络,保存参数,训练数据;创建训器
    # trainer.train()
    # net = models.ONet()
    # trainer = Trainer(net, 'onet.pt', r"D:\DataSet\48") # 网络,保存参数,训练数据;创建训器
    # trainer.train()


R网络训练

# 创建训练器----以训练三个网络


import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
from torch.utils.data import DataLoader
import torch
from torch import nn
import torch.optim as optim
from sampling import FaceDataset  # 导入数据集
from models import models
from torch.utils.tensorboard import SummaryWriter


# 创建训练器
class Trainer:
    def __init__(self, net, save_path, dataset_path, isCuda=True,
                 SummaryWriter_path=r"run"):  # 网络,参数保存路径,训练数据路径,cuda加速为True
        self.net = net
        self.save_path = save_path
        self.dataset_path = dataset_path
        self.isCuda = isCuda
        # print(self.net.name)
        # self.net.name
        summaryWriter_path = os.path.join(SummaryWriter_path, self.net.name)

        if not os.path.exists(summaryWriter_path):
            os.makedirs(summaryWriter_path)

        length = len(os.listdir(summaryWriter_path))
        path_name = os.path.join(summaryWriter_path, "exp" + str(length))
        os.makedirs(path_name)

        self.summaryWriter = SummaryWriter(path_name)

        if self.isCuda:  # 默认后面有个else
            self.net.cuda()  # 给网络加速

        # 创建损失函数
        # 置信度损失
        self.cls_loss_fn = nn.BCELoss()  # ★二分类交叉熵损失函数,是多分类交叉熵(CrossEntropyLoss)的一个特例;用BCELoss前面必须用sigmoid激活,用CrossEntropyLoss前面必须用softmax函数
        # 偏移量损失
        self.offset_loss_fn = nn.SmoothL1Loss()

        # 创建优化器
        self.optimizer = optim.SGD(self.net.parameters(),lr=0.0001,momentum=0.8)

        # 恢复网络训练---加载模型参数,继续训练
        if os.path.exists(self.save_path):  # 如果文件存在,接着继续训练
            net.load_state_dict(torch.load(self.save_path),strict=False)

    # 训练方法
    def train(self):
        faceDataset = FaceDataset(size=self.dataset_path)  # 数据集
        dataloader = DataLoader(faceDataset, batch_size=128, shuffle=True, num_workers=2, drop_last=True)  # 数据加载器
        # num_workers=4:有4个线程在加载数据(加载数据需要时间,以防空置);drop_last:为True时表示,防止批次不足报错。
        self.best_loss = None
        while True:
            for i, (img_data_, category_, offset_) in enumerate(dataloader):  # 样本,置信度,偏移量
                if self.isCuda:  # cuda把数据读到显存里去了(先经过内存);没有cuda在内存,有cuda在显存
                    img_data_ = img_data_.cuda()  # [512, 3, 12, 12]
                    category_ = category_.cuda()  # 512, 1]
                    offset_ = offset_.cuda()  # [512, 4]

                # 网络输出
                _output_category, _output_offset = self.net(img_data_)  # 输出置信度,偏移量

                # print(_output_category.shape)     # [512, 1, 1, 1]
                # print(_output_offset.shape)       # [512, 4, 1, 1]
                output_category = _output_category.view(-1, 1)  # [512,1]
                output_offset = _output_offset.view(-1, 4)  # [512,4]
                # output_landmark = _output_landmark.view(-1, 10)

                # 计算分类的损失----置信度
                category_mask = torch.lt(category_, 2)  # 对置信度小于2的正样本(1)和负样本(0)进行掩码; ★部分样本(2)不参与损失计算;符合条件的返回1,不符合条件的返回0
                category = torch.masked_select(category_, category_mask)  # 对“标签”中置信度小于2的选择掩码,返回符合条件的结果
                output_category = torch.masked_select(output_category, category_mask)  # 预测的“标签”进掩码,返回符合条件的结果
                cls_loss = self.cls_loss_fn(output_category, category)  # 对置信度做损失

                # 计算bound回归的损失----偏移量
                offset_mask = torch.gt(category_, 0)  # 对置信度大于0的标签,进行掩码;★负样本不参与计算,负样本没偏移量;[512,1]
                offset_index = torch.nonzero(offset_mask)[:, 0]  # 选出非负样本的索引;[244]
                offset = offset_[offset_index]  # 标签里饿偏移量;[244,4]
                output_offset = output_offset[offset_index]  # 输出的偏移量;[244,4]
                offset_loss = self.offset_loss_fn(output_offset, offset)  # 偏移量损失

                # 总损失
                loss = 0.5*cls_loss + offset_loss

                if i == 0:
                    self.best_loss = loss.cpu().data.numpy()
                # 反向传播,优化网络
                self.optimizer.zero_grad()  # 清空之前的梯度
                loss.backward()  # 计算梯度
                self.optimizer.step()  # 优化网络

                # 输出损失:loss-->gpu-->cup(变量)-->tensor-->array
                print("i=", i, "loss:", loss.cpu().data.numpy(), " cls_loss:", cls_loss.cpu().data.numpy(),
                      " offset_loss",
                      offset_loss.cpu().data.numpy())

                self.summaryWriter.add_scalars("loss", {"loss": loss.cpu().data.numpy(),
                                                        "cls_loss": cls_loss.cpu().data.numpy(),
                                                        "offser_loss": offset_loss.cpu().data.numpy()},i)
                # self.summaryWriter.add_histogram("pre_conv_layer1", self.net.pre_layer[0].weight,i)
                # self.summaryWriter.add_histogram("pre_conv_layer2", self.net.pre_layer[3].weight,i)
                # self.summaryWriter.add_histogram("pre_conv_layer3", self.net.pre_layer[6].weight,i)
                # 保存
                if (i + 1) % 100 == 0 or self.best_loss>loss.cpu().data.numpy():
                    self.best_loss = loss.cpu().data.numpy()
                    torch.save(self.net.state_dict(), self.save_path)  # state_dict保存网络参数,save_path参数保存路径
                    print("save success")  # 每轮次保存一次;最好做一判断:损失下降时保存一次


if __name__ == '__main__':

    net = models.RNet()
    trainer = Trainer(net, 'rnet.pt', 24) # 网络,保存参数,训练数据;创建训器
    trainer.train()


O网络训练

# 创建训练器----以训练三个网络


import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
from torch.utils.data import DataLoader
import torch
from torch import nn
import torch.optim as optim
from sampling import FaceDataset  # 导入数据集
from models import models
from torch.utils.tensorboard import SummaryWriter


# 创建训练器
class Trainer:
    def __init__(self, net, save_path, dataset_path, isCuda=True,
                 SummaryWriter_path=r"run"):  # 网络,参数保存路径,训练数据路径,cuda加速为True
        self.net = net
        self.save_path = save_path
        self.dataset_path = dataset_path
        self.isCuda = isCuda
        # print(self.net.name)
        # self.net.name
        summaryWriter_path = os.path.join(SummaryWriter_path, self.net.name)

        if not os.path.exists(summaryWriter_path):
            os.makedirs(summaryWriter_path)

        length = len(os.listdir(summaryWriter_path))
        path_name = os.path.join(summaryWriter_path, "exp" + str(length))
        os.makedirs(path_name)

        self.summaryWriter = SummaryWriter(path_name)

        if self.isCuda:  # 默认后面有个else
            self.net.cuda()  # 给网络加速

        # 创建损失函数
        # 置信度损失
        self.cls_loss_fn = nn.BCELoss()  # ★二分类交叉熵损失函数,是多分类交叉熵(CrossEntropyLoss)的一个特例;用BCELoss前面必须用sigmoid激活,用CrossEntropyLoss前面必须用softmax函数
        # 偏移量损失
        self.offset_loss_fn = nn.SmoothL1Loss()

        # 创建优化器
        self.optimizer = optim.SGD(self.net.parameters(),lr=0.0001,momentum=0.8)

        # 恢复网络训练---加载模型参数,继续训练
        if os.path.exists(self.save_path):  # 如果文件存在,接着继续训练
            net.load_state_dict(torch.load(self.save_path),strict=False)

    # 训练方法
    def train(self):
        faceDataset = FaceDataset(size=self.dataset_path)  # 数据集
        dataloader = DataLoader(faceDataset, batch_size=128, shuffle=True, num_workers=2, drop_last=True)  # 数据加载器
        # num_workers=4:有4个线程在加载数据(加载数据需要时间,以防空置);drop_last:为True时表示,防止批次不足报错。
        self.best_loss = None
        while True:
            for i, (img_data_, category_, offset_) in enumerate(dataloader):  # 样本,置信度,偏移量
                if self.isCuda:  # cuda把数据读到显存里去了(先经过内存);没有cuda在内存,有cuda在显存
                    img_data_ = img_data_.cuda()  # [512, 3, 12, 12]
                    category_ = category_.cuda()  # 512, 1]
                    offset_ = offset_.cuda()  # [512, 4]

                # 网络输出
                _output_category, _output_offset = self.net(img_data_)  # 输出置信度,偏移量

                # print(_output_category.shape)     # [512, 1, 1, 1]
                # print(_output_offset.shape)       # [512, 4, 1, 1]
                output_category = _output_category.view(-1, 1)  # [512,1]
                output_offset = _output_offset.view(-1, 4)  # [512,4]
                # output_landmark = _output_landmark.view(-1, 10)

                # 计算分类的损失----置信度
                category_mask = torch.lt(category_, 2)  # 对置信度小于2的正样本(1)和负样本(0)进行掩码; ★部分样本(2)不参与损失计算;符合条件的返回1,不符合条件的返回0
                category = torch.masked_select(category_, category_mask)  # 对“标签”中置信度小于2的选择掩码,返回符合条件的结果
                output_category = torch.masked_select(output_category, category_mask)  # 预测的“标签”进掩码,返回符合条件的结果
                cls_loss = self.cls_loss_fn(output_category, category)  # 对置信度做损失

                # 计算bound回归的损失----偏移量
                offset_mask = torch.gt(category_, 0)  # 对置信度大于0的标签,进行掩码;★负样本不参与计算,负样本没偏移量;[512,1]
                offset_index = torch.nonzero(offset_mask)[:, 0]  # 选出非负样本的索引;[244]
                offset = offset_[offset_index]  # 标签里饿偏移量;[244,4]
                output_offset = output_offset[offset_index]  # 输出的偏移量;[244,4]
                offset_loss = self.offset_loss_fn(output_offset, offset)  # 偏移量损失

                # 总损失
                loss = 0.5*cls_loss + offset_loss

                if i == 0:
                    self.best_loss = loss.cpu().data.numpy()
                # 反向传播,优化网络
                self.optimizer.zero_grad()  # 清空之前的梯度
                loss.backward()  # 计算梯度
                self.optimizer.step()  # 优化网络

                # 输出损失:loss-->gpu-->cup(变量)-->tensor-->array
                print("i=", i, "loss:", loss.cpu().data.numpy(), " cls_loss:", cls_loss.cpu().data.numpy(),
                      " offset_loss",
                      offset_loss.cpu().data.numpy())

                self.summaryWriter.add_scalars("loss", {"loss": loss.cpu().data.numpy(),
                                                        "cls_loss": cls_loss.cpu().data.numpy(),
                                                        "offser_loss": offset_loss.cpu().data.numpy()},i)
                # self.summaryWriter.add_histogram("pre_conv_layer1", self.net.pre_layer[0].weight,i)
                # self.summaryWriter.add_histogram("pre_conv_layer2", self.net.pre_layer[3].weight,i)
                # self.summaryWriter.add_histogram("pre_conv_layer3", self.net.pre_layer[6].weight,i)
                # 保存
                if (i + 1) % 100 == 0 or self.best_loss>loss.cpu().data.numpy():
                    self.best_loss = loss.cpu().data.numpy()
                    torch.save(self.net.state_dict(), self.save_path)  # state_dict保存网络参数,save_path参数保存路径
                    print("save success")  # 每轮次保存一次;最好做一判断:损失下降时保存一次


if __name__ == '__main__':

    net = models.RNet()
    trainer = Trainer(net, 'rnet.pt', 24) # 网络,保存参数,训练数据;创建训器
    trainer.train()

侦测代码

  • p网络采用全卷积手法的原因,可以输入不同尺度的图片,利用输出特征图进行反算
import os
import time

import cv2

from tools import utils
import numpy as np
import torch
from PIL import Image, ImageDraw
from torchvision import transforms

from models import models


class Detector():
    def __init__(self,
                 pnet_param="pnet.pt",
                 rnet_param="rnet.pt",
                 onet_param="onet.pt",
                 isCuda=True,
                 # 网络调参
                 p_cls=0.6,  # 原为0.6
                 p_nms=0.5,  # 原为0.5
                 r_cls=0.6,  # 原为0.6
                 r_nms=0.5,  # 原为0.5
                 # R网络:
                 o_cls=0.99,  # 原为0.97
                 o_nms=0.6,  # 原为0.7
                 ):


        self.isCuda = isCuda
        self.pnet = models.PNet()  # 创建实例变量,实例化P网络
        self.rnet = models.RNet()
        self.onet = models.ONet()

        if self.isCuda:
            self.pnet.cuda()
            self.rnet.cuda()
            self.onet.cuda()

        self.pnet.load_state_dict(torch.load(pnet_param))  # 把训练好的权重加载到P网络中
        self.rnet.load_state_dict(torch.load(rnet_param))
        self.onet.load_state_dict(torch.load(onet_param))

        self.pnet.eval()  # 训练网络里有BN(批归一化时),要调用eval方法,使用是不用BN,dropout方法
        self.rnet.eval()
        self.onet.eval()

        self.p_cls = p_cls  # 原为0.6
        self.p_nms = p_nms  # 原为0.5
        self.r_cls = r_cls  # 原为0.6
        self.r_nms = r_nms   # 原为0.5
        # R网络:
        self.o_cls = o_cls  # 原为0.97
        self.o_nms = o_nms  # 原为0.7

        self.__image_transform = transforms.Compose(
            [
                transforms.ToTensor()
            ]
        )

    def detect(self, image):
        # P网络检测-----1st
        start_time = time.time()
        pnet_boxes = self.__pnet_detect(image) # 调用__pnet_detect函数(后面定义)
        if pnet_boxes.shape[0]==0:
            return np.array([])
        end_time = time.time()
        t_pnet = end_time - start_time  # P网络所占用的时间差
        # print(pnet_boxes.shape)
        # return pnet_boxes                    # p网络检测出的框

        start_time = time.time()
        rnet_boxes = self.__rnet_detect(image,pnet_boxes)  # 调用__pnet_detect函数(后面定义)
        if rnet_boxes.shape[0] == 0:
            return np.array([])
        end_time = time.time()
        t_rnet = end_time - start_time  # r网络所占用的时间差
        # return rnet_boxes                    # r网络检测出的框

        onet_boxes = self.__onet_detect(image,rnet_boxes)  # 调用__pnet_detect函数(后面定义)
        if rnet_boxes.shape[0] == 0:
            return np.array([])
        end_time = time.time()
        t_onet = end_time - start_time  # P网络所占用的时间差
        # return onet_boxes                    # p网络检测出的框

        # 三网络检测的总时间
        t_sum = t_pnet + t_rnet + t_onet
        print("total:{0} pnet:{1} rnet:{2} onet:{3}".format(t_sum, t_pnet, t_rnet, t_onet))

        return onet_boxes

    def __pnet_detect(self, image):  # ★p网络全部是卷积,与输入图片大小无关,可输出任意形状图片
        boxes = []  # 创建空列表,接收符合条件的建议框

        img = image
        w, h = img.size
        min_side_len = min(w, h)  # 获取图片的最小边长

        scale = 1  # 初始缩放比例(为1时不缩放):得到不同分辨率的图片
        while min_side_len > 12:  # 直到缩放到小于等于12时停止
            img_data = self.__image_transform(img)  # 将图片数组转成张量
            if self.isCuda:
                img_data = img_data.cuda()  # 将图片tensor传到cuda里加速
            img_data.unsqueeze_(0)  # 在“批次”上升维(测试时传的不止一张图片)
            # print("img_data:",img_data.shape) # [1, 3, 416, 500]:C=3,W=416,H=500

            _cls, _offest = self.pnet(img_data)  # ★★返回多个置信度和偏移量
            # print("_cls",_cls.shape)         # [1, 1, 203, 245]:NCWH:分组卷积的特征图的通道和尺寸★
            # print("_offest", _offest.shape) # [1, 4, 203, 245]:NCWH

            cls = _cls[0][0].cpu().data  # [203, 245]:分组卷积特征图的尺寸:W,H
            offest = _offest[0].cpu().data  # [4, 203, 245] # 分组卷积特征图的通道、尺寸:C,W,H
            idxs = torch.nonzero(torch.gt(cls, self.p_cls))  # ★置信度大于0.6的框索引;把P网络输出,看有没没框到的人脸,若没框到人脸,说明网络没训练好;或者置信度给高了、调低
            # print(idxs)
            for idx in idxs:  # 根据索引,依次添加符合条件的框;cls[idx[0], idx[1]]在置信度中取值:idx[0]行索引,idx[1]列索引
                boxes.append(self.__box(idx, offest, cls[idx[0], idx[1]], scale))  # ★调用框反算函数_box(把特征图上的框,反算到原图上去),把大于0.6的框留下来;
            scale *= 0.7  # 缩放图片:循环控制条件
            _w = int(w * scale)  # 新的宽度
            _h = int(h * scale)

            img = img.resize((_w, _h))  # 根据缩放后的宽和高,对图片进行缩放
            min_side_len = min(_w, _h)  # 重新获取最小宽高

        return utils.nms(np.array(boxes), self.p_nms)  # 返回框框,原阈值给p_nms=0.5(iou为0.5),尽可能保留IOU小于0.5的一些框下来,若网络训练的好,值可以给低些

        # 特征反算:将回归量还原到原图上去,根据特征图反算的到原图建议框

    def __box(self, start_index, offset, cls, scale, stride=2, side_len=12):  # p网络池化步长为2

        _x1 = (start_index[1].float() * stride) / scale  # 索引乘以步长,除以缩放比例;★特征反算时“行索引,索引互换”,原为[0]
        _y1 = (start_index[0].float() * stride) / scale
        _x2 = (start_index[1].float() * stride + side_len) / scale
        _y2 = (start_index[0].float() * stride + side_len) / scale

        ow = _x2 - _x1  # 人脸所在区域建议框的宽和高
        oh = _y2 - _y1

        _offset = offset[:, start_index[0], start_index[1]]  # 根据idxs行索引与列索引,找到对应偏移量△δ:[x1,y1,x2,y2]

        x1 = _x1 + ow * _offset[0]  # 根据偏移量算实际框的位置,x1=x1_+w*△δ;生样时为:△δ=x1-x1_/w
        y1 = _y1 + oh * _offset[1]
        x2 = _x2 + ow * _offset[2]
        y2 = _y2 + oh * _offset[3]

        return [x1, y1, x2, y2, cls]  # 正式框:返回4个坐标点和1个偏移量


    def __rnet_detect(self, image, pnet_boxes):

        _img_dataset = []  # 创建空列表,存放抠图
        _pnet_boxes = utils.convert_to_square(pnet_boxes)  # ★给p网络输出的框,找出中心点,沿着最大边长的两边扩充成“正方形”,再抠图
        for _box in _pnet_boxes:  # ★遍历每个框,每个框返回框4个坐标点,抠图,放缩,数据类型转换,添加列表
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])

            img = image.crop((_x1, _y1, _x2, _y2))  # 根据4个坐标点抠图
            img = img.resize((24, 24))  # 放缩在固尺寸
            img_data = self.__image_transform(img)  # 将图片数组转成张量
            _img_dataset.append(img_data)

        img_dataset = torch.stack(_img_dataset)  # stack堆叠(默认在0轴),此处相当数据类型转换,见例子2★
        if self.isCuda:
            img_dataset = img_dataset.cuda()  # 给图片数据采用cuda加速

        _cls, _offset = self.rnet(img_dataset)  # ★★将24*24的图片传入网络再进行一次筛选

        cls = _cls.cpu().data.numpy()  # 将gpu上的数据放到cpu上去,在转成numpy数组
        offset = _offset.cpu().data.numpy()
        # print("r_cls:",cls.shape)  # (11, 1):P网络生成了11个框
        # print("r_offset:", offset.shape)  # (11, 4)

        boxes = []  # R 网络要留下来的框,存到boxes里
        idxs, _ = np.where(
            cls > self.r_cls)  # 原置信度0.6是偏低的,时候很多框并没有用(可打印出来观察),可以适当调高些;idxs置信度框大于0.6的索引;★返回idxs:0轴上索引[0,1],_:1轴上索引[0,0],共同决定元素位置,见例子3
        for idx in idxs:  # 根据索引,遍历符合条件的框;1轴上的索引,恰为符合条件的置信度索引(0轴上索引此处用不到)
            _box = _pnet_boxes[idx]
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])

            ow = _x2 - _x1  # 基准框的宽
            oh = _y2 - _y1

            x1 = _x1 + ow * offset[idx][0]  # 实际框的坐标点
            y1 = _y1 + oh * offset[idx][1]
            x2 = _x2 + ow * offset[idx][2]
            y2 = _y2 + oh * offset[idx][3]

            boxes.append([x1, y1, x2, y2, cls[idx][0]])  # 返回4个坐标点和置信度

        return utils.nms(np.array(boxes), self.r_nms)  # 原r_nms为0.5(0.5要往小调),上面的0.6要往大调;小于0.5的框被保留下来

        # 创建O网络检测函数

    def __onet_detect(self, image, rnet_boxes):

        _img_dataset = []  # 创建列表,存放抠图r
        _rnet_boxes = utils.convert_to_square(rnet_boxes)  # 给r网络输出的框,找出中心点,沿着最大边长的两边扩充成“正方形”
        for _box in _rnet_boxes:  # 遍历R网络筛选出来的框,计算坐标,抠图,缩放,数据类型转换,添加列表,堆叠
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])

            img = image.crop((_x1, _y1, _x2, _y2))  # 根据坐标点“抠图”
            img = img.resize((48, 48))
            img_data = self.__image_transform(img)  # 将抠出的图转成张量
            _img_dataset.append(img_data)

        img_dataset = torch.stack(_img_dataset)  # 堆叠,此处相当数据格式转换,见例子2
        if self.isCuda:
            img_dataset = img_dataset.cuda()

        _cls, _offset = self.onet(img_dataset)
        cls = _cls.cpu().data.numpy()  # (1, 1)
        offset = _offset.cpu().data.numpy()  # (1, 4)

        boxes = []  # 存放o网络的计算结果
        idxs, _ = np.where(
            cls > self.o_cls)  # 原o_cls为0.97是偏低的,最后要达到标准置信度要达到0.99999,这里可以写成0.99998,这样的话出来就全是人脸;留下置信度大于0.97的框;★返回idxs:0轴上索引[0],_:1轴上索引[0],共同决定元素位置,见例子3
        for idx in idxs:  # 根据索引,遍历符合条件的框;1轴上的索引,恰为符合条件的置信度索引(0轴上索引此处用不到)
            _box = _rnet_boxes[idx]  # 以R网络做为基准框
            _x1 = int(_box[0])
            _y1 = int(_box[1])
            _x2 = int(_box[2])
            _y2 = int(_box[3])

            ow = _x2 - _x1  # 框的基准宽,框是“方”的,ow=oh
            oh = _y2 - _y1

            x1 = _x1 + ow * offset[idx][0]  # O网络最终生成的框的坐标;生样,偏移量△δ=x1-_x1/w*side_len
            y1 = _y1 + oh * offset[idx][1]
            x2 = _x2 + ow * offset[idx][2]
            y2 = _y2 + oh * offset[idx][3]

            boxes.append([x1, y1, x2, y2, cls[idx][0]])  # 返回4个坐标点和1个置信度

        return utils.nms(np.array(boxes), self.o_nms, isMin=True)  # 用最小面积的IOU;原o_nms(IOU)为小于0.7的框被保留下来


def detect(imgs_path="./test_images", save_path="./result_images",test_video=False):
    if not os.path.exists(save_path):
        os.makedirs(save_path)


    if test_video:
    #  多张图片显示
        detector = Detector()
        cap = cv2.VideoCapture('./test_video/蔡徐坤.mp4')
        num = 0
        while cap.isOpened():
            ret,frame = cap.read()
            if cv2.waitKey(25) & 0xFF == ord('q'):
                break
            image = Image.fromarray(cv2.cvtColor(frame,cv2.COLOR_BGR2RGB))
            print("-------------------")
            boxes = detector.detect(image)
            print("size:", image.size)
            for box in boxes:
                x1 = int(box[0])
                y1 = int(box[1])
                x2 = int(box[2])
                y2 = int(box[3])
                print(x1, y1, x2, y2)
                print("conf:", box[4])
                head = image.crop(image,[x1,y1,x2,y2])
                head.save(f"./蔡徐坤照片/{num}.jpg")
                num+=1
                cv2.rectangle(frame, (x1, y1), (x2, y2), (0,0,255), 2)
            cv2.imshow("res",frame)


        cap.release()
        cv2.destroyAllWindows()

    else:
        for img in os.listdir(imgs_path):
            detector = Detector()
            with Image.open(os.path.join(imgs_path, img)) as image:
                print("-------------------")
                boxes = detector.detect(image)
                print("size:", image.size)
                img_draw = ImageDraw.Draw(image)
                for box in boxes:
                    x1 = int(box[0])
                    y1 = int(box[1])
                    x2 = int(box[2])
                    y2 = int(box[3])
                    print(x1, y1, x2, y2,"conf:", box[4])
                    # print("conf:", box[4])
                    img_draw.rectangle((x1, y1, x2, y2), outline="red")

                image.show()
                image.save(os.path.join(save_path, img))

if __name__ == '__main__':
    detect()


总结

人脸侦测中遇到了很多问题:

  1. MTCNN的第一阶段,图像金字塔会反反复复地很多次调用一个很浅层的P-NET网络,导致数据会反反复复地从内存COPY到显存,又从显存COPY到内存,而这个复制操作消耗很大,甚至比计算本身耗时。

  2. P网络运行速度对整个模型的影响较大;R、O网络抠图文件操作耗费时间;for循环串行耗费时间;硬件问题加剧模型耗时
    原因:图像金字塔需要耗费很多时间,反算没使用Tensor和矩阵进行计算;抠图未使用切片完成;一般越高级的语言运行越慢。
    解决方法:根据实际需要调整缩放比;使用tensor和矩阵运算优化代码。

  3. 调用模型时,记得eval()

  4. 解决问题的最好方式,是增加准确的数据

  5. 这里别用ReLU,效果不好,负半轴的丢失会造成一定的信息损失。

初次总结:如有不足,敬请求指教。

相关代码链接已上传csdn 0积分 。
下载链接
本人方向目标识别,有兴趣的小伙伴可以一起交流
WX:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2102672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小企业仓库管理升级:WMS助力智能化转型

WMS 在小型企业中的推进优势与必要性 WMS 是数字化工厂中不可或缺的系统之一&#xff0c;它不仅在大型企业中发挥着重要作用&#xff0c;对于小型企业同样具有巨大价值。通过引入 WMS&#xff0c;小型企业可以显著提高仓储管理效率&#xff0c;降低运营成本&#xff0c;支持业务…

vllm源码解析(二):调度策略分析

五 vllm调度逻辑 前面分享的知识确实遗漏了许多细节&#xff0c;不过不影响我们接下来的讲解&#xff0c;我们在第一篇文章中已经详细讲解过三个队列用途&#xff0c;不熟悉的同学可以回看。 本章涉及到的流程图已上传github: https://github.com/yblir/packages/blob/main/v…

力扣-968监控二叉树(Java贪心详细题解)

题目链接&#xff1a;968. 监控二叉树 - 力扣&#xff08;LeetCode&#xff09; 前情提要&#xff1a; 本题是一道名副其实的hard题目&#xff0c;他考察二叉树和贪心的综合运用能力。 所以我们不仅要会贪心还要会二叉树的一些知识&#xff0c;如果没有写二叉树类型的题目&a…

实战项目-快速实战-springboot dataway

最后附项目源码, 开箱即用 访问地址 http://127.0.0.1:8101/interface-ui/#/ 效果图 具体怎么用, 大家还是看官网,中文文档 https://www.dataql.net/docs/dataway/ui/ui-list 项目结构 代码 DataWayApplication package com.zero.dataway;import net.hasor.spring.boot…

集合框架,List常用API,栈和队列初识

回顾 集合框架 两个重点——ArrayList和HashSet. Vector/ArraysList/LinkedList区别 VectorArraysListLinkedList底层实现数组数组链表线程安全安全不安全不安全增删效率较低较低高扩容*2*1.5-------- &#xff08;>>&#xff09;运算级最低&#xff0c;记得加括号。 常…

窖藏之秘:白酒在窖藏过程中经历了哪些变化?

在中华五千年的文明史中&#xff0c;白酒一直扮演着举足轻重的角色。它不仅是文人墨客笔下的灵感源泉&#xff0c;更是亲朋好友间传递情感的桥梁。在众多白酒品牌中&#xff0c;豪迈白酒&#xff08;HOMANLISM&#xff09;以其不同的酿造工艺和窖藏技艺&#xff0c;成为了酒中翘…

EasyExcel模板导出与公式计算(上)

目录 环境要求 功能预览 需求分析 源码跟踪 自定义数据处理器 ​总结 最近做项目时遇到这样一个需求&#xff0c;将数据库表的含有公式的信息导出为Excel文件并且需要计算其结果&#xff0c;由于上网查询资料后未能完美解决&#xff0c;故将此踩坑过程记录下来以供参考。…

Datawhale X 李宏毅苹果书 AI夏令营第五期 DL进阶方向 Task3笔记

Datawhale X 李宏毅苹果书 向李宏毅学深度学习&#xff08;进阶&#xff09; 是 Datawhale 2024 年 AI 夏令营第五期的学习活动&#xff08;“深度学习 进阶”方向&#xff09; 往期task1链接&#xff1a;深度学习进阶-Task1 往期task2链接&#xff1a;深度学习进阶-Task2 我做…

惠中科技RDS自清洁膜层:光伏领域的绿色革命

惠中科技RDS自清洁膜层&#xff1a;光伏领域的绿色革命 在全球能源转型和光伏产业蓬勃发展的背景下&#xff0c;光伏电站的运营维护面临着诸多挑战&#xff0c;其中灰尘污染问题尤为突出。灰尘的堆积不仅降低了光伏板的透光率&#xff0c;还直接影响了电站的发电效率和经济效益…

算法复盘——Leetcode hot100: 双指针算法

双指针算法 11. 盛最多水的容器 - 力扣&#xff08;LeetCode&#xff09; 优化解法&#xff1a;用 left 和 right 两个指针从两端向中心收缩&#xff0c;一边收缩一边计算 [left, right] 之间的矩形面积&#xff0c;取最大的面积值即是答案。 其实是优化区间选择 直接丢弃体…

51单片机-串口通信(单片机和PC互发数据)

作者&#xff1a;Whappy 时间&#xff1a;2024.9.3 关于串口的疑问&#xff1f; 根据我的代码是不是初始化完成串口之后&#xff0c;只要我们使用串口发送数据就会触发中断&#xff1f; &#xff08;在文章下面&#xff09; ChatGPT said: ChatGPT 是的&#xff0c;根据…

IDEA项目启动在不同端口的方法,服务多端口启动

前言 在本地测试分布式事务以及分布式锁的过程中&#xff0c;在IDEA中多端口启动服务&#xff0c;可以高效方便开展测试调试工作。 开启流程 1.打开Edit Configurations 2.选中要复制的服务&#xff0c;点击复制小图标 3.配置启动端口号&#xff0c;点击保存 --server.port1…

解密Docker核心:深入理解Docker基础架构

随着云计算技术的普及&#xff0c;Docker容器技术在现代应用开发和部署中占据了重要地位。要充分理解Docker的优势与运用&#xff0c;深入掌握其基础架构是关键。本文将深入探讨Docker的核心组成部分及其在容器化平台中的角色和作用。 一、Docker的基础架构概述 Docker的基础…

leetcode 2816.翻倍以链表形式表示的数字

1.题目要求: 给你一个 非空 链表的头节点 head &#xff0c;表示一个不含前导零的非负数整数。将链表 翻倍 后&#xff0c;返回头节点 head 。2.题目代码: /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ str…

AI建模——文/图生模型产品介绍与模型免费下载

说明&#xff1a; 记录AI文生3D模型、图生3D模型的相关产品&#xff1b;记录其性能、功能、收费与免费方法 1.AI建模产品 Robin MeshAnything Meshy 生成效果比较&#xff1a; 2. Rodin 官网&#xff1a;gHyperHuman 支持&#xff1a;文生模型、图生模型 模型生成与下载…

自动控制:模糊控制器的原理及设计

自动控制&#xff1a;模糊控制器的原理及设计 引言 随着控制技术的不断发展&#xff0c;模糊控制器&#xff08;Fuzzy Controller&#xff09;作为一种智能控制技术&#xff0c;广泛应用于许多复杂系统中。与传统的线性控制器不同&#xff0c;模糊控制器无需精确的数学模型&a…

IOS17.0安装巨魔:TrollRestore巨魔发布

&#x1f47b; TrollRestore 17.0 巨魔发布 15.0 - 16.7 RC&#xff08;20H18&#xff09;和17.0。 官网&#xff1a;https://trollrestore.com/ 下载&#xff1a;https://pan.metanetdisk.com/IOS/%E5%B7%A8%E9%AD%94%E7%8E%A9%E5%AE%B6/TrollRestore.com 使用&#xff1a;ht…

《OpenCV计算机视觉》—— 图像边缘检测

文章目录 一、图像边缘检测概述二、常见的图像边缘检测算法&#xff08;简单介绍&#xff09;1.sobel算子2.Scharr算子3.Laplacian算子4.Canny算子 三、代码实现 一、图像边缘检测概述 图像边缘检测是一种重要的图像处理技术&#xff0c;用于定位二维或三维图像中对象的边缘。…

【运维监控】prometheus+node exporter+grafana 监控linux机器运行情况(1)

本示例是通过prometheus的node exporter收集主机的信息&#xff0c;然后在grafana的dashborad进行展示。本示例使用到的组件均是最新的&#xff0c;下文中会有具体版本说明&#xff0c;linux环境是centos。本示例分为四个部分&#xff0c;即prometheus、grafana、node exporter…

南京网站建设自己网站

南京是一座古老而又现代化的城市&#xff0c;拥有悠久的历史和文化底蕴。在这个信息时代&#xff0c;网站已经成为了企业和个人宣传推广的重要途径之一。南京网站建设作为一种推广方式&#xff0c;不仅能够展示企业形象&#xff0c;还能够传递信息、吸引客户、增加销售。 南京网…