RetinaFace was added as a face detection scheme.
This commit is contained in:
parent
90c7c9284b
commit
cd9cfd9ee8
7 changed files with 208 additions and 9 deletions
21
Licenses/Pytorch_Retinaface.txt
Normal file
21
Licenses/Pytorch_Retinaface.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
11
README.md
11
README.md
|
|
@ -141,6 +141,17 @@ For all training after the first model, a modified version of [Adaptive Wing Los
|
||||||
|
|
||||||
For expression detection, [LIBSVM](https://www.csie.ntu.edu.tw/~cjlin/libsvm/) is used.
|
For expression detection, [LIBSVM](https://www.csie.ntu.edu.tw/~cjlin/libsvm/) is used.
|
||||||
|
|
||||||
|
Face detection is done using OpenCV's Haar Cascade classifier or RetinaFace.
|
||||||
|
|
||||||
|
@inproceedings{deng2019retinaface,
|
||||||
|
title={RetinaFace: Single-stage Dense Face Localisation in the Wild},
|
||||||
|
author={Deng, Jiankang and Guo, Jia and Yuxiang, Zhou and Jinke Yu and Irene Kotsia and Zafeiriou, Stefanos},
|
||||||
|
booktitle={arxiv},
|
||||||
|
year={2019}
|
||||||
|
}
|
||||||
|
|
||||||
|
RetinaFace detection is based on [this](https://github.com/biubug6/Pytorch_Retinaface) implementation. The pretrained model was modified to remove unnecessary landmark detection and converted to ONNX format for a resolution of 384x384.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
The code and models are distributed under the BSD 2-clause license.
|
The code and models are distributed under the BSD 2-clause license.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ parser.add_argument("-v", "--visualize", type=int, help="Set this to 1 to visual
|
||||||
parser.add_argument("-P", "--pnp-points", type=int, help="Set this to 1 to add the 3D fitting points to the visualization", default=0)
|
parser.add_argument("-P", "--pnp-points", type=int, help="Set this to 1 to add the 3D fitting points to the visualization", default=0)
|
||||||
parser.add_argument("-s", "--silent", type=int, help="Set this to 1 to prevent text output on the console", default=0)
|
parser.add_argument("-s", "--silent", type=int, help="Set this to 1 to prevent text output on the console", default=0)
|
||||||
parser.add_argument("--faces", type=int, help="Set the maximum number of faces (slow)", default=1)
|
parser.add_argument("--faces", type=int, help="Set the maximum number of faces (slow)", default=1)
|
||||||
|
parser.add_argument("--scan-retinaface", type=int, help="When set to 1, scanning for additional faces will be performed using RetinaFace in a background thread, otherwise a simpler, faster face detection mechanism is used. When the maximum number of faces is 1, this option does nothing.", default=0)
|
||||||
parser.add_argument("--scan-every", type=int, help="Set after how many frames a scan for new faces should run", default=3)
|
parser.add_argument("--scan-every", type=int, help="Set after how many frames a scan for new faces should run", default=3)
|
||||||
parser.add_argument("--discard-after", type=int, help="Set the how long the tracker should keep looking for lost faces", default=10)
|
parser.add_argument("--discard-after", type=int, help="Set the how long the tracker should keep looking for lost faces", default=10)
|
||||||
parser.add_argument("--video-out", help="Set this to the filename of an AVI file to save the tracking visualization as a video", default=None)
|
parser.add_argument("--video-out", help="Set this to the filename of an AVI file to save the tracking visualization as a video", default=None)
|
||||||
|
|
@ -106,7 +107,7 @@ try:
|
||||||
first = False
|
first = False
|
||||||
height, width, channels = frame.shape
|
height, width, channels = frame.shape
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
tracker = Tracker(width, height, threshold=args.threshold, max_threads=args.max_threads, max_faces=args.faces, discard_after=args.discard_after, scan_every=args.scan_every, silent=False if args.silent == 0 else True, model_type=args.model, pnp_quality=args.high_quality_3d, model_dir=args.model_dir, no_gaze=False if args.gaze_tracking != 0 else True)
|
tracker = Tracker(width, height, threshold=args.threshold, max_threads=args.max_threads, max_faces=args.faces, discard_after=args.discard_after, scan_every=args.scan_every, silent=False if args.silent == 0 else True, model_type=args.model, pnp_quality=args.high_quality_3d, model_dir=args.model_dir, no_gaze=False if args.gaze_tracking != 0 else True, use_retinaface=args.scan_retinaface)
|
||||||
if not args.video_out is None:
|
if not args.video_out is None:
|
||||||
out = cv2.VideoWriter(args.video_out, cv2.VideoWriter_fourcc('F','F','V','1'), 24, (width,height))
|
out = cv2.VideoWriter(args.video_out, cv2.VideoWriter_fourcc('F','F','V','1'), 24, (width,height))
|
||||||
|
|
||||||
|
|
|
||||||
1
models/priorbox_384.json
Normal file
1
models/priorbox_384.json
Normal file
File diff suppressed because one or more lines are too long
BIN
models/retinaface_opt.onnx
Normal file
BIN
models/retinaface_opt.onnx
Normal file
Binary file not shown.
150
retinaface.py
Normal file
150
retinaface.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
import onnxruntime
|
||||||
|
import time
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
|
||||||
|
def py_cpu_nms(dets, thresh):
|
||||||
|
""" Pure Python NMS baseline.
|
||||||
|
Copyright (c) 2015 Microsoft
|
||||||
|
Licensed under The MIT License
|
||||||
|
Written by Ross Girshick
|
||||||
|
"""
|
||||||
|
x1 = dets[:, 0]
|
||||||
|
y1 = dets[:, 1]
|
||||||
|
x2 = dets[:, 2]
|
||||||
|
y2 = dets[:, 3]
|
||||||
|
scores = dets[:, 4]
|
||||||
|
|
||||||
|
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
|
||||||
|
order = scores.argsort()[::-1]
|
||||||
|
|
||||||
|
keep = []
|
||||||
|
while order.size > 0:
|
||||||
|
i = order[0]
|
||||||
|
keep.append(i)
|
||||||
|
xx1 = np.maximum(x1[i], x1[order[1:]])
|
||||||
|
yy1 = np.maximum(y1[i], y1[order[1:]])
|
||||||
|
xx2 = np.minimum(x2[i], x2[order[1:]])
|
||||||
|
yy2 = np.minimum(y2[i], y2[order[1:]])
|
||||||
|
|
||||||
|
w = np.maximum(0.0, xx2 - xx1 + 1)
|
||||||
|
h = np.maximum(0.0, yy2 - yy1 + 1)
|
||||||
|
inter = w * h
|
||||||
|
ovr = inter / (areas[i] + areas[order[1:]] - inter)
|
||||||
|
|
||||||
|
inds = np.where(ovr <= thresh)[0]
|
||||||
|
order = order[inds + 1]
|
||||||
|
|
||||||
|
return keep
|
||||||
|
|
||||||
|
def decode(loc, priors, variances):
|
||||||
|
data = (
|
||||||
|
priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
|
||||||
|
priors[:, 2:] * np.exp(loc[:, 2:] * variances[1])
|
||||||
|
)
|
||||||
|
boxes = np.concatenate(data, 1)
|
||||||
|
boxes[:, :2] -= boxes[:, 2:] / 2
|
||||||
|
boxes[:, 2:] += boxes[:, :2]
|
||||||
|
return boxes
|
||||||
|
|
||||||
|
def worker_thread(rfd, frame):
|
||||||
|
results = rfd.detect_retina(frame, is_background=True)
|
||||||
|
rfd.results.put(results, False)
|
||||||
|
rfd.finished = True
|
||||||
|
rfd.running = False
|
||||||
|
|
||||||
|
class RetinaFaceDetector():
|
||||||
|
def __init__(self, model_path="models/retinaface_opt.onnx", json_path="models/priorbox_384.json", threads=4, min_conf=0.3, nms_threshold=0.4, top_k=1):
|
||||||
|
options = onnxruntime.SessionOptions()
|
||||||
|
options.inter_op_num_threads = 1
|
||||||
|
options.intra_op_num_threads = threads
|
||||||
|
options.execution_mode = onnxruntime.ExecutionMode.ORT_SEQUENTIAL
|
||||||
|
options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||||
|
options.log_severity_level = 3
|
||||||
|
self.session = onnxruntime.InferenceSession(model_path, sess_options=options)
|
||||||
|
self.res = 384
|
||||||
|
with open(json_path, "r") as prior_file:
|
||||||
|
self.priorbox = np.array(json.loads(prior_file.read()))
|
||||||
|
self.min_conf = min_conf
|
||||||
|
self.nms_threshold = nms_threshold
|
||||||
|
self.top_k = top_k
|
||||||
|
self.finished = False
|
||||||
|
self.running = False
|
||||||
|
self.results = queue.Queue()
|
||||||
|
|
||||||
|
def detect_retina(self, frame, is_background=False):
|
||||||
|
h, w, _ = frame.shape
|
||||||
|
im = None
|
||||||
|
if h > w:
|
||||||
|
resize = h / self.res
|
||||||
|
im = cv2.resize(frame, (int(w / resize), self.res), interpolation=cv2.INTER_LINEAR)
|
||||||
|
resize = 1 / resize
|
||||||
|
else:
|
||||||
|
resize = w / self.res
|
||||||
|
im = cv2.resize(frame, (self.res, int(h / resize)), interpolation=cv2.INTER_LINEAR)
|
||||||
|
resize = 1 / resize
|
||||||
|
pad_h = self.res - im.shape[0]
|
||||||
|
pad_w = self.res - im.shape[1]
|
||||||
|
im = cv2.copyMakeBorder(im, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, (0, 0, 0))
|
||||||
|
im = np.float32(im)
|
||||||
|
scale = np.array((self.res, self.res, self.res, self.res))
|
||||||
|
im -= (104, 117, 123)
|
||||||
|
im = im.transpose(2, 0, 1)
|
||||||
|
im = np.expand_dims(im, 0)
|
||||||
|
output = self.session.run([], {"input0": im})
|
||||||
|
loc, conf = output[0][0], output[1][0]
|
||||||
|
boxes = decode(loc, self.priorbox, [0.1, 0.2])
|
||||||
|
boxes = boxes * scale / resize
|
||||||
|
scores = conf[:, 1]
|
||||||
|
|
||||||
|
inds = np.where(scores > self.min_conf)[0]
|
||||||
|
boxes = boxes[inds]
|
||||||
|
scores = scores[inds]
|
||||||
|
|
||||||
|
dets = np.hstack((boxes, scores[:, np.newaxis])).astype(np.float32, copy=False)
|
||||||
|
keep = py_cpu_nms(dets, self.nms_threshold)
|
||||||
|
dets = dets[keep, :]
|
||||||
|
dets = dets[:self.top_k, 0:4]
|
||||||
|
dets[:, 2:4] = dets[:, 2:4] - dets[:, 0:2]
|
||||||
|
|
||||||
|
if is_background:
|
||||||
|
upsize = dets[:, 2:4] * np.array([[0.1, 0.0]])
|
||||||
|
dets[:, 0:2] -= upsize
|
||||||
|
dets[:, 2:4] += upsize * 2
|
||||||
|
|
||||||
|
return list(map(tuple, dets))
|
||||||
|
|
||||||
|
def background_detect(self, frame):
|
||||||
|
if self.running or self.finished:
|
||||||
|
return
|
||||||
|
self.running = True
|
||||||
|
im = copy.copy(frame)
|
||||||
|
thread = threading.Thread(target=worker_thread, args=(self, im))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def get_results(self):
|
||||||
|
if self.finished:
|
||||||
|
results = []
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
detection = self.results.get(False)
|
||||||
|
results.append(detection)
|
||||||
|
except:
|
||||||
|
"No error"
|
||||||
|
self.finished = False
|
||||||
|
return list(*results)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if __name__== "__main__":
|
||||||
|
retina = RetinaFaceDetector(top_k=4)
|
||||||
|
im = cv2.imread(sys.argv[1], cv2.IMREAD_COLOR)
|
||||||
|
start = time.perf_counter()
|
||||||
|
faces = retina.detect_retina(im)
|
||||||
|
end = 1000 * (time.perf_counter() - start)
|
||||||
|
print(f"Runtime: {end:.3f}ms")
|
||||||
21
tracker.py
21
tracker.py
|
|
@ -7,6 +7,7 @@ import time
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import copy
|
import copy
|
||||||
|
from retinaface import RetinaFaceDetector
|
||||||
|
|
||||||
def resolve(name):
|
def resolve(name):
|
||||||
f = os.path.join(os.path.dirname(__file__), name)
|
f = os.path.join(os.path.dirname(__file__), name)
|
||||||
|
|
@ -226,7 +227,7 @@ class FaceInfo():
|
||||||
|
|
||||||
|
|
||||||
class Tracker():
|
class Tracker():
|
||||||
def __init__(self, width, height, model_type=3, threshold=0.4, max_faces=1, discard_after=5, scan_every=3, bbox_growth=0.0, max_threads=4, silent=False, pnp_quality=1, model_dir=None, no_gaze=False):
|
def __init__(self, width, height, model_type=3, threshold=0.4, max_faces=1, discard_after=5, scan_every=3, bbox_growth=0.0, max_threads=4, silent=False, pnp_quality=1, model_dir=None, no_gaze=False, use_retinaface=False):
|
||||||
options = onnxruntime.SessionOptions()
|
options = onnxruntime.SessionOptions()
|
||||||
options.inter_op_num_threads = 1
|
options.inter_op_num_threads = 1
|
||||||
options.intra_op_num_threads = max_threads
|
options.intra_op_num_threads = max_threads
|
||||||
|
|
@ -248,6 +249,10 @@ class Tracker():
|
||||||
else:
|
else:
|
||||||
model_base_path = model_dir
|
model_base_path = model_dir
|
||||||
|
|
||||||
|
self.retinaface = RetinaFaceDetector(model_path=os.path.join(model_base_path, "retinaface_opt.onnx"), json_path=os.path.join(model_base_path, "priorbox_384.json"), threads=max_threads, top_k=max_faces)
|
||||||
|
self.retinaface_slow = RetinaFaceDetector(model_path=os.path.join(model_base_path, "retinaface_opt.onnx"), json_path=os.path.join(model_base_path, "priorbox_384.json"), threads=2, top_k=max_faces)
|
||||||
|
self.use_retinaface = use_retinaface
|
||||||
|
|
||||||
# Single face instance with multiple threads
|
# Single face instance with multiple threads
|
||||||
self.session = onnxruntime.InferenceSession(os.path.join(model_base_path, model), sess_options=options)
|
self.session = onnxruntime.InferenceSession(os.path.join(model_base_path, model), sess_options=options)
|
||||||
|
|
||||||
|
|
@ -664,8 +669,18 @@ class Tracker():
|
||||||
new_faces.extend(self.faces)
|
new_faces.extend(self.faces)
|
||||||
new_faces.extend(additional_faces)
|
new_faces.extend(additional_faces)
|
||||||
self.wait_count += 1
|
self.wait_count += 1
|
||||||
if self.detected < self.max_faces:
|
if self.detected == 0:
|
||||||
if self.detected == 0 or self.wait_count >= self.scan_every:
|
start_fd = time.perf_counter()
|
||||||
|
new_faces.extend(self.retinaface.detect_retina(frame))
|
||||||
|
duration_fd = 1000 * (time.perf_counter() - start_fd)
|
||||||
|
self.wait_count = 0
|
||||||
|
elif self.detected < self.max_faces:
|
||||||
|
if self.use_retinaface > 0:
|
||||||
|
new_faces.extend(self.retinaface_slow.get_results())
|
||||||
|
if self.wait_count >= self.scan_every:
|
||||||
|
if self.use_retinaface > 0:
|
||||||
|
self.retinaface_slow.background_detect(frame)
|
||||||
|
else:
|
||||||
start_fd = time.perf_counter()
|
start_fd = time.perf_counter()
|
||||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
new_faces.extend(list(self.faceCascade.detectMultiScale(gray, 1.3, 4, 0 | cv2.CASCADE_SCALE_IMAGE, (50, 50))))
|
new_faces.extend(list(self.faceCascade.detectMultiScale(gray, 1.3, 4, 0 | cv2.CASCADE_SCALE_IMAGE, (50, 50))))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue