用滴滴云Notebook笔记本服务实现Yolo3口罩检测

滴滴云技术支持发表于:2020年03月12日 20:17:20更新于:2020年05月12日 16:47:05

在本教程中,您将学习如何使用经典的one-stage目标检测网络Yolo v3来实现口罩检测,关于Yolo v3的资料可以阅读paper。 本教程所有工程文件可在滴滴云S3存储服务下载。

import cv2
import math
import matplotlib.pyplot as plt
import numpy as np
import os
import random
import time
import torch
import torchvision
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
import xml.etree.ElementTree as ET

from torch.utils.data import Dataset, DataLoader

下载口罩检测数据集并上传到Notebook服务器,这里我们以AIZOO开源数据集为例,下载地址:https://pan.baidu.com/s/1nsQf_Py5YyKm87-8HiyJeQ ,提取码:eyfz,大文件上传可能需要用到滴滴云S3存储服务。为了便于用户下载,我们提前上传到公共数据集对象存储S3,执行以下shell命令即可,如自行上传可以跳过

!wget https://dataset-public.s3.didiyunapi.com/detection/人脸口罩检测/part1.tgz
!wget https://dataset-public.s3.didiyunapi.com/detection/人脸口罩检测/part2.tgz
!wget https://dataset-public.s3.didiyunapi.com/detection/人脸口罩检测/val.tgz
!tar -zxf part1.tgz
!tar -zxf part2.tgz
!tar -zxf val.tgz

 

--2020-03-05 14:59:25--  https://dataset-public.s3.didiyunapi.com/detection/%E4%BA%BA%E8%84%B8%E5%8F%A3%E7%BD%A9%E6%A3%80%E6%B5%8B/part1.tgz
Resolving dataset-public.s3.didiyunapi.com (dataset-public.s3.didiyunapi.com)... 125.94.54.9
Connecting to dataset-public.s3.didiyunapi.com (dataset-public.s3.didiyunapi.com)|125.94.54.9|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 270682151 (258M) [application/gzip]
Saving to: ‘part1.tgz’
100%[======================================>] 270,682,151 6.15MB/s   in 42s    
2020-03-05 15:00:06 (6.18 MB/s) - ‘part1.tgz’ saved [270682151/270682151]
--2020-03-05 15:00:07--  https://dataset-public.s3.didiyunapi.com/detection/%E4%BA%BA%E8%84%B8%E5%8F%A3%E7%BD%A9%E6%A3%80%E6%B5%8B/part2.tgz
Resolving dataset-public.s3.didiyunapi.com (dataset-public.s3.didiyunapi.com)... 125.94.54.9
Connecting to dataset-public.s3.didiyunapi.com (dataset-public.s3.didiyunapi.com)|125.94.54.9|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 337432016 (322M) [application/gzip]
Saving to: ‘part2.tgz’
100%[======================================>] 337,432,016 6.41MB/s   in 53s    
2020-03-05 15:00:59 (6.10 MB/s) - ‘part2.tgz’ saved [337432016/337432016]
--2020-03-05 15:01:00--  https://dataset-public.s3.didiyunapi.com/detection/%E4%BA%BA%E8%84%B8%E5%8F%A3%E7%BD%A9%E6%A3%80%E6%B5%8B/val.tgz
Resolving dataset-public.s3.didiyunapi.com (dataset-public.s3.didiyunapi.com)... 125.94.54.9
Connecting to dataset-public.s3.didiyunapi.com (dataset-public.s3.didiyunapi.com)|125.94.54.9|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 184383116 (176M) [application/gzip]
Saving to: ‘val.tgz’
100%[======================================>] 184,383,116 6.25MB/s   in 29s    
2020-03-05 15:01:28 (6.12 MB/s) - ‘val.tgz’ saved [184383116/184383116]

加载数据,这里我们需要编写自定义数据集的Dataset类型

VOC_CLASSES = ('face', 'face_mask')
# 标注格式转换类型定义
class AnnotationTransform(object):
    def __init__(self, class_to_ind=None, keep_difficult=True):
        self.class_to_ind = class_to_ind or dict(
            zip(VOC_CLASSES, range(len(VOC_CLASSES))))
        self.keep_difficult = keep_difficult
    def __call__(self, target):
        res = np.empty((0,5)) 
        for obj in target.iter('object'):
            difficult = int(obj.find('difficult').text) == 1
            if not self.keep_difficult and difficult:
                continue
            name = obj.find('name').text.lower().strip()
            bbox = obj.find('bndbox')
            pts = ['xmin', 'ymin', 'xmax', 'ymax']
            bndbox = []
            for i, pt in enumerate(pts):
                cur_pt = int(bbox.find(pt).text) - 1
                bndbox.append(cur_pt)
            label_idx = self.class_to_ind[name]
            bndbox.append(label_idx)
            res = np.vstack((res, bndbox))  # [xmin, ymin, xmax, ymax, label_ind]
        return res  # [[xmin, ymin, xmax, ymax, label_ind], ... ]
def preproc_for_test(image, input_size, mean, std):
    interp_methods = [cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_NEAREST, cv2.INTER_LANCZOS4]
    interp_method = interp_methods[random.randrange(5)]
    image = cv2.resize(image, input_size, interpolation=interp_method)
    image = image.astype(np.float32)
    image = image[:, :, ::-1]
    image /= 255.
    if mean is not None:
        image -= mean
    if std is not None:
        image /= std
    return image.transpose(2, 0, 1)
class TrainTransform(object):
    def __init__(self, rgb_means=None, std=None, max_labels=50):
        self.means = rgb_means
        self.std = std
        self.max_labels = max_labels
    def __call__(self, image, targets, img_size):
        boxes = targets[:, :4].copy()  # Nx4
        labels = targets[:, 4].copy()
        if len(boxes) == 0:
            targets = np.zeros((self.max_labels, 5), dtype=np.float32)
            image = preproc_for_test(image, img_size, self.means, self.std)
            image = np.ascontiguousarray(image, dtype=np.float32)
            return torch.from_numpy(image), torch.from_numpy(targets)
        height, width, _ = image.shape
        boxes_o = targets[:, :4]
        labels = targets[:, 4]
        b_x_o = (boxes_o[:, 2] + boxes_o[:, 0]) * .5
        b_y_o = (boxes_o[:, 3] + boxes_o[:, 1]) * .5
        b_w_o = (boxes_o[:, 2] - boxes_o[:, 0]) * 1.
        b_h_o = (boxes_o[:, 3] - boxes_o[:, 1]) * 1.
        boxes_o[:, 0] = b_x_o
        boxes_o[:, 1] = b_y_o
        boxes_o[:, 2] = b_w_o
        boxes_o[:, 3] = b_h_o
        # resize
        interp_methods = [cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_NEAREST, cv2.INTER_LANCZOS4]
        interp_method = interp_methods[random.randrange(5)]
        image_t = cv2.resize(image, img_size, interpolation=interp_method)
        boxes = boxes_o
        boxes[:, 0::2] /= width
        boxes[:, 1::2] /= height
        boxes[:, 0::2] *= img_size[0]
        boxes[:, 1::2] *= img_size[1]
        image_t = preproc_for_test(image_t, img_size, self.means, self.std)
        
        labels = np.expand_dims(labels, 1)
        targets_t = np.hstack((labels, boxes))
        padded_labels = np.zeros((self.max_labels, 5))
        padded_labels[range(len(targets_t))[:self.max_labels]] = targets_t[:self.max_labels]
        padded_labels = np.ascontiguousarray(padded_labels, dtype=np.float32)
        image_t = np.ascontiguousarray(image_t, dtype=np.float32)
        return torch.from_numpy(image_t), torch.from_numpy(padded_labels)
    
# 数据集类型定义
class VOCDetection(Dataset):
    def __init__(self, root, preproc=None, target_transform=AnnotationTransform(), img_size=(416, 416), split='train'):
        super().__init__()
        self.root = root
        self.preproc = preproc
        self.target_transform = target_transform
        self.img_size = img_size
        self._annopath = os.path.join('%s', 'Annotations', '%s.xml')
        self._imgpath = os.path.join('%s', 'JPEGImages', '%s.jpg')
        self._classes = VOC_CLASSES
        self._year = '2012'  # options: '2007', which is related to eval protocol
        self.item_container = set()
        if split == 'train':
            for folder in ['part1', 'part2']:
                for item in os.listdir(os.path.join(self.root, folder)):
                    self.item_container.add(os.path.join(self.root, folder, item[:-4]))
        else:
            for folder in ['val']:
                for item in os.listdir(os.path.join(self.root, folder)):
                    self.item_container.add(os.path.join(self.root, folder, item[:-4]))
        self.item_container = list(self.item_container)
    def __getitem__(self, index):
        item = self.item_container[index]
        target = ET.parse(item+'.xml').getroot()
        img = cv2.imread(item+'.jpg')
        # img = Image.open(self._imgpath % img_id).convert('RGB')
        height, width, _ = img.shape
        if self.target_transform is not None:
            target = self.target_transform(target)
        if self.preproc is not None:
            img, target = self.preproc(img, target, self.img_size)
        img_info = (width, height)
        return img, target, img_info, item
    def __len__(self):
        return len(self.item_container)
    
dataset = VOCDetection(root='./', preproc=TrainTransform(),split='train')

定义模型,对模型参数进行初始化操作,这里需要导入自定义库yolo,可在滴滴云S3存储服务下载。

from yolo import YOLOv3
model = YOLOv3(num_classes = len(VOC_CLASSES))
def init_yolo(M):
    for m in M.modules():
        if isinstance(m, nn.Conv2d):
            init.kaiming_normal_(m.weight, a=0.1, mode='fan_in')
            if m.bias is not None:
                init.zeros_(m.bias)
        elif isinstance(m, nn.BatchNorm2d):
            init.ones_(m.weight)
            init.zeros_(m.bias)
        elif isinstance(m, nn.Linear):
            init.normal_(m.weight, 0, 0.01)
            init.zeros_(m.bias)
            m.state_dict()[key][...] = 0
model.apply(init_yolo)
model.train()
torch.backends.cudnn.benchmark = True
device = torch.device("cuda")
model = model.to(device)

现在我们使用Adam算法来训练模型

# 在训练之前我们先定义一些超参数
batch_size = 8  # 每一批次训练大小,不宜太小,大小受GPU显存限制
base_lr = 0.0001  # 基准学习率
warmup_epochs = 10  # 学习率逐渐增加到base_lr的epoch
epochs = 70  # 总共训练epoch数
save_interval = 10  # 保存模型的epoch间隔
steps = [50, 60]  # 学习率减少的epoch
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)
optimizer = optim.Adam(model.parameters(), lr=base_lr, weight_decay=0.0005)
epoch_size = len(dataset) // (batch_size*1)
epoch = 1
def set_lr(tmp_lr):
    for param_group in optimizer.param_groups:
        param_group['lr'] = tmp_lr
while epoch < epochs+1:
    print('\n[Epoch {} started]'.format(epoch))
    for iter_i,  (imgs, targets, _, _) in enumerate(dataloader):
        start = time.time()
        if epoch % save_interval == 0:
            torch.save(model.state_dict(), 'yolov3_mask_detection_{}.pth'.format(epoch))
        # 更新学习率
        if epoch < warmup_epochs:
            tmp_lr = base_lr * pow((iter_i+epoch*epoch_size)*1. / (warmup_epochs*epoch_size), 1)
            set_lr(tmp_lr)
        elif epoch == warmup_epochs:
            tmp_lr = base_lr
            set_lr(tmp_lr)
        elif epoch in steps and iter_i == 0:
            tmp_lr = tmp_lr * 0.1
            set_lr(tmp_lr)
        optimizer.zero_grad()
        imgs = imgs.to(device).to(torch.float32)
        targets = targets.to(device).to(torch.float32)
        loss_dict = model(imgs, targets, epoch)
        loss = sum(loss for loss in loss_dict['losses'])
        loss.backward()
        optimizer.step()
        end = time.time()
        if iter_i % 1 == 0:
        # 打印训练过程信息
            print('\r[Epoch %d/%d][Iter %d/%d][LR %.6f]'
                '[Loss: l1 %.2f, conf %.6f, cls %.6f][Time: %.2f s]......'
            % (epoch, epochs, iter_i+1, epoch_size, tmp_lr,
             sum(l1_loss for l1_loss in loss_dict['l1_losses']).item(),
             sum(conf_loss for conf_loss in loss_dict['conf_losses']).item(),
             sum(cls_loss for cls_loss in loss_dict['cls_losses']).item(), end-start),
            end='')
    epoch += 1
torch.save(model.state_dict(), 'yolov3_mask_detection_final.pth'.format(epoch))
[Epoch 1 started]
[Epoch 1/70][Iter 765/765][LR 0.000020][Loss: l1 4.36, conf 1747.822632, cls 1.573278][Time: 0.61 s]........
[Epoch 2 started]
[Epoch 2/70][Iter 765/765][LR 0.000030][Loss: l1 5.01, conf 781.659180, cls 1.723707][Time: 0.63 s].........
[Epoch 3 started]
[Epoch 3/70][Iter 765/765][LR 0.000040][Loss: l1 18.94, conf 331.378754, cls 5.523039][Time: 0.69 s].......
[Epoch 4 started]
[Epoch 4/70][Iter 78/765][LR 0.000041][Loss: l1 6.72, conf 293.010193, cls 1.820950][Time: 0.66 s].......
KeyboardInterrupt:

得到训练的模型之后可以开始测试

class ValTransform(object):
    def __init__(self, rgb_means=None, std=None, swap=(2, 0, 1)):
        self.means = rgb_means
        self.swap = swap
        self.std = std
    def __call__(self, img, res, input_size):
        interp_methods = [cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_NEAREST, cv2.INTER_LANCZOS4]
        interp_method = interp_methods[0]
        img = cv2.resize(np.array(img), input_size, interpolation=interp_method).astype(np.float32)
        img = img[:, :, ::-1]
        img /= 255.
        if self.means is not None:
            img -= self.means
        if self.std is not None:
            img /= self.std
        img = img.transpose(self.swap)
        img = np.ascontiguousarray(img, dtype=np.float32)
        return torch.from_numpy(img), torch.zeros(1, 5)
    
transform = ValTransform()
im = cv2.imread("val/test_00000760.jpg")  # 输入的图片
ori_im = im.copy()
height, width, _ = im.shape
test_size = (416, 416)
im_input, _ = transform(im, None, test_size)
im_input = im_input.to(device).type(torch.float32).unsqueeze(0)
model.load_state_dict(torch.load('yolov3_mask_detection_final.pth'))  # 加载训练权重
device = torch.device("cuda")
model = model.to(device)
model.eval()
outputs = model(im_input)

对模型输出做后处理,除去置信度较低的bbox,并利用非极大抑制(NMS)去除同类型IoU较大的bbox

def postprocess(prediction, num_classes=2, conf_thre=0.3, nms_thre=0.45):
    box_corner = prediction.new(prediction.shape)
    box_corner[:, :, 0] = prediction[:, :, 0] - prediction[:, :, 2] / 2
    box_corner[:, :, 1] = prediction[:, :, 1] - prediction[:, :, 3] / 2
    box_corner[:, :, 2] = prediction[:, :, 0] + prediction[:, :, 2] / 2
    box_corner[:, :, 3] = prediction[:, :, 1] + prediction[:, :, 3] / 2
    prediction[:, :, :4] = box_corner[:, :, :4]
    output = [None for _ in range(len(prediction))]
    for i, image_pred in enumerate(prediction):
        # If none are remaining => process next image
        if not image_pred.size(0):
            continue
        # Get score and class with highest confidence
        class_conf, class_pred = torch.max(
            image_pred[:, 5:5 + num_classes], 1,  keepdim=True)
        conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= conf_thre).squeeze()
        # Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred)
        detections = torch.cat(
            (image_pred[:, :5], class_conf, class_pred.float()), 1)
        detections = detections[conf_mask]
        if not detections.size(0):
            continue
        # Iterate through all predicted classes
        unique_labels = detections[:, -1].unique()
        for c in unique_labels:
            # Get the detections with the particular class
            detections_class = detections[detections[:, -1] == c]
            nms_out_index = torchvision.ops.nms(
                detections_class[:, :4], detections_class[:, 4]*detections_class[:, 5], nms_thre)
            detections_class = detections_class[nms_out_index]
            if output[i] is None:
                output[i] = detections_class
            else:
                output[i] = torch.cat((output[i], detections_class))
    return output
outputs = postprocess(outputs, 2, 0.01, 0.35)
outputs = outputs[0].cpu().data
bboxes = outputs[:, 0:4]
bboxes[:, 0::2] *= width / test_size[0]
bboxes[:, 1::2] *= height / test_size[1]
bboxes[:, 2] = bboxes[:, 2] - bboxes[:, 0]
bboxes[:, 3] = bboxes[:, 3] - bboxes[:, 1]
cls = outputs[:, 6]
scores = outputs[:, 4] * outputs[:, 5]

最后我们将处理得到的结果可视化出来

def vis(img, boxes, scores, cls_ids, conf=0.5, class_names=None, color=None):
    colors = torch.FloatTensor([[1,0,1],[0,0,1],[0,1,1],[0,1,0],[1,1,0],[1,0,0]]);
    def get_color(c, x, max_val):
        ratio = float(x)/max_val * 5
        i = int(math.floor(ratio))
        j = int(math.ceil(ratio))
        ratio = ratio - i
        r = (1-ratio) * colors[i][c] + ratio*colors[j][c]
        return int(r*255)
    width = img.shape[1]
    height = img.shape[0]
    for i in range(len(boxes)):
        box = boxes[i]
        cls_conf = scores[i]
        if cls_conf < conf:
            continue
        x1 = int(box[0])
        y1 = int(box[1])
        x2 = int(box[0]+box[2])
        y2 = int(box[1]+box[3])
        if color:
            rgb = color
        else:
            rgb = (255, 0, 0)
        if class_names is not None:
            cls_conf = scores[i]
            cls_id = int(cls_ids[i])
            class_name = class_names[cls_id]
            classes = len(class_names)
            offset = cls_id * 123456 % classes
            red   = get_color(2, offset, classes)
            green = get_color(1, offset, classes)
            blue  = get_color(0, offset, classes)
            if color is None:
                rgb = (red, green, blue)
            img = cv2.putText(img, '%s: %.2f'%(class_name,cls_conf), (x1,y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, rgb, 2)
        img = cv2.rectangle(img, (x1,y1), (x2,y2), rgb, 1)
    return img
pred_im = vis(ori_im, bboxes.numpy(), scores.numpy(), cls.numpy(), conf=0.3, class_names=VOC_CLASSES)
plt.rcParams['figure.figsize'] = (20, 12)
plt.imshow(pred_im[:,:,::-1])
<matplotlib.image.AxesImage at 0x7f0c4cfc7950>

0015e6a27a5a1671b03db56e2718f19

需要说明的是本教程只是用于入门学习,输入图像尺寸选择416x416,没有加入复杂的数据增广算法、网络训练优化tricks,这些因素都导致模型存在人群密集情况漏检以及小人头漏检问题。如果希望获得更好的效果,可以使用人脸检测模型,混入这个口罩数据集,这样能够利用更多的人脸数据。最后对检测出的人脸图像做2分类即可。