301 lines
9.5 KiB
Python
301 lines
9.5 KiB
Python
# Compare incomming video with known faces
|
|
# Running in a local python instance to get around PATH issues
|
|
|
|
# Import time so we can start timing asap
|
|
import time
|
|
|
|
# Start timing
|
|
timings = {
|
|
"st": time.time()
|
|
}
|
|
|
|
# Import required modules
|
|
import sys
|
|
import os
|
|
import json
|
|
import configparser
|
|
import cv2
|
|
import dlib
|
|
import numpy as np
|
|
import _thread as thread
|
|
|
|
|
|
def init_detector(lock):
|
|
"""Start face detector, encoder and predictor in a new thread"""
|
|
global face_detector, pose_predictor, face_encoder
|
|
|
|
# Test if at lest 1 of the data files is there and abort if it's not
|
|
if not os.path.isfile(PATH + "/dlib-data/shape_predictor_5_face_landmarks.dat"):
|
|
print("Data files have not been downloaded, please run the following commands:")
|
|
print("\n\tcd " + PATH + "/dlib-data")
|
|
print("\tsudo ./install.sh\n")
|
|
lock.release()
|
|
sys.exit(1)
|
|
|
|
# Use the CNN detector if enabled
|
|
if use_cnn:
|
|
face_detector = dlib.cnn_face_detection_model_v1(PATH + "/dlib-data/mmod_human_face_detector.dat")
|
|
else:
|
|
face_detector = dlib.get_frontal_face_detector()
|
|
|
|
# Start the others regardless
|
|
pose_predictor = dlib.shape_predictor(PATH + "/dlib-data/shape_predictor_5_face_landmarks.dat")
|
|
face_encoder = dlib.face_recognition_model_v1(PATH + "/dlib-data/dlib_face_recognition_resnet_model_v1.dat")
|
|
|
|
# Note the time it took to initialize detectors
|
|
timings["ll"] = time.time() - timings["ll"]
|
|
lock.release()
|
|
|
|
|
|
def stop(status):
|
|
"""Stop the execution and close video stream"""
|
|
video_capture.release()
|
|
sys.exit(status)
|
|
|
|
|
|
# Make sure we were given an username to tast against
|
|
if len(sys.argv) < 2:
|
|
sys.exit(12)
|
|
|
|
# Get the absolute path to the current directory
|
|
PATH = os.path.abspath(__file__ + "/..")
|
|
|
|
# The username of the user being authenticated
|
|
user = sys.argv[1]
|
|
# The model file contents
|
|
models = []
|
|
# Encoded face models
|
|
encodings = []
|
|
# Amount of ignored 100% black frames
|
|
black_tries = 0
|
|
# Amount of ingnored dark frames
|
|
dark_tries = 0
|
|
# Total amount of frames captured
|
|
frames = 0
|
|
# face recognition/detection instances
|
|
face_detector = None
|
|
pose_predictor = None
|
|
face_encoder = None
|
|
|
|
# Try to load the face model from the models folder
|
|
try:
|
|
models = json.load(open(PATH + "/models/" + user + ".dat"))
|
|
|
|
for model in models:
|
|
encodings += model["data"]
|
|
except FileNotFoundError:
|
|
sys.exit(10)
|
|
|
|
# Check if the file contains a model
|
|
if len(models) < 1:
|
|
sys.exit(10)
|
|
|
|
# Read config from disk
|
|
config = configparser.ConfigParser()
|
|
config.read(PATH + "/config.ini")
|
|
|
|
# Get all config values needed
|
|
use_cnn = config.getboolean("core", "use_cnn", fallback=False)
|
|
timeout = config.getint("video", "timout", fallback=5)
|
|
dark_threshold = config.getfloat("video", "dark_threshold", fallback=50.0)
|
|
video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10
|
|
end_report = config.getboolean("debug", "end_report", fallback=False)
|
|
|
|
# Save the time needed to start the script
|
|
timings["in"] = time.time() - timings["st"]
|
|
|
|
# Import face recognition, takes some time
|
|
timings["ll"] = time.time()
|
|
|
|
# Start threading and wait for init to finish
|
|
lock = thread.allocate_lock()
|
|
lock.acquire()
|
|
thread.start_new_thread(init_detector, (lock, ))
|
|
|
|
# Start video capture on the IR camera
|
|
timings["ic"] = time.time()
|
|
|
|
# Check if the user explicitly set ffmpeg as recorder
|
|
if config.get("video", "recording_plugin") == "ffmpeg":
|
|
# Set the capture source for ffmpeg
|
|
from recorders.ffmpeg_reader import ffmpeg_reader
|
|
video_capture = ffmpeg_reader(config.get("video", "device_path"), config.get("video", "device_format"))
|
|
elif config.get("video", "recording_plugin") == "pyv4l2":
|
|
# Set the capture source for pyv4l2
|
|
from recorders.pyv4l2_reader import pyv4l2_reader
|
|
video_capture = pyv4l2_reader(config.get("video", "device_path"), config.get("video", "device_format"))
|
|
else:
|
|
# Start video capture on the IR camera through OpenCV
|
|
video_capture = cv2.VideoCapture(config.get("video", "device_path"), cv2.CAP_V4L)
|
|
|
|
# Force MJPEG decoding if true
|
|
if config.getboolean("video", "force_mjpeg", fallback=False):
|
|
# Set a magic number, will enable MJPEG but is badly documented
|
|
# 1196444237 is "GPJM" in ASCII
|
|
video_capture.set(cv2.CAP_PROP_FOURCC, 1196444237)
|
|
|
|
# Set the frame width and height if requested
|
|
fw = config.getint("video", "frame_width", fallback=-1)
|
|
fh = config.getint("video", "frame_height", fallback=-1)
|
|
if fw != -1:
|
|
video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, fw)
|
|
if fh != -1:
|
|
video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh)
|
|
|
|
# Capture a single frame so the camera becomes active
|
|
# This will let the camera adjust its light levels while we're importing for faster scanning
|
|
video_capture.grab()
|
|
|
|
# Read exposure from config to use in the main loop
|
|
exposure = config.getint("video", "exposure", fallback=-1)
|
|
|
|
# Note the time it took to open the camera
|
|
timings["ic"] = time.time() - timings["ic"]
|
|
|
|
# wait for thread to finish
|
|
lock.acquire()
|
|
lock.release()
|
|
del lock
|
|
|
|
# Fetch the max frame height
|
|
max_height = config.getfloat("video", "max_height", fallback=0.0)
|
|
# Get the height of the image
|
|
height = video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT) or 1
|
|
|
|
# Calculate the amount the image has to shrink
|
|
scaling_factor = (max_height / height) or 1
|
|
|
|
# Fetch config settings out of the loop
|
|
timeout = config.getint("video", "timeout")
|
|
dark_threshold = config.getfloat("video", "dark_threshold")
|
|
end_report = config.getboolean("debug", "end_report")
|
|
|
|
# Start the read loop
|
|
frames = 0
|
|
valid_frames = 0
|
|
timings["fr"] = time.time()
|
|
dark_running_total = 0
|
|
|
|
while True:
|
|
# Increment the frame count every loop
|
|
frames += 1
|
|
|
|
# Stop if we've exceded the time limit
|
|
if time.time() - timings["fr"] > timeout:
|
|
if (dark_tries == valid_frames ):
|
|
print("All frames were too dark, please check dark_threshold in config")
|
|
print("Average darkness: " + str(dark_running_total / valid_frames) + ", Threshold: " + str(dark_threshold))
|
|
stop(13)
|
|
stop(11)
|
|
|
|
# Grab a single frame of video
|
|
ret, frame = video_capture.read()
|
|
|
|
if frames == 1 and ret is False:
|
|
print("Could not read from camera")
|
|
exit(12)
|
|
|
|
try:
|
|
# Convert from color to grayscale
|
|
# First processing of frame, so frame errors show up here
|
|
gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
except RuntimeError:
|
|
gsframe = frame
|
|
except cv2.error:
|
|
print("\nUnknown camera, please check your 'device_path' config value.\n")
|
|
raise
|
|
|
|
# Create a histogram of the image with 8 values
|
|
hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256])
|
|
# All values combined for percentage calculation
|
|
hist_total = np.sum(hist)
|
|
|
|
# Calculate frame darkness
|
|
darkness = (hist[0] / hist_total * 100)
|
|
|
|
# If the image is fully black due to a bad camera read,
|
|
# skip to the next frame
|
|
if (hist_total == 0) or (darkness == 100):
|
|
black_tries += 1
|
|
continue
|
|
|
|
dark_running_total += darkness
|
|
valid_frames += 1
|
|
# If the image exceeds darkness threshold due to subject distance,
|
|
# skip to the next frame
|
|
if (darkness > dark_threshold):
|
|
dark_tries += 1
|
|
continue
|
|
|
|
# If the hight is too high
|
|
if scaling_factor != 1:
|
|
# Apply that factor to the frame
|
|
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
|
|
gsframe = cv2.resize(gsframe, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
|
|
|
|
# Get all faces from that frame as encodings
|
|
# Upsamples 1 time
|
|
face_locations = face_detector(gsframe, 1)
|
|
|
|
# Loop through each face
|
|
for fl in face_locations:
|
|
if use_cnn:
|
|
fl = fl.rect
|
|
|
|
# Fetch the faces in the image
|
|
face_landmark = pose_predictor(frame, fl)
|
|
face_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1))
|
|
|
|
# Match this found face against a known face
|
|
matches = np.linalg.norm(encodings - face_encoding, axis=1)
|
|
|
|
# Get best match
|
|
match_index = np.argmin(matches)
|
|
match = matches[match_index]
|
|
|
|
# Check if a match that's confident enough
|
|
if 0 < match < video_certainty:
|
|
timings["tt"] = time.time() - timings["st"]
|
|
timings["fr"] = time.time() - timings["fr"]
|
|
|
|
# If set to true in the config, print debug text
|
|
if end_report:
|
|
def print_timing(label, k):
|
|
"""Helper function to print a timing from the list"""
|
|
print(" %s: %dms" % (label, round(timings[k] * 1000)))
|
|
|
|
# Print a nice timing report
|
|
print("Time spent")
|
|
print_timing("Starting up", "in")
|
|
print(" Open cam + load libs: %dms" % (round(max(timings["ll"], timings["ic"]) * 1000, )))
|
|
print_timing(" Opening the camera", "ic")
|
|
print_timing(" Importing recognition libs", "ll")
|
|
print_timing("Searching for known face", "fr")
|
|
print_timing("Total time", "tt")
|
|
|
|
print("\nResolution")
|
|
width = video_capture.get(cv2.CAP_PROP_FRAME_WIDTH) or 1
|
|
print(" Native: %dx%d" % (height, width))
|
|
# Save the new size for diagnostics
|
|
scale_height, scale_width = frame.shape[:2]
|
|
print(" Used: %dx%d" % (scale_height, scale_width))
|
|
|
|
# Show the total number of frames and calculate the FPS by deviding it by the total scan time
|
|
print("\nFrames searched: %d (%.2f fps)" % (frames, frames / timings["fr"]))
|
|
print("Black frames ignored: %d " % (black_tries, ))
|
|
print("Dark frames ignored: %d " % (dark_tries, ))
|
|
print("Certainty of winning frame: %.3f" % (match * 10, ))
|
|
|
|
print("Winning model: %d (\"%s\")" % (match_index, models[match_index]["label"]))
|
|
|
|
# End peacefully
|
|
stop(0)
|
|
|
|
if exposure != -1:
|
|
# For a strange reason on some cameras (e.g. Lenoxo X1E)
|
|
# setting manual exposure works only after a couple frames
|
|
# are captured and even after a delay it does not
|
|
# always work. Setting exposure at every frame is
|
|
# reliable though.
|
|
video_capture.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual
|
|
video_capture.set(cv2.CAP_PROP_EXPOSURE, float(exposure))
|