完整代码已经上传,可以关注微信公众号 "老居搞机" 回复关键词 "人脸识别" 获取源代码后直接运行,一边吃着西瓜一边看着这篇一边Run着,效果最佳!

人脸识别在生活中已经越来越常见了,手机解锁刷刷脸,进高铁站刷个脸,去酒店入住刷个脸,就连跑了十几年的逃犯最近也因为人脸识别的普及而屡屡被抓获!

计算机是怎么认识一张脸对应是哪个人的呢,听上去很神奇,这篇就用Pytorch手把手教你做一个

在开始之前,先做一下准备工作,列一张我们需要用到的清单:

一个摄像头,用于拍摄人脸视频
Opencv软件包,用于对人脸的图像进行相应的处理
卷积神经网络(CNN)知识, 如果不熟悉可以阅读上一篇文章:卷积神经网络(CNN)
Pytorch软件包,用于创建CNN训练人脸识别模型的深度学习框架
两张人脸,目的不言自明,本篇我用上了小居来充当道具

首先使用Opencv来获取摄像头的视频流:

# -*- coding: utf-8 -*-
import cv2

def catch_video(tag, window_name='catch face', camera_idx=0):
    cv2.namedWindow(window_name)
  # 视频来源,可以来自一段已存好的视频,也可以直接来自摄像头
    cap = cv2.VideoCapture(camera_idx)
    while cap.isOpened():
       # 读取一帧数据
        ok, frame = cap.read()
        if not ok:
            break
      # 抓取人脸的方法, 后面介绍
        catch_face(frame, tag)
        # 输入'q'退出程序
        cv2.imshow(window_name, frame)
        c = cv2.waitKey(1)
        if c & 0xFF == ord('q'):
            break
  # 释放摄像头并销毁所有窗口
    cap.release()
    cv2.destroyAllWindows()

方法里面的参数介绍一下:

  • camera_id:这个就是摄像头的设备索引号,一般是第一个0
  • tag:抓取谁的人脸的标记, 后面根据tag来保存目录
  • window_name:窗口的名字

从实时视频流抓取人脸区域,这部分事情属于目标检测,可以使用Faster-R-CNN等来做的,不过这里我们暂时不深入这部分,直接使用Opencv已有的函数来获取人脸的区域即可:

def catch_face(frame, tag):
    # 告诉OpenCV使用人脸识别分类器
    classfier = cv2.CascadeClassifier("/Users/alan/.virtualenvs/face_recognize/lib/python2.7/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
    # 识别出人脸后要画的边框的颜色,RGB格式
    color = (0, 255, 0)
    # 将当前帧转换成灰度图像
    grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)    
    # 人脸检测,1.2和2分别为图片缩放比例和需要检测的有效点数
    face_rects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
    num = 1
    if len(face_rects) > 0: # 大于0则检测到人脸
        # 图片帧中有多个图片,框出每一个人脸
        for face_rects in face_rects:
            x, y, w, h = face_rects
            image = frame[y - 10:y + h + 10, x - 10:x + w + 10]
            # 保存人脸图像
            save_face(image, tag, num)
            cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
            num += 1

def save_face(image, tag, num):
  # DATA_TRAIN为抓取的人脸存放目录,如果目录不存在则创建
    makedir_exist_ok(os.path.join(DATA_TRAIN, str(tag)))
    img_name = os.path.join(DATA_TRAIN, str(tag), '{}_{}.jpg'.format(int(time.time()), num))
    # 保存人脸图像到指定的位置, 其中会创建一个tag对应的目录,用于后面的分类训练
    cv2.imwrite(img_name, image)

来看一下抓取老居这张老脸的效果:

img

这里主要是Opencv帮我们做的人脸抓取, 解释下这一行:

classfier = cv2.CascadeClassifier("/Users/alan/.virtualenvs/face_recognize/lib/python2.7/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
  • CascadeClassifier方法指定OpenCV选择使用哪种分类器,OpenCV提供了多种分类器,也可以使用别的分类器看看效果, 这些分类器都放在对应安装包cv2/data/目录下
face_rects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))

classfier.detectMultiScale()即是完成实际人脸识别工作的函数

该函数参数说明如下:

  • grey:要识别的图像数据(即使不转换成灰度也能识别,但是灰度图可以降低计算强度,因为检测的依据是哈尔特征,转换后每个点的RGB数据变成了一维的灰度,这样计算强度就减少很多)
  • scaleFactor:图像缩放比例,可以理解为同一个物体与相机距离不同,其大小亦不同,必须将其缩放到一定大小才方便识别,该参数指定每次缩放的比例
  • minNeighbors:对特征检测点周边多少有效点同时检测,这样可避免因选取的特征检测点太小而导致遗漏
  • minSize:特征检测点的最小值

DATA_TRAIN 即抓取的人脸保存的位置,比如我放在项目目录下的 data/train/下面,整体的配置参数如下:

# -*- encoding: utf8 -*-
import os

PROJECT_PATH = os.path.abspath(
    os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir))

# 训练数据集
DATA_TRAIN = os.path.join(PROJECT_PATH, "data/train")
# 验证数据集
DATA_TEST = os.path.join(PROJECT_PATH, "data/test")
# 模型保存地址
DATA_MODEL = os.path.join(PROJECT_PATH, "data/model")
  • DATA_TRAIN:训练集数据存放的目录
  • DATA_TEST:验证集数据存放的目录
  • DATA_MODEL:模型保存目录

一帧图片里面可能会出现多张人脸,循环处理每张人脸图片, 运行这个程序抓取到足够人脸数据之后,到DATA_TRAIN对应的目录下将错误的和不清晰的图片做一些删减,只保留清晰的人脸图片,并且拷一部分到DATA_TEST对应的目录下用作为验证集使用, 然后就可以进入到我们的下一步了

上面一步我们已经抓取到不同类型足够的人脸图片了,接下来我们加载这些图片进行一些必要的预处理便于后面的模型训练,这里使用Pytorch框架。

Pytorch是Facebook开源的深度学习框架,具有先进的设计理念,提供的API简单易用,上手非常容易,性能卓越,推荐使用(一波小广告666)

准备dataset,对图片做一些预处理,如缩放到标准尺寸,归一化,对图像做一些增强如:旋转、切割等(不过我们这里使用摄像头都是垂直的,所以就不做图像增强了),先上代码再慢慢解释:

# -*- encoding: utf8 -*-
import config
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import ImageFolder

def get_transform():
    return transforms.Compose([
            # 图像缩放到32 x 32
            transforms.Resize(32),
            # 中心裁剪 32 x 32
            transforms.CenterCrop(32),
            transforms.ToTensor(),
            # 对每个像素点进行归一化
            transforms.Normalize(mean=[0.4, 0.4, 0.4],
                                 std=[0.2, 0.2, 0.2])
        ])

def get_dataset(batch_size=10, num_workers=1):
    data_transform = get_transform()
    # load训练集图片
    train_dataset = ImageFolder(root=config.DATA_TRAIN, transform=data_transform)
    # load验证集图片
    test_dataset = ImageFolder(root=config.DATA_TEST, transform=data_transform)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    return train_loader, test_loader

这里解释一下几段:

transforms.Compose([
            # 图像缩放到32 x 32
            transforms.Resize(32),
            # 中心裁剪 32 x 32
            transforms.CenterCrop(32),
            transforms.ToTensor(),
            # 对每个像素点进行归一化
            transforms.Normalize(mean=[0.4, 0.4, 0.4],
                                 std=[0.2, 0.2, 0.2])
        ])
  • transforms.Compose 这个类的主要作用是串联多个图片变换的操作
  • transforms.Resize 将图像缩放到32x32,因为我们用opencv抓取的图片尺寸是不一样的,这里统一缩放到32x32
  • transforms.CenterCrop 进行中心剪裁
  • transforms.ToTensor 把像素灰度范围从0-255变换到0-1之间
  • transforms.Normalize 对图像进行归一化, 把0-1变换到(-1,1).具体地说,对每个通道而言,Normalize执行以下操作: img=(img-mean)/std
  # load训练集图片
    train_dataset = ImageFolder(root=config.DATA_TRAIN, transform=data_transform)
    # load验证集图片
    test_dataset = ImageFolder(root=config.DATA_TEST, transform=data_transform)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    return train_loader, test_loader

train_dataset为训练集数据,test_dataset为验证集数据

ImageFolder是Pytorch框架提供的图片目录加载类,可以极大的减少我们加载目录中图片的工作量,它主要有四个参数:

  • root:在root指定的路径下寻找图片
  • transform:对PIL Image进行的转换操作,transform的输入是使用loader读取图片的返回对象,也就是我们上面指定的transforms
  • target_transform:对label的转换
  • loader:给定路径后如何读取图片,默认读取为RGB格式的PIL Image对象

DataLoader:生成Pytorch训练时候使用的可迭代对象数据,用到的参数:

  • batch_size:每次迭代使用多少个样本
  • shuffle:每次重新迭代时候,是否对数据进行随机重新排序
  • num_workers:几个进程来处理data loading

调用get_dataset() 方法来获取训练集和验证集,下一步针对这些数据进行训练

我们使用卷积神经网络(CNN)来识别人脸,关于卷积神经网络的详细说明可以看上一篇:卷积神经网络(CNN)

创建用于人脸识别的卷积神经网络:

# -*- encoding: utf8 -*-
from torch import nn

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 第一层卷积->激活->池化->Dropout
        self.conv1 = nn.Sequential(
            nn.Conv2d(
                in_channels=3,
                out_channels=16,
                kernel_size=5,
                stride=1,
                padding=2,
            ),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
            nn.Dropout(0.2)
        )
        # 第二层卷积->激活->池化->Dropout
        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Dropout(0.2)
        )
        # 全连接层 
        self.out = nn.Linear(32 * 8 * 8, 2)
        
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        x = self.out(x)
        # 对结果进行log + softmax并输出
        return F.log_softmax(x, dim=1)

解释一下主要的几个方法:

self.conv1 = nn.Sequential(
            nn.Conv2d(
                in_channels=3,
                out_channels=16,
                kernel_size=5,
                stride=1,
                padding=2,
            ),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
            nn.Dropout(0.2)
        )
  • nn.Conv2d:卷积计算,in_channels输入的RGB图像为3层,out_channels输出16层, kernel_size卷积核为5x5, stride步长为1x1, 也就是每次卷积计算自后向右(下)移动一位, padding图像周围加2x2的边框, 卷积核为5x5, 图片加上2x2的边框之后卷积计算得到的结果还是32x32大小的图片(32 - 5 + 2 + 2)
  • nn.ReLU:使用ReLU激活函数
  • nn.MaxPool2d:池化层2x2大小,32x32大小的图片经过2x2的池化之后就是16x16(32/2, 32/2)大小的图片了
  • nn.Dropout:随机丢弃0.2的神经元权重, Dropout是深度神经网络防止过拟合很好用的方法(所谓过拟合是指在测试/验证集上表现比训练集上预测效果差很多)

self.conv2和self.conv1一样也是 卷积->激活->池化->Dropout

全连接层 32x32的图片经过两次池化之后大小为8x8,上一层卷积输出是32层,所以总共的大小就是 32 x 8 x 8个神经元, 输出2个分类(如果有多个人输出可以改成更多)

self.out = nn.Linear(32 * 8 * 8, 2)

forward是Pytorch神经网络的推理过程, 将前面的计算串联在一起计算结果:

def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        x = self.out(x)
        return F.log_softmax(x, dim=1)
  • F.log_softmax:对结果取log softmax

下面使用这个神经网络来训练已经准备好的数据:

# -*- encoding: utf8 -*-
import config
import os
import torch
from data_set import get_dataset, get_transform
from model import Net
import torch.nn.functional as F

## 检查是否有GPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def train_model():
    train_loader, test_loader = get_dataset(batch_size=config.BATCH_SIZE)
    net = Net().to(DEVICE)
    # 使用Adam优化器
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    for epoch in range(config.EPOCHS):
        for step, (x, y) in enumerate(train_loader):
            x, y = x.to(DEVICE), y.to(DEVICE)
            output = net(x)
            # 使用最大似然 / log似然代价函数
            loss = F.nll_loss(output, y)
            # Pytorch会梯度累计所以需要梯度清零
            optimizer.zero_grad()
            # 反向传播
            loss.backward()
            # 使用Adam进行梯度更新
            optimizer.step()

            if (step + 1) % 3 == 0:
                print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                    epoch + 1, step * len(x), len(train_loader.dataset),
                    100. * step / len(train_loader), loss.item()))
    # 使用验证集查看模型效果
    test(net, test_loader)
    # 保存模型权重到 config.DATA_MODEL目录
    torch.save(net.state_dict(), os.path.join(config.DATA_MODEL, config.DEFAULT_MODEL))
    return net

这里需要重点说明的是:

# 检查是否有GPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  • 检查是否有GPU, 如果有GPU则使用cuda, 后面模型和数据可以使用如Net().to(DEVICE)转为GPU的Tensor
loss = F.nll_loss(output, y)
# 使用Adam优化器
optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
  • 使用Adam优化器,后面执行optimizer.step()进行梯度更新,关于优化器可以看之前写的这篇:带动量的梯度下降
# 保存模型权重到 config.DATA_MODEL目录
torch.save(net.state_dict(), os.path.join(config.DATA_MODEL, config.DEFAULT_MODEL))
  • 保存训练好的模型权重到config.DATA_MODEL目录,如我这边是放在 data/model目录下

使用验证集数据用于验证模型的好坏,关于模型好坏的评价指标,可以看之前的这篇文章: 机器学习的效果评价指标

def test(model, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for x, y in test_loader:
            x, y = x.to(DEVICE), y.to(DEVICE)
            output = model(x)
            test_loss += F.nll_loss(output, y, reduction='sum').item()
            pred = output.max(1, keepdim=True)[1]
            correct += pred.eq(y.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    print '\ntest loss={:.4f}, accuracy={:.4f}\n'.format(test_loss, float(correct) / len(test_loader.dataset))
  • loss:验证集合上的损失
  • accuracy:验证集上的准确率

创建文件main.py作为入口文件,执行一下看看效果:

# -*- encoding: utf8 -*-
import fire
import logging
from face.catch_face import catch_video
from model.train import train_model

def catch(tag):
    catch_video(tag)
    logging.info("catch_face done.")

def train():
    train_model()
    logging.info("train done.")

if __name__ == "__main__":
    logging.getLogger().setLevel(logging.INFO)
    fire.Fire()

训练模型

  • $ python main.py train

img

此时我们就训练好了Pytorch模型,模型的参数权重保存在了data/model目录下,下一步使用这个模型来识别一下摄像头中抓取的人脸

好了到了我们的最后一步了,抓取摄像头的人脸并放到训练的模型中识别出对应的人:

def recognize_video(window_name='face recognize', camera_idx=0):
    cv2.namedWindow(window_name)
    cap = cv2.VideoCapture(camera_idx)
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        catch_frame = catch_face(frame)
        cv2.imshow(window_name, catch_frame)
        c = cv2.waitKey(1)
        if c & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

def catch_face(frame):
    classfier = cv2.CascadeClassifier("/Users/alan/.virtualenvs/kepler/lib/python2.7/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
    color = (0, 255, 0)
    grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    face_rects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
    if len(face_rects) > 0:
        for face_rects in face_rects:
            x, y, w, h = face_rects
            image = frame[y - 10:y + h + 10, x - 10:x + w + 10]
            # opencv 2 PIL格式图片
            PIL_image = cv2pil(image)
            # 使用模型进行人脸识别
            label = predict_model(PIL_image)
            cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
            # 将人脸对应人名写到图片上, 以为是中文名所以需要加载中文字体库
            frame = paint_chinese_opencv(frame, FACE_LABEL[label], (x-10, y+h+10), color)

    return frame

这一步跟第一步的抓取人脸Opencv用法差不多,就不多说明了

因为抓取摄像头图片使用的Opencv, Pytorch识别图像使用PIL格式,所以需要做个转换:

def cv2pil(image):
    return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))

加载我们现有的模型进行预测:

def predict_model(image):
    data_transform = get_transform()
    # 对图片进行预处理,同训练的时候一样
    image = data_transform(image)
    image = image.view(-1, 3, 32, 32)
    net = Net().to(DEVICE)
    # 加载模型参数权重
    net.load_state_dict(torch.load(os.path.join(config.DATA_MODEL, config.DEFAULT_MODEL)))
    output = net(image.to(DEVICE))
    # 获取最大概率的下标
    pred = output.max(1, keepdim=True)[1]
    return pred.item()
  • 重点说一下net.load_state_dict()方法,用于加载刚才我们训练的模型参数权重,这是预测新图片的关键

对预测出来的人脸类型在图像上进行打标,这里为了支持中文引用Songti.ttc字体:

def paint_chinese_opencv(im, chinese, pos, color):
    img_PIL = Image.fromarray(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
    # 引用字体库
    font = ImageFont.truetype('/Library/Fonts/Songti.ttc', 20)
    fillColor = color
    position = pos
    if not isinstance(chinese, unicode):
        chinese = chinese.decode('utf-8')
    draw = ImageDraw.Draw(img_PIL)
    # 写上人脸对应的人名
    draw.text(position, chinese, font=font, fill=fillColor)
    img = cv2.cvtColor(np.asarray(img_PIL), cv2.COLOR_RGB2BGR)
    return img

在main.py文件里面新增方法:

from face.recognize_face import recognize_video

def predict():
    recognize_video()
    logging.info("predict done.")

执行一下进行人脸识别:

  • $ python main.py predict

这时它已经能够从摄像头拍摄的实时视频流中找出哪一个是老居,哪一个是小居了

img

Last modification:September 17th, 2020 at 10:57 pm
如果觉得我的文章对你有用,请随意赞赏