MTCNN网络之P网络——pytorch代码实现

2023-06-17  本文已影响0人  小黄不头秃

MTCNN网络是多任务级联卷积神经网络,基于级联的特定目标检测器,在人脸识别中有着出色的表现。由P、R、O三个网络构成。常用于目标检测的算法还有SSD(单激多发探测器)、RCNN、YOLO等

如果对MTCNN的基础知识不太清楚,可以参考我的文章:

一、网络结构

MTCNN由三个子网络构成:P网络、R网络、O网络
P网络的输入是12*12,R网络输入是24*24,O网络输入是48*48。P网络注重从大量数据中筛选有效信息,R网络对P网络的筛选结果做二次筛选,O网络注重识别的精度。

(1)P网络(3层)

图像金字塔和滑动窗口把一张图片分割成了很多张图片,所以P网络面对的数据量是最大的。

代码实现:

"""
P网络结构
"""
import torch 
import torch.nn as nn
from PIL import Image
import torchvision

class PNet(nn.Module):
    def __init__(self):
        super(PNet, self).__init__()
        self.layers = nn.Sequential(
            nn.Conv2d(3,10,3,1,bias=False),nn.BatchNorm2d(10),nn.ReLU(), nn.MaxPool2d(2), # [1, 10, 5, 5]
            nn.Conv2d(10,16,3,1,bias=False),nn.BatchNorm2d(16),nn.ReLU(), # [1, 16, 3, 3]
            nn.Conv2d(16,32,3,1), # [1, 5, 1, 1]
        )

        self.cond = nn.Conv2d(32,1,1,1)
        self.offset = nn.Conv2d(32,4,1,1)
 
    def forward(self, x):
        y = self.layers(x)
        # print()
        category = self.cond(y)
        offset = self.offset(y)
        category = torch.sigmoid(category.float())
        return category, offset

if __name__ == "__main__":
    net = PNet()
    net.load_state_dict(torch.load("./param/pnet.pt"))
    x = Image.open(r"D:\document\DL\MTCNN_face_detect\dataset\12\positive\0.jpg")
    x = torchvision.transforms.ToTensor()(x).unsqueeze_(0)
    y1, y2 = net(x)
    print(y1)
    print(y2)
P网络数据准备:

数据准备时应注意:

"""
wider-face 生成12*12的训练数据
"""
import os
import sys
sys.path.append("./")
# print(os.getcwd())
import cv2
from PIL import Image
import numpy as np
from tqdm import tqdm
from utils.utils import IOU2 
import torch

TARGET_PATH = "./dataset/"
IMG_PATH = "./dataset/WIDER_train/images/"
DST_PATH = "./dataset/"
face_size = 12

label_file_path = TARGET_PATH + "wider_face_train.txt"

# 首先先生成对应的文件夹
if not os.path.exists(DST_PATH+f"{face_size}"):
    os.mkdir(DST_PATH+"12")

# pos,part,neg裁剪图片放置位置
pos_save_dir = os.path.join(DST_PATH, f'{face_size}/positive')
part_save_dir = os.path.join(DST_PATH, f'{face_size}/part')
neg_save_dir = os.path.join(DST_PATH, f'{face_size}/negative')
# PNet数据地址
save_dir = os.path.join(DST_PATH, f'{face_size}/')

# 创建文件夹
if not os.path.exists(save_dir):
    os.mkdir(save_dir)
if not os.path.exists(pos_save_dir):
    os.mkdir(pos_save_dir)
if not os.path.exists(part_save_dir):
    os.mkdir(part_save_dir)
if not os.path.exists(neg_save_dir):
    os.mkdir(neg_save_dir)

# 生成后的数据列表文件
positive_file = open(os.path.join(save_dir, 'positive.txt'), 'w')
negative_file = open(os.path.join(save_dir, 'negative.txt'), 'w')
part_file = open(os.path.join(save_dir, 'part.txt'), 'w')

# 原数据集的列表文件
with open(label_file_path, 'r') as f:
    annotations = f.readlines()
num = len(annotations)
print('总共的图片数: %d' % num)

# 记录pos,neg,part三类生成数
positive_counter = 0
negative_counter = 0
part_counter = 0

# 记录读取的图片数
idx = 0
for anno in tqdm(annotations):
    anno = anno.strip().split(' ')
    img_name = anno[0]
    img_path = IMG_PATH+img_name+".jpg"
    # 获取人脸的box所有坐标
    box = list(map(float, anno[1:]))
    # 把所有box的坐标按照4分割,里面是左上角和右下角的信息
    boxes = np.array(box, dtype=np.float32).reshape(-1, 4)
    w = box[2] - box[0]
    h = box[3] - box[1]
    
    # 已经获取了坐标信息下面对图像进行操作
    with Image.open(img_path) as img:
        idx += 1
        img_w, img_h = img.size

        # 过滤字段
        if max(w,h)<40 or box[0]<0 or box[1]<0 or w<0 or h<0:  continue 
        # 标注不标准,给框适当的偏移量
        x1 = int(box[0] + w*0.12)
        y1 = int(box[1] + h*0.1)
        x2 = int(box[0] + w*0.9)
        y2 = int(box[1] + h*0.85)
        w = int(x2-x1)
        h = int(y2-y1)

        boxes = [[x1, y1, x2, y2]]
        cx = x1 + w/2
        cy = y1 + h/2

        # # 查看框是否合适
        # img = cv2.imread(img_path)
        # img = cv2.rectangle(img, (x1,y1), (x2,y2),(0,0,255),2)
        # cv2.imshow("img",img)
        # cv2.waitKey(0)

        # 让正样本和部分样本随着图片中心进行随机偏移
        for _ in range(5):
            w_ = np.random.randint(-w*0.2, w*0.2)
            h_ = np.random.randint(-h*0.2, h*0.2)
            cx_ = cx + w_
            cy_ = cy + h_

            side_len = np.random.randint(int(min(w,h)*0.8), np.ceil(max(w,h)*1.25))
            # print("asdfaadscsdc  ",side_len)
            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_])

            # img2 = cv2.imread(img_path)
            # img2 = cv2.rectangle(img2,(int(x1_), int(y1_)), (int(x2_), int(y2_)),(0,0,255),2)
            # img2 = cv2.rectangle(img2, (x1,y1), (x2,y2),(0,255,0),2)
            # cv2.imshow("img", img2)
            # cv2.waitKey(0)
            # 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

            face_crop = img.crop(crop_box)
            face_resize = face_crop.resize((face_size, face_size), Image.Resampling.LANCZOS)

            iou = IOU2(torch.tensor(boxes), torch.tensor(crop_box)).item()

            if iou > 0.65: # 正样本
                positive_file.write(f"positive/{ positive_counter}.jpg 1 {offset_x1} {offset_y1} {offset_x2} {offset_y2}\n")
                positive_file.flush()
                face_resize.save(pos_save_dir+f"/{positive_counter}.jpg")
                positive_counter += 1
            elif iou>0.4: # 部分样本
                part_file.write(f"part/{ part_counter}.jpg 2 {offset_x1} {offset_y1} {offset_x2} {offset_y2}\n")
                part_file.flush()
                face_resize.save(part_save_dir+f"/{ part_counter}.jpg")
                part_counter += 1
            # elif iou < 0.1: # 负样本
            #     negative_file.write(f"negative/{ negative_counter}.jpg 0 0 0 0 0\n")
            #     negative_file.flush()
            #     face_resize.save(neg_save_dir+f"/{ negative_counter}.jpg")
            #     negative_counter += 1
        
        _boxes = torch.tensor(boxes[0])

        for _ in range(5): # 这里是为了能够生成足够的负样本
            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 IOU2(torch.tensor(crop_box),_boxes).item() < 0.1:
                face_crop = img.crop(crop_box)
                face_resize = face_crop.resize((face_size, face_size), Image.Resampling.LANCZOS)
                
                negative_file.write(f"negative/{ negative_counter}.jpg 0 0 0 0 0\n")
                negative_file.flush()
                face_resize.save(neg_save_dir+f"/{ negative_counter}.jpg")
                negative_counter += 1

P网络训练代码实现:
import torch 
import os 
import sys
sys.path.append("./")
import torch.nn as nn
from models.PNet import PNet
from utils.trainer import Trainer 
from utils.data import MTCNN_Dataset
from torch.utils.data import DataLoader, Dataset 

model_save_path = "./param/pnet.pt"
data = MTCNN_Dataset("./dataset/12")
data_loader = DataLoader(data, shuffle=True, batch_size=100)
net = PNet()

trainer = Trainer(net, model_save_path,data_loader,50)
trainer.train()


训练器和工具类
import torch 
import torch.nn as nn
import os 
import sys
sys.path.append("./")
from utils.utils import accuracy

class Trainer():
    def __init__(self,net, param_path, data_loader, epoch):
        self.net = net
        self.param_path = param_path
        self.data_loader = data_loader 
        self.epoch = epoch
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        # self.classLoss = nn.BCELoss(reduction="mean")
        self.offsetLoss = nn.MSELoss(reduction="mean") 
        # self.optim = torch.optim.Adam(self.net.parameters(),lr=0.001,weight_decay=0.0001)
        self.optim = torch.optim.SGD(self.net.parameters(),lr=1e-5,weight_decay=0.0001) # 使用SGD进行微调
        self.scheduler = torch.optim.lr_scheduler.MultiStepLR(self.optim, milestones=[6, 14, 20], gamma=0.1)

    def train(self):
        if os.path.exists(self.param_path):
            self.net.load_state_dict(torch.load(self.param_path))
        self.net.to(self.device)
        self.net.train()

        for e in range(self.epoch):
            for b_i, (img, cat, offset) in enumerate(self.data_loader):
                if self.device == "cuda":
                    img = img.to(self.device)
                    cat = cat.to(self.device)
                    offset = offset.to(self.device)
                cat_out, offset_out = self.net(img)
                cat_out = cat_out.reshape(-1,1) # [1024,1]
                offset_out = offset_out.reshape(-1, 4) # [1024, 4]

                # 在计算损失的时候,置信度只需要正样本和负样本
                # 偏移量只需要部分样本和正样本
                category_mask = cat < 2
                category_select = torch.masked_select(cat, category_mask)
                category_out_select = torch.masked_select(cat_out, category_mask)
                pred = torch.clamp(category_out_select, min=1e-7, max=1-1e-7)
                cls_loss = - category_select * torch.log(pred) - (1-category_select) * torch.log(1-pred)
                cls_loss = torch.mean(cls_loss)
                # cls_loss = self.classLoss(category_select, category_out_select) # 这里会出现nan的大坑

                offset_mask = cat > 0 
                offset_index = torch.nonzero(offset_mask)[:,0]
                offset_select = offset[offset_index]
                offset_out_select = offset_out[offset_index]
                offset_loss = self.offsetLoss(offset_select, offset_out_select)
                loss = cls_loss+offset_loss

                # print(cls_loss, offset_loss)
                # 更新参数
                self.optim.zero_grad()
                loss.backward()
                self.optim.step()

                if b_i%100 == 0:
                    acc = accuracy(class_out=cat_out, label=cat)
                    print('Train epoch %d, batch %d, total_loss: %f, cls_loss: %f, box_loss: %f,'
                  'accuracy:%.2f' % (e, b_i, loss.item(), cls_loss.item(), offset_loss.item(), acc*100))
                    torch.save(self.net.state_dict(), self.param_path)

            self.scheduler.step()
import pickle
import shutil
import numpy as np
import random
import os
import cv2
from tqdm import tqdm
import torch

def IOU(box, boxes):
    """裁剪的box和图片所有人脸box的iou值
    参数:
      box:裁剪的box,当box维度为4时表示box左上右下坐标,维度为5时,最后一维为box的置信度
      boxes:图片所有人脸box,[n,4]
    返回值:
      iou值,[n,]
    """
    # box面积
    box_area = (box[2] - box[0] + 1) * (box[3] - box[1] + 1)
    # boxes面积,[n,]
    area = (boxes[:, 2] - boxes[:, 0] + 1) * (boxes[:, 3] - boxes[:, 1] + 1)
    # 重叠部分左上右下坐标
    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 + 1)
    h = np.maximum(0, yy2 - yy1 + 1)
    # 重叠部分面积
    inter = w * h
    return inter / (box_area + area - inter + 1e-10)

def IOU2(box, other_boxes):
    #box: [x1,y1,x2,y2] 分别表示方框的左上角的点和右下角的点
    #other_boxs: N个box,多了一个维度(代表box的数量)
    if len(box.shape)==2: box = box.squeeze()
    if len(other_boxes.shape)==1: other_boxes = other_boxes.unsqueeze(dim=0)
    box_area = (box[2]-box[0])*(box[3]-box[1])
    other_boxes_area = (other_boxes[:,2]-other_boxes[:,0]) * (other_boxes[:,3]-other_boxes[:,1])

    #交集
    x1 = torch.max(box[0],other_boxes[:,0])
    y1 = torch.max(box[1],other_boxes[:,1])
    x2 = torch.min(box[2],other_boxes[:,2])
    y2 = torch.min(box[3],other_boxes[:,3])
    Min = torch.tensor([0])
    w,h = torch.max(Min,x2-x1),torch.max(Min,y2-y1) #如果没有相交的框,两者相减是负值,为防止此类事情出现
    # 交集的面积
    overlap_area = w*h

    # 交并比 = 交集 / 并集
    iou = overlap_area / (box_area+other_boxes_area-overlap_area)
    return iou

# 求训练时的准确率
def accuracy(class_out, label):
    # 查找neg 0 和pos 1所在的位置
    class_out = class_out.detach().cpu().numpy().round()
    label = label.detach().cpu().numpy()
    mask = class_out == label
    correct = np.sum(mask)
    return correct/len(class_out)

def IOU3(box, other_boxes, isMin=False):
    #box: [x1,y1,x2,y2] 分别表示方框的左上角的点和右下角的点
    #other_boxs: N个box,多了一个维度(代表box的数量)
    if len(box.shape)==2: box = box.squeeze()
    if len(other_boxes.shape)==1: other_boxes = other_boxes.unsqueeze(dim=0)
    box_area = (box[2]-box[0])*(box[3]-box[1])
    other_boxes_area = (other_boxes[:,2]-other_boxes[:,0]) * (other_boxes[:,3]-other_boxes[:,1])

    #交集
    x1 = torch.max(box[0],other_boxes[:,0])
    y1 = torch.max(box[1],other_boxes[:,1])
    x2 = torch.min(box[2],other_boxes[:,2])
    y2 = torch.min(box[3],other_boxes[:,3])
    Min = torch.tensor([0])
    w,h = torch.max(Min,x2-x1),torch.max(Min,y2-y1) #如果没有相交的框,两者相减是负值,为防止此类事情出现
    # 交集的面积
    overlap_area = w*h

    if isMin:
        # 交并比 = 交集 / 最小面积
        iou = overlap_area / torch.min(box_area,other_boxes_area)
    else:
        # 交并比 = 交集 / 并集
        iou = overlap_area / (box_area+other_boxes_area-overlap_area)
    return iou


def NMS(boxes, thre = 0.5, isMin=False):
    # boxes: [[置信度, x1,y1,x2,y2], [置信度, x1,y1,x2,y2], ...]
    # - 第一步:找出所有预测目标中得分最高的预测框作为基准框;
    # - 第二步:计算剩余的预测框与基准框的IOU,如果IOU>阈值t,则将这些预测框删除,因为,这些预测框与0.97的太相似了,所以是冗余框,需要抑制;
    # - 第三步:在去除冗余框后剩余预测框中,找到得分第二高的预测框0.95作为基准框,执行第二步操作;
    # - 第四步:重复上述过程,直至剩余预测框中的每个预测框都曾被用作过基准框。即剩余的预测框任意两个IOU均小于t,没有两个预测框过于相似,完成nms操作。
    res = []
    if len(boxes) == 0:
        return torch.tensor([])
    sort_boxes = boxes[boxes[:, 0].argsort(descending=True)] # 获得置信度降序排序的下标,并根据下标进行索引。

    # 防止传入错误的值
    if len(sort_boxes) == 0:
        print("ERROR: NO boxes! ")
        return []

    while len(sort_boxes) > 1:
        ref_box = sort_boxes[0]
        res.append(ref_box)

        other_boxes = sort_boxes[1:]
        sort_boxes = other_boxes[torch.where(IOU3(ref_box[1:], other_boxes[:,1:], isMin=isMin)<thre)]
    
    if len(sort_boxes)>0: res.append(sort_boxes[0])

    return torch.stack(res)

def convert2square(p_box):
    square_box = p_box.copy()
    if square_box.shape[0] == 0: return np.array([])
    h = p_box[:, 3] - p_box[:, 1]
    w = p_box[:, 2] - p_box[:, 0]
    maxside = np.maximum(h,w)
    square_box[:, 0] = p_box[:, 0] + w*0.5 - maxside*0.5
    square_box[:, 1] = p_box[:, 1] + w*0.5 - maxside*0.5
    square_box[:, 2] = square_box[:, 0] + maxside
    square_box[:, 3] = square_box[:, 1] + maxside
    return square_box

本文代码仅供参考,具体代码可参照:Pytorch-MTCNN: 基于Pytorch实现的MTCNN模型,人脸检测,人脸关键点检测。 (gitee.com)

上一篇下一篇

猜你喜欢

热点阅读