本文講講述如何使用Python在自定義人臉檢測(cè)數(shù)據(jù)集上微調(diào)預(yù)訓(xùn)練的目標(biāo)檢測(cè)模型。學(xué)習(xí)如何為Detectron2和PyTorch準(zhǔn)備自定義人臉檢測(cè)數(shù)據(jù)集,微調(diào)預(yù)訓(xùn)練模型以在圖像中找到人臉邊界。
人臉檢測(cè)是在圖像中找到(邊界的)人臉的任務(wù)。這在以下情況下很有用:
安全系統(tǒng)(識(shí)別人員的第一步)
為拍攝出色的照片進(jìn)行自動(dòng)對(duì)焦和微笑檢測(cè)
檢測(cè)年齡、種族和情感狀態(tài)以用于營(yíng)銷(xiāo)
歷史上,這是一個(gè)非常棘手的問(wèn)題。大量的手動(dòng)特征工程、新穎的算法和方法被開(kāi)發(fā)出來(lái)以改進(jìn)最先進(jìn)技術(shù)。
如今,人臉檢測(cè)模型已經(jīng)包含在幾乎每個(gè)計(jì)算機(jī)視覺(jué)包/框架中。其中一些表現(xiàn)最佳的模型使用了深度學(xué)習(xí)方法。例如,OpenCV提供了各種工具,如級(jí)聯(lián)分類(lèi)器。
在本指南中,您將學(xué)習(xí)如何:
準(zhǔn)備一個(gè)用于人臉檢測(cè)的自定義數(shù)據(jù)集,以用于Detectron2
使用(接近)最先進(jìn)的目標(biāo)檢測(cè)模型在圖像中查找人臉
您可以將這項(xiàng)工作擴(kuò)展到人臉識(shí)別
Detectron2
Detectron2是一個(gè)用于構(gòu)建最先進(jìn)的目標(biāo)檢測(cè)和圖像分割模型的框架,由Facebook Research團(tuán)隊(duì)開(kāi)發(fā)。Detectron2是第一個(gè)版本的完全重寫(xiě)。Detectron2使用PyTorch(與最新版本兼容),并且允許進(jìn)行超快速訓(xùn)練。您可以在Facebook Research的入門(mén)博客文章中了解更多信息。
Detectron2的真正強(qiáng)大之處在于模型動(dòng)物園中提供了大量的預(yù)訓(xùn)練模型。但是,如果您不能在自己的數(shù)據(jù)集上對(duì)其進(jìn)行微調(diào),那又有什么好處呢?幸運(yùn)的是,這非常容易!在本指南中,我們將看到如何完成這項(xiàng)工作。
安裝Detectron2
在撰寫(xiě)本文時(shí),Detectron2仍處于alpha階段。雖然有官方版本,但我們將從主分支克隆和編譯。這應(yīng)該等于版本0.1。讓我們首先安裝一些要求:
!pip install -q cython pyyaml == 5.1
!pip install -q -U 'git+https://github.com/cocodataset/cocoapi.git#subdirectory=PythonAPI'
然后,下載、編譯和安裝Detectron2包:
!git clone https://github.com/facebookresearch/detectron2 detectron2_repo
!pip install -q -e detectron2_repo
此時(shí),您需要重新啟動(dòng)筆記本運(yùn)行時(shí)以繼續(xù)!
%reload_ext watermark %watermark -v -p numpy,pandas,pycocotools,torch,torchvision,detectron2
CPython 3.6.9
IPython 5.5.0
numpy 1.17.5
pandas 0.25.3
pycocotools 2.0
torch 1.4.0
torchvision 0.5.0
detectron2 0.1
import torch, torchvision
import detectron2
from detectron2.utils.logger import setup_logger
setup_logger()
import glob
import os
import ntpath
import numpy as np
import cv2
import random
import itertools
import pandas as pd
from tqdm import tqdm
import urllib
import json
import PIL.Image as Image
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor, DefaultTrainer
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer, ColorMode
from detectron2.data import DatasetCatalog, MetadataCatalog, build_detection_test_loader
from detectron2.evaluation import COCOEvaluator, inference_on_dataset
from detectron2.structures import BoxMode
import seaborn as sns
from pylab import rcParams
import matplotlib.pyplot as plt
from matplotlib import rc
%matplotlib inline
%config InlineBackend.figure_format='retina'
sns.set(style='whitegrid', palette='muted', font_scale=1.2)
HAPPY_COLORS_PALETTE = ["#01BEFE", "#FFDD00", "#FF7D00", "#FF006D", "#ADFF02", "#8F00FF"]
sns.set_palette(sns.color_palette(HAPPY_COLORS_PALETTE))
rcParams['figure.figsize'] = 12, 8
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
人臉檢測(cè)數(shù)據(jù)
該數(shù)據(jù)集在公共領(lǐng)域免費(fèi)提供。它由Dataturks提供,并托管在Kaggle上:圖像中標(biāo)有邊界框的人臉。有大約500張圖像,通過(guò)邊界框手動(dòng)標(biāo)記了大約1100個(gè)人臉。
我已經(jīng)下載了包含注釋的JSON文件,并將其上傳到了Google Drive。讓我們獲取它:
!gdown --id 1K79wJgmPTWamqb04Op2GxW0SW9oxw8KS
讓我們將文件加載到Pandas數(shù)據(jù)框中:
faces_df = pd.read_json('face_detection.json', lines=True)
每行包含一個(gè)單獨(dú)的人臉注釋。請(qǐng)注意,多行可能指向單個(gè)圖像(例如,每個(gè)圖像有多個(gè)人臉)。
數(shù)據(jù)預(yù)處理
數(shù)據(jù)集僅包含圖像URL和注釋。我們將不得不下載這些圖像。我們還將對(duì)注釋進(jìn)行標(biāo)準(zhǔn)化,以便稍后在Detectron2中更容易使用:
os.makedirs("faces", exist_ok=True)
dataset = []
for index, row in tqdm(faces_df.iterrows(), total=faces_df.shape):
img = urllib.request.urlopen(row["content"])
img = Image.open(img)
img = img.convert('RGB')
image_name = f'face_{index}.jpeg'
img.save(f'faces/{image_name}', "JPEG")
annotations = row['annotation']
for an in annotations:
data = {}
width = an['imageWidth']
height = an['imageHeight']
points = an['points']
data['file_name'] = image_name
data['width'] = width
data['height'] = height
data["x_min"] = int(round(points["x"] * width))
data["y_min"] = int(round(points["y"] * height))
data["x_max"] = int(round(points["x"] * width))
data["y_max"] = int(round(points["y"] * height))
data['class_name'] = 'face'
dataset.append(data)
讓我們將數(shù)據(jù)放入數(shù)據(jù)框中,以便我們可以更好地查看:
df = pd.DataFrame(dataset)
print(df.file_name.unique().shape, df.shape)
409 1132
我們總共有409張圖像(比承諾的500張少得多)和1132個(gè)注釋。讓我們將它們保存到磁盤(pán)上(以便您可以重用它們):
數(shù)據(jù)
讓我們查看一些示例注釋數(shù)據(jù)。我們將使用OpenCV加載圖像,添加邊界框并調(diào)整大小。我們將定義一個(gè)助手函數(shù)來(lái)完成所有這些操作:
def annotate_image(annotations, resize=True):
file_name = annotations.file_name.to_numpy()
img = cv2.cvtColor(cv2.imread(f'faces/{file_name}'), cv2.COLOR_BGR2RGB)
for i, a in annotations.iterrows():
cv2.rectangle(img, (a.x_min, a.y_min), (a.x_max, a.y_max), (0, 255, 0), 2)
if not resize:
return img
return cv2.resize(img, (384, 384), interpolation = cv2.INTER_AREA)
讓我們首先顯示一些帶注釋的圖像:
這些都是不錯(cuò)的圖像,注釋清晰可見(jiàn)。我們可以使用torchvision創(chuàng)建一個(gè)圖像網(wǎng)格。請(qǐng)注意,這些圖像具有不同的大小,因此我們將對(duì)其進(jìn)行調(diào)整大小:
您可以清楚地看到一些注釋缺失(第4列)。這就是現(xiàn)實(shí)生活中的數(shù)據(jù),有時(shí)您必須以某種方式處理它。
使用Detectron 2進(jìn)行人臉檢測(cè)
現(xiàn)在,我們將逐步介紹使用自定義數(shù)據(jù)集微調(diào) 模型的步驟。但首先,讓我們保留5%的數(shù)據(jù)進(jìn)行測(cè)試:
df = pd.read_csv('annotations.csv')
IMAGES_PATH = f'faces'
unique_files = df.file_name.unique()
train_files = set(np.random.choice(unique_files, int(len(unique_files) * 0.95), replace=False))
train_df = df[df.file_name.isin(train_files)]
test_df = df[~df.file_name.isin(train_files)]
在這里,經(jīng)典的訓(xùn)練測(cè)試分割方法不適用,因?yàn)槲覀兿M谖募g進(jìn)行分割。
接下來(lái)的部分以稍微通用的方式編寫(xiě)。顯然,我們只有一個(gè)類(lèi)別-人臉。但是,添加更多類(lèi)別應(yīng)該就像向數(shù)據(jù)框中添加更多注釋一樣簡(jiǎn)單:
classes = df.class_name.unique().tolist()
接下來(lái),我們將編寫(xiě)一個(gè)將我們的數(shù)據(jù)集轉(zhuǎn)換為Detectron2:
def create_dataset_dicts(df, classes):
dataset_dicts = []
for image_id, img_name in enumerate(df.file_name.unique()):
record = {}
image_df = df[df.file_name == img_name]
file_path = f'{IMAGES_PATH}/{img_name}'
record["file_name"] = file_path
record["image_id"] = image_id
record["height"] = int(image_df.iloc.height)
record["width"] = int(image_df.iloc.width)
objs = []
for _, row in image_df.iterrows():
xmin = int(row.x_min)
ymin = int(row.y_min)
xmax = int(row.x_max)
ymax = int(row.y_max)
poly = [
(xmin, ymin), (xmax, ymin),
(xmax, ymax), (xmin, ymax)
]
poly = list(itertools.chain.from_iterable(poly))
obj = {
"bbox": [xmin, ymin, xmax, ymax],
"bbox_mode": BoxMode.XYXY_ABS,
"segmentation": [poly],
"category_id": classes.index(row.class_name),
"iscrowd": 0
}
objs.append(obj)
record["annotations"] = objs
dataset_dicts.append(record)
return dataset_dicts
使用的格式的函數(shù):我們將每個(gè)注釋行轉(zhuǎn)換為一個(gè)具有注釋列表的單個(gè)記錄。您可能還會(huì)注意到,我們正在構(gòu)建一個(gè)與邊界框完全相同形狀的多邊形。這對(duì)于Detectron2中的圖像分割模型是必需的。
您將不得不將數(shù)據(jù)集注冊(cè)到數(shù)據(jù)集和元數(shù)據(jù)目錄中:
for d in ["train", "val"]:
DatasetCatalog.register("faces_" + d, lambda d=d: create_dataset_dicts(train_df if d == "train" else test_df, classes))
MetadataCatalog.get("faces_" + d).set(thing_classes=classes)
statement_metadata = MetadataCatalog.get("faces_train")
不幸的是,默認(rèn)情況下不包含測(cè)試集的評(píng)估器。我們可以通過(guò)編寫(xiě)自己的訓(xùn)練器輕松修復(fù)它:
class CocoTrainer(DefaultTrainer):
@classmethod
def build_evaluator(cls, cfg, dataset_name, output_folder=None):
if output_folder is None:
os.makedirs("coco_eval", exist_ok=True)
output_folder = "coco_eval"
return COCOEvaluator(dataset_name, cfg, False, output_folder)
如果未提供文件夾,則評(píng)估結(jié)果將存儲(chǔ)在coco_eval文件夾中。
在Detectron2模型上微調(diào)與編寫(xiě)PyTorch代碼完全不同。我們將加載配置文件,更改一些值,然后啟動(dòng)訓(xùn)練過(guò)程。但是嘿,如果您知道自己在做什么,這真的會(huì)有所幫助。在本教程中,我們將使用Mask R-CNN X101-FPN模型。它在COCO數(shù)據(jù)集上進(jìn)行了預(yù)訓(xùn)練,并且表現(xiàn)非常好。缺點(diǎn)是訓(xùn)練速度較慢。
讓我們加載配置文件和預(yù)訓(xùn)練的模型權(quán)重:
cfg = get_cfg()
cfg.merge_from_file(
model_zoo.get_config_file(
"COCO-InstanceSegmentation/mask_rcnn_X_101_32x8d_FPN_3x.yaml"
)
)
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url(
"COCO-InstanceSegmentation/mask_rcnn_X_101_32x8d_FPN_3x.yaml"
)
指定我們將用于訓(xùn)練和評(píng)估的數(shù)據(jù)集(我們注冊(cè)了這些數(shù)據(jù)集):
cfg.DATASETS.TRAIN = ("faces_train",)
cfg.DATASETS.TEST = ("faces_val",)
cfg.DATALOADER.NUM_WORKERS = 4
至于優(yōu)化器,我們將進(jìn)行一些魔法以收斂到某個(gè)好的值:
cfg.SOLVER.IMS_PER_BATCH = 4
cfg.SOLVER.BASE_LR = 0.001
cfg.SOLVER.WARMUP_ITERS = 1000
cfg.SOLVER.MAX_ITER = 1500
cfg.SOLVER.STEPS = (1000, 1500)
cfg.SOLVER.GAMMA = 0.05
除了標(biāo)準(zhǔn)的內(nèi)容(批量大小、最大迭代次數(shù)和學(xué)習(xí)率)外,我們還有幾個(gè)有趣的參數(shù):
WARMUP_ITERS - 學(xué)習(xí)率從0開(kāi)始,并在此次數(shù)的迭代中逐漸增加到預(yù)設(shè)值
STEPS - 學(xué)習(xí)率將在其檢查點(diǎn)(迭代次數(shù))降低的次數(shù)
最后,我們將指定類(lèi)別的數(shù)量以及我們將在測(cè)試集上進(jìn)行評(píng)估的周期:
cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = 64
cfg.MODEL.ROI_HEADS.NUM_CLASSES = len(classes)
cfg.TEST.EVAL_PERIOD = 500
是時(shí)候開(kāi)始訓(xùn)練了,使用我們自定義的訓(xùn)練器:
os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)
trainer = CocoTrainer(cfg)
trainer.resume_or_load(resume=False)
trainer.train()
評(píng)估目標(biāo)檢測(cè)模型
與評(píng)估標(biāo)準(zhǔn)分類(lèi)或回歸模型相比,評(píng)估目標(biāo)檢測(cè)模型有點(diǎn)不同。您需要了解的主要指標(biāo)是IoU(交并比)。它測(cè)量?jī)蓚(gè)邊界之間的重疊程度-預(yù)測(cè)的和真實(shí)的。它可以在0和1之間獲得值。
使用IoU,可以定義閾值(例如> 0.5)來(lái)分類(lèi)預(yù)測(cè)是否為真陽(yáng)性(TP)或假陽(yáng)性(FP)。現(xiàn)在,您可以通過(guò)獲取精度-召回曲線下的區(qū)域來(lái)計(jì)算平均精度(AP)現(xiàn)在,AP@X(例如AP50)只是某個(gè)IoU閾值下的AP。這應(yīng)該讓您對(duì)如何評(píng)估目標(biāo)檢測(cè)模型有一個(gè)工作的了解。
我已經(jīng)準(zhǔn)備了一個(gè)預(yù)訓(xùn)練模型,因此不必等待訓(xùn)練完成。下載它:
!gdown --id 18Ev2bpdKsBaDufhVKf0cT6RmM3FjW3nL
!mv face_detector.pth output/model_final.pth
我們可以通過(guò)加載模型并設(shè)置最低的85%的置信度閾值來(lái)開(kāi)始進(jìn)行預(yù)測(cè),以此來(lái)將預(yù)測(cè)視為正確:
cfg.MODEL.WEIGHTS = os.path.join(cfg.OUTPUT_DIR, "model_final.pth")
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.85
predictor = DefaultPredictor(cfg)
運(yùn)行評(píng)估器與訓(xùn)練好的模型:
evaluator = COCOEvaluator("faces_val", cfg, False, output_dir="./output/")
val_loader = build_detection_test_loader(cfg, "faces_val")
inference_on_dataset(trainer.model, val_loader, evaluator)
在圖像中查找人臉
接下來(lái),讓我們創(chuàng)建一個(gè)文件夾,并保存測(cè)試集中所有帶有預(yù)測(cè)注釋的圖像:
os.makedirs("annotated_results", exist_ok=True)
test_image_paths = test_df.file_name.unique()
for clothing_image in test_image_paths:
file_path = f'{IMAGES_PATH}/{clothing_image}'
im = cv2.imread(file_path)
outputs = predictor(im)
v = Visualizer(
im[:, :, ::-1],
metadata=statement_metadata,
scale=1.,
instance_mode=ColorMode.IMAGE
)
instances = outputs["instances"].to("cpu")
instances.remove('pred_masks')
v = v.draw_instance_predictions(instances)
result = v.get_image()[:, :, ::-1]
file_name = ntpath.basename(clothing_image)
write_res = cv2.imwrite(f'annotated_results/{file_name}', result)
· END ·