MTCNN之人脸检测——pytorch代码实现

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

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

如果对MTCNN的基础知识不太清楚,可以参考我的文章:
MTCNN之基础知识笔记 - 简书 (jianshu.com)

一、网络结构

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

(1)P网络(3层)

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

P网络就相当于是一个12*12的卷积核,对大量图片进行筛选。并且P网络是全卷积结构,这就说明P网络的输入可以是大小不固定的图片。

(2)R网络(4层)

使用三层卷积层后接一个全连接层。这里与P网络不一样。然而P网络是全卷积结构,为什么会造成这样的差异呢?因为我们并不清楚一张图片中有多少个人脸,图片大小也不一样,自然不能使用全连接层。但是R网络接收的是P网络的输出结果,输入的大小是固定的。所以这里并没有使用全卷积结构,而是使用传统的但目标识别网络结构。

(3)O网络(5层)

是三个网络中参数最多,网络层数最深的,因为到了最后一步,我们需要对目标进行更加精确的识别,所以相对神经网络的能力要求更高。

二、实现步骤

  1. 下载数据集
  2. 训练P网络、制作训练数据、保存模型
  3. 训练R网络、制作训练数据、保存模型
  4. 训练O网络、制作训练数据、保存模型
  5. 将三个网络结合起来使用。

模型代码实现(仅供参考):

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
"""
R网络
"""
class RNet(nn.Module):
    def __init__(self):
        super(RNet, self).__init__()
        self.layer = nn.Sequential(
            nn.Conv2d(3,28,3,1,1,bias=False),nn.BatchNorm2d(28),nn.ReLU(),
            nn.MaxPool2d(3,stride=2),
            nn.Conv2d(28,48,3,1,0,bias=False),nn.BatchNorm2d(48),nn.ReLU(),
            nn.MaxPool2d(3,stride=2),
            nn.Conv2d(48,64,2,1,bias=False),nn.BatchNorm2d(64),nn.ReLU()
        )

        self.fc1 = nn.Sequential(
            nn.Linear(64*3*3, 128),nn.ReLU(),
            nn.Linear(128,1)
        )

        self.fc2 = nn.Sequential(
            nn.Linear(64*3*3, 128),nn.ReLU(),
            nn.Linear(128,4)
        )

    def forward(self, x):
        y = self.layer(x)
        y = y.reshape(-1, 64*3*3)
        class_out = torch.sigmoid(self.fc1(y))
        bbox_out = self.fc2(y)
        return class_out, bbox_out 

"""
O网络
"""
class ONet(nn.Module):
    def __init__(self):
        super(ONet, self).__init__()
        self.layer = nn.Sequential(
            nn.Conv2d(3,32,3,1,0,bias=False),nn.BatchNorm2d(32),nn.ReLU(),
            nn.MaxPool2d(3,stride=2,padding=1),
            nn.Conv2d(32,64,3,1,0,bias=False),nn.BatchNorm2d(64),nn.ReLU(),
            nn.MaxPool2d(3,stride=2),
            nn.Conv2d(64,64,3,1,0,bias=False),nn.BatchNorm2d(64),nn.ReLU(),
            nn.MaxPool2d(2,stride=2),
            nn.Conv2d(64,128,2,1,bias=False),nn.BatchNorm2d(128),nn.ReLU()
        )
        self.fc1 = nn.Sequential(
            nn.Linear(128*3*3, 256),nn.ReLU(),
            nn.Linear(256,1)
        )

        self.fc2 = nn.Sequential(
            nn.Linear(128*3*3, 256),nn.ReLU(),
            nn.Linear(256,4)
        )

    def forward(self, x):
        y = self.layer(x)
        y = y.reshape(-1, 128*3*3)
        class_out = torch.sigmoid(self.fc1(y))
        bbox_out = self.fc2(y)
        return class_out, bbox_out

二、数据集制作

在实验的过程中使用的是WIDER FACE数据集。
关于数据集:可以在这里下载:celeba官网 或者 WIDER FACE: A Face Detection Benchmark

在制作训练神经网络的数据的时候,利用图片大小和建议框的IOU,将数据划分为负样本、正样本和地标:

地标不参与训练过程,地标的作用是拉大正负样本之间的距离。
这里得看你的数据集是什么样的。

数据准备时应注意

这里我提供了两种代码将人脸数据集转换成MTCNN能训练的数据结构的方法:Celeb A & Wider Face

数据集制作代码(仅参考):

"""
celeb A 数据集转换成12,24,48大小的训练数据
"""
import numpy as np 
import os 
from PIL import Image 
from utils import IOU, NMS
import torch

TARGET_PATH = "./data/celeba/Datasets/CelebA/Anno/"
IMG_PATH = "./data/celeba/Datasets/CelebA/Img/img_align_celeba/img_align_celeba"
DST_PATH = "./data/train/"

label_file_path = TARGET_PATH + "list_bbox_celeba.txt"

# 首先先生成对应的文件夹
if not os.path.exists(DST_PATH):
    os.mkdir(DST_PATH)

# 生成不同尺寸的人脸样本:负样本:正样本:部分样本 = 3:1:2
for face_size in (12,24,48):
    base_path = DST_PATH + f"{face_size}/"
    if not os.path.exists(base_path):
        os.mkdir(base_path)
    if not os.path.exists(base_path+"positive"):
        os.mkdir(base_path+"positive")
    if not os.path.exists(base_path+"negative"):
        os.mkdir(base_path+"negative")
    if not os.path.exists(base_path+"part"):
        os.mkdir(base_path+"part")
    
    # 样本标签存储路径
    positive_filename = base_path+"positive.txt"
    negative_filename = base_path+"negative.txt"
    part_filename = base_path+"part.txt"

    # 计数值
    positive_counter = 0
    negative_counter = 0
    part_counter = 0
    
    try:
        positive_file = open(positive_filename, "w")
        negative_file = open(negative_filename, "w")
        part_file = open(part_filename, "w")

        for i, line in enumerate(open(label_file_path)):
            print(f"positive:{positive_counter}, negative:{negative_counter}, part:{part_counter}")
            if i<2: continue 
            try:
                strs = line.strip().split(" ") # 删除两边的空格 ['000001.jpg', '', '', '', '95', '', '71', '226', '313']
                strs = list(filter(bool, strs))# 过滤序列
                
                image_filename = strs[0]
                image_filepath = IMG_PATH+image_filename

                with Image.open(image_filepath) as img:
                    img_w,img_h = img.size 
                    x1 = float(strs[1].strip())
                    y1 = float(strs[2].strip())
                    w = float(strs[3].strip())
                    h = float(strs[4].strip())
                    x2 = float(x1 + w)
                    y2 = float(y1 + h)

                    # 过滤字段
                    if max(w,h)<40 or x1<0 or y1<0 or w<0 or h<0: continue 

                    # 标注不标准,给框适当的偏移量
                    x1 = int(x1 + w*0.12)
                    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]]
                    cx = w/2 + x1
                    cy = h/2 + y1

                    # 让正样本和部分样本随着图片中心进行随机偏移
                    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(round(min(w,h)*0.8), round(max(w,h)*1.25))
                        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_])

                        # 计算坐标偏移量
                        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.ANTIALIAS)

                        iou = IOU(torch.tensor(boxes), torch.tensor(crop_box))

                        if iou > 0.6: # 正样本
                            positive_file.write(f"positive/{ positive_counter}.jpg 1 {offset_x1} {offset_y1} {offset_x2} {offset_y2}")
                            positive_file.flush()
                            face_resize.save(base_path+f"positive/{ part_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}")
                            part_file.flush()
                            face_resize.save(base_path+f"part/{ part_counter}.jpg")
                            part_counter += 1
                        elif iou < 0.1: # 负样本
                            negative_file.write(f"negative/{ negative_counter}.jpg 0 0 0 0 0")
                            negative_file.flush()
                            face_resize.save(base_path+f"negative/{ negative_counter}.jpg")
                            negative_counter += 1
                    
                    _boxes = torch.tensor(boxes)

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

            except Exception as e:
                pass 
    except Exception as e:
        pass
"""
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(round(min(w,h)*0.8), round(max(w,h)*1.25))
            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_])

            # 计算坐标偏移量
            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.ANTIALIAS)

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

            if iou > 0.6: # 正样本
                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(10): # 这里是为了能够生成足够的负样本
            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.15:
                face_crop = img.crop(crop_box)
                face_resize = face_crop.resize((face_size, face_size), Image.ANTIALIAS)
                
                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

数据集制作的代码实现:

import torch 
from torch.utils.data import DataLoader, Dataset 
from torchvision.transforms import ToTensor
from PIL import Image

class MTCNN_Dataset(Dataset):
    def __init__(self, path) -> None:
        super().__init__()
        self.path = path 
        self.dataset = []
        self.trans = ToTensor()
        self.dataset.extend(open(self.path + "/positive.txt"))
        self.dataset.extend(open(self.path + "/negative.txt"))
        self.dataset.extend(open(self.path + "/part.txt"))

    def __len__(self):
        return len(self.dataset)
    
    def __getitem__(self, index):
        strs = self.dataset[index].strip().split(" ")
        img_path = self.path + f"/{strs[0]}"
        cls = float(strs[1])
        offset = list(map(float, strs[2:]))
        img = Image.open(img_path)
        img = self.trans(img)
        return img, torch.tensor([cls]), torch.tensor(offset) 

三、模型训练

模型在训练过程中有一个重要的点,就是我们需要计算两个损失,一个是置信度的损失,一个是偏移量的损失。关于置信度的损失,我们使用的是sigmoid激活函数 + BCELoss的方式,偏移量的损失使用MSELoss。

在计算置信度损失的时候,我们仅需要一个批次中的正样本和负样本来计算损失(不需要部分样本)。
在计算偏移量损失的时候,我们仅需要一个批次中的正样本和部分样本,而不需要负样本。

利用标签中的对正样本(1)、负样本(0)、部分样本(2)的置信度标识可以实现。例如:小于2的数据就是正样本和负样本,大于0的是正样本和部分样本。

关于三个网络的网络结构和模型训练的代码,我的另外的笔记中有写,这里给出链接,以下代码仅供参考:
P网络结构的pytorch代码实现和训练代码的实现可以参考我的笔记:
MTCNN网络之P网络——pytorch代码实现 - 简书 (jianshu.com)

R网络结构的pytorch代码实现和训练代码的实现可以参考我的笔记:
MTCNN之R网络——pytorch代码实现 - 简书 (jianshu.com)

O网络结构的pytorch代码实现和训练代码的实现可以参考我的笔记:
MTCNN网络之O网络——pytorch代码实现 - 简书 (jianshu.com)

损失函数代码实现:

class ClassLoss(nn.Module):
    def __init__(self):
        super(ClassLoss, self).__init__()
        self.entropy_loss = nn.CrossEntropyLoss(ignore_index=-100, reduction='none')
        self.keep_ratio = 0.7

    def forward(self, class_out, label):
        # 保留neg 0 和pos 1 的数据,忽略掉part -1, landmark -2
        label = torch.where(label < 0, -100, label)
        # 求neg 0 和pos 1 的数据70%数据
        valid_label = torch.where(label >= 0, 1, 0)
        num_valid = torch.sum(valid_label)
        keep_num = int((num_valid * self.keep_ratio).cpu().numpy())
        label = torch.squeeze(label)
        # 计算交叉熵损失
        loss = self.entropy_loss(input=class_out, target=label)
        # 取有效数据的70%计算损失
        loss, _ = torch.topk(torch.squeeze(loss), k=keep_num)
        return torch.mean(loss)


class BBoxLoss(nn.Module):
    def __init__(self):
        super(BBoxLoss, self).__init__()
        self.square_loss = nn.MSELoss(reduction='none')
        self.keep_ratio = 1.0

    def forward(self, bbox_out, bbox_target, label):
        # 保留pos 1 和part -1 的数据
        valid_label = torch.where(torch.abs(label) == 1, 1, 0)
        valid_label = torch.squeeze(valid_label)
        # 获取有效值的总数
        keep_num = int(torch.sum(valid_label).cpu().numpy() * self.keep_ratio)
        loss = self.square_loss(input=bbox_out, target=bbox_target)
        loss = torch.sum(loss, dim=1)
        loss = loss.cuda() * valid_label
        # 取有效数据计算损失
        loss, _ = torch.topk(loss, k=keep_num, dim=0)
        return torch.mean(loss)

模型训练代码实现(以P网络为例):

# 设置优化方法
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.001, weight_decay=1e-4)

# 获取学习率衰减函数
scheduler = MultiStepLR(optimizer, milestones=[6, 14, 20], gamma=0.1)

# 获取损失函数
class_loss = ClassLoss()
bbox_loss = BBoxLoss()
landmark_loss = LandmarkLoss()

# 开始训练
for epoch in range(epoch_num):
    for batch_id, (img, label, bbox, landmark) in enumerate(train_loader):
        img = img.to(device)
        label = label.to(device).long()
        bbox = bbox.to(device)
        landmark = landmark.to(device)
        class_out, bbox_out, landmark_out = model(img)
        cls_loss = class_loss(class_out, label)
        box_loss = bbox_loss(bbox_out, bbox, label)
        landmarks_loss = landmark_loss(landmark_out, landmark, label)
        total_loss = radio_cls_loss * cls_loss + radio_bbox_loss * box_loss + radio_landmark_loss * landmarks_loss
        optimizer.zero_grad()
        total_loss.backward()
        optimizer.step()
        if batch_id % 100 == 0:
            acc = accuracy(class_out, label)
            print('[%s] Train epoch %d, batch %d, total_loss: %f, cls_loss: %f, box_loss: %f, landmarks_loss: %f, '
                  'accuracy:%f' % (datetime.now(), epoch, batch_id, total_loss, cls_loss, box_loss, landmarks_loss, acc))
    scheduler.step()

    # 保存模型
    if not os.path.exists(model_path):
        os.makedirs(model_path)
    torch.jit.save(torch.jit.script(model), os.path.join(model_path, 'PNet.pth'))

四、模型测试

(1)图片识别代码实现:
import argparse 
import os 
import cv2 
from PIL import Image, ImageDraw, ImageFont
from torchvision import transforms
import numpy as np 
import torch 
from models.PNet import PNet
from models.ONet import ONet
from models.RNet import RNet
from utils.utils import NMS, convert2square

font = cv2.FONT_HERSHEY_DUPLEX  # 设置字体

parser = argparse.ArgumentParser()
parser.add_argument('--model_path', type=str, default='./param/',      help='PNet、RNet、ONet三个模型文件存在的文件夹路径')
parser.add_argument('--p_cls', type=float, default=0.7,      help='参数')
parser.add_argument('--p_nms', type=float, default=0.4,      help='参数')
parser.add_argument('--r_cls', type=float, default=0.8,      help='参数')
parser.add_argument('--r_nms', type=float, default=0.4,      help='参数')
parser.add_argument('--o_cls', type=float, default=0.8,      help='参数')
parser.add_argument('--o_nms', type=float, default=0.1,      help='参数')
args = parser.parse_args()

device = "cuda" if torch.cuda.is_available() else "cpu"

class Detecter:
    def __init__(self) -> None:
        # 获取P模型
        self.pnet = PNet()
        self.pnet.load_state_dict(torch.load(args.model_path + "pnet.pt"))
        self.pnet.to(device)
        self.pnet.eval()

        # 获取R模型
        self.rnet = RNet()
        self.rnet.load_state_dict(torch.load(args.model_path + "rnet.pt"))
        self.rnet.to(device)
        self.rnet.eval()

        # 获取R模型
        self.onet = ONet()
        self.onet.load_state_dict(torch.load(args.model_path + "onet.pt"))
        self.onet.to(device)
        self.onet.eval()

        self._image_transform = transforms.ToTensor()

    def detect_img(self, image):
        # P网络检测
        pnet_boxes = self.__pnet_detect(image)
        if pnet_boxes.shape[0] == 0:
            return np.array([])
        # return pnet_boxes
        
        # R网络检测
        rnet_boxes = self.__rnet_detect(image, pnet_boxes)
        if rnet_boxes.shape[0] == 0:
            return np.array([])
        # return rnet_boxes
        
        # O网络检测
        onet_boxes = self.__onet_detect(image, rnet_boxes)
        if onet_boxes.shape[0] == 0:
            return np.array([])
        return onet_boxes
    
    def __pnet_detect(self, image):
        boxes = []
        img = image 
        w,h = img.size 
        min_side_len = min(w, h)

        scale = 1
        while min_side_len > 12:
            img_data = self._image_transform(img).to(device)
            img_data.unsqueeze_(0) # 将图片升维

            cls, offset = self.pnet(img_data)
            cls = cls[0][0].cpu().data # [995, 1495]
            offset = offset[0].cpu().data # [4, 995, 1495]

            idxs = torch.nonzero(torch.gt(cls, args.p_cls)) # 获取置信度>0.6的值索引 [7277, 2]

            for idx in idxs:
                boxes.append(self.__box(idx, offset, cls[idx[0], idx[1]], scale)) # __box是偏移量的反算函数

            scale *= 0.8 
            _w = int(w*scale)
            _h = int(h*scale)

            img = img.resize((_w, _h))
            min_side_len = min(_h, _w)
        # print(len(boxes))
        # exit()
        boxes = NMS(torch.tensor(boxes),thre=args.p_nms)
        return boxes
    
    def __rnet_detect(self, image, p_box):
        _img_dataset = []
        suqare_box = convert2square(np.array(p_box[:,1:]))
        for _box in suqare_box:
            # 从原图上切割图片
            _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((24,24))
            img_data = self._image_transform(img)
            _img_dataset.append(img_data)
        img_dataset = torch.stack(_img_dataset).to(device)

        # R网络检测
        _cls, _offset = self.rnet(img_dataset)
        cls = _cls.cpu().data
        offset = _offset.cpu().data

        boxes = []
        idxs, _ = np.where(cls > args.r_cls)
        for idx in idxs:
            _box = suqare_box[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([cls[idx][0], x1,y1,x2,y2])
        boxes = NMS(torch.tensor(boxes),thre=args.r_nms,isMin=True)
        return boxes
    
    def __onet_detect(self, image, r_box):
        _img_dataset = []
        suqare_box = convert2square(np.array(r_box[:,1:]))
        for _box in suqare_box:
            # 从原图上切割图片
            _x1 = int(_box[0]) 
            _y1 = int(_box[1]) 
            _x2 = int(_box[2]) 
            _y2 = int(_box[3])

            img = image.crop((_x1, _y1, _x2, _y2)) 
            # img.show()
            img = img.resize((48,48))
            img_data = self._image_transform(img)
            _img_dataset.append(img_data)
        img_dataset = torch.stack(_img_dataset).to(device)

        # O网络检测
        _cls, _offset = self.onet(img_dataset)
        cls = _cls.cpu().data
        offset = _offset.cpu().data

        boxes = []
        idxs, _ = np.where(cls > args.o_cls)
        for idx in idxs:
            # _box = r_box[idx][1:]
            _box = suqare_box[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([cls[idx][0], x1,y1,x2,y2])
        boxes = NMS(torch.tensor(boxes),thre=args.o_nms, isMin=True)
        return boxes

    def __box(self, idx, offset, cls, scale, stride=2, side_len=12): # P网络池化步长为2
        # 计算的是原格子的坐标
        _x1 = (idx[1].float() * stride) / scale
        _y1 = (idx[0].float() * stride) / scale
        _x2 = (idx[1].float() * stride + side_len - 1) / scale
        _y2 = (idx[0].float() * stride + side_len - 1) / scale

        ow = _x2 - _x1
        oh = _y2 - _y1
        
        _offset = offset[:, idx[0], idx[1]] # 某点的偏移量 [4]

        x1 = _x1 + ow * _offset[0]
        y1 = _y1 + oh * _offset[1]
        x2 = _x2 + ow * _offset[2]
        y2 = _y2 + oh * _offset[3]

        return [cls, x1,y1,x2,y2]

if __name__ == "__main__":
    img_path = "./dataset/0.jpg"
    img = Image.open(img_path)
    de = Detecter()
    boxes = de.detect_img(img)
    # print(boxes.shape)

    img2 = cv2.imread(img_path)
    for box in boxes:
        img2 = cv2.rectangle(img2, (int(box[1]), int(box[2])), (int(box[3]), int(box[4])),(0,0,255),2)
        cls = round(box[0].item(),2)
        img2 = cv2.putText(img2, f"{cls}", (int(box[1]), int(box[2])), font, 1, (255, 0, 0), 2)
        # break
    img2 = cv2.resize(img2, (1000, 1000))
    cv2.imshow("img", img2)
    cv2.waitKey(0)

(2)开启相机,进行实时检测。

from face_detect import Detecter 
import cv2 
from PIL import Image 

font = cv2.FONT_HERSHEY_DUPLEX  # 设置字体

if __name__ == "__main__":
    de = Detecter()
    capture = cv2.VideoCapture(0)
    while True:
        ret, frame = capture.read()
        frame = cv2.flip(frame, 1)
        img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        boxes = de.detect_img(img)
        for box in boxes:
            frame = cv2.rectangle(frame, (int(box[1]), int(box[2])), (int(box[3]), int(box[4])),(0,0,255),2)
            cls = round(box[0].item(),2)
            frame = cv2.putText(frame, f"{cls}", (int(box[1]), int(box[2])), font, 1, (254, 0, 0), 2)
        cv2.imshow("Face_Detect", frame)
        cv2.waitKey(50)
    capture.release()
    cv2.destroyAllWindows()


实验结果:


图片检测 相机实时检测,码是后期打上的

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

上一篇下一篇

猜你喜欢

热点阅读