Merge pull request #8 from Boltgolt/dev

A wide range of fixes and improvements
This commit is contained in:
boltgolt 2018-02-20 23:14:58 +01:00 committed by GitHub
commit 10aae8266c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 298 additions and 91 deletions

View file

@ -12,7 +12,7 @@ Run the installer by pasting (`ctrl+shift+V`) the following command into the ter
wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py
``` ```
This will guide you through the installation. When that's done run `howdy USER add` and replace `USER` with your username to add a face model. This will guide you through the installation. When that's done run `sudo howdy USER add` and replace `USER` with your username to add a face model.
If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action. If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action.

60
cli.py
View file

@ -1,41 +1,53 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# CLI directly called by running the howdy command # CLI directly called by running the howdy command
# Import required modules # Import required modules
import sys import sys
import os import os
# Check if the minimum of 3 arugemnts has been met and print help otherwise # Check if if a command has been given and print help otherwise
if (len(sys.argv) < 3): if (len(sys.argv) < 2):
print("Howdy IR face recognition help") print("Howdy IR face recognition help")
import cli.help import cli.help
sys.exit() sys.exit()
# The command given # The command given
cmd = sys.argv[2] cmd = sys.argv[1]
# Requre sudo for comamnds that need root rights to read the model files # Call the right files for commands that don't need root
if cmd in ["list", "add", "remove", "clear"] and os.getenv("SUDO_USER") is None: if cmd == "help":
print("Please run this command with sudo")
sys.exit()
# Call the right files for the given command
if cmd == "list":
import cli.list
elif cmd == "help":
print("Howdy IR face recognition") print("Howdy IR face recognition")
import cli.help import cli.help
elif cmd == "add": elif cmd == "test":
import cli.add import cli.test
elif cmd == "remove":
import cli.remove
elif cmd == "clear":
import cli.clear
else: else:
# If the comand is invalid, check if the user hasn't swapped the username and command # Check if the minimum of 3 arugemnts has been met and print help otherwise
if sys.argv[1] in ["list", "add", "remove", "clear", "help"]: if (len(sys.argv) < 3):
print("Usage: howdy <user> <command>") print("Howdy IR face recognition help")
else:
print('Unknown command "' + cmd + '"')
import cli.help import cli.help
sys.exit()
# Requre sudo for comamnds that need root rights to read the model files
if os.getenv("SUDO_USER") is None:
print("Please run this command with sudo")
sys.exit()
# Frome here on we require the second argument to be the username,
# switching the command to the 3rd
cmd = sys.argv[2]
if cmd == "list":
import cli.list
elif cmd == "add":
import cli.add
elif cmd == "remove":
import cli.remove
elif cmd == "clear":
import cli.clear
else:
# If the comand is invalid, check if the user hasn't swapped the username and command
if sys.argv[1] in ["list", "add", "remove", "clear"]:
print("Usage: howdy <user> <command>")
else:
print('Unknown command "' + cmd + '"')
import cli.help

View file

@ -0,0 +1 @@
# Marks this folder as importable

View file

@ -6,9 +6,9 @@ import time
import os import os
import sys import sys
import json import json
import cv2
import configparser import configparser
# Try to import face_recognition and give a nice error if we can't # Try to import face_recognition and give a nice error if we can't
# Add should be the first point where import issues show up # Add should be the first point where import issues show up
try: try:
@ -27,50 +27,8 @@ path = os.path.dirname(os.path.abspath(__file__))
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(path + "/../config.ini") config.read(path + "/../config.ini")
def captureFrame(delay):
"""Capture and encode 1 frame of video"""
global insert_model
# Call fswebcam to save a frame to /tmp with a set delay
exit_code = subprocess.call(["fswebcam", "-S", str(delay), "--no-banner", "-d", "/dev/video" + str(config.get("video", "device_id")), tmp_file])
# Check if fswebcam exited normally
if (exit_code != 0):
print("Webcam frame capture failed!")
print("Please make sure fswebcam is installed on this system")
sys.exit()
# Try to load the image from disk
try:
ref = face_recognition.load_image_file(tmp_file)
except FileNotFoundError:
print("No webcam frame captured, check if /dev/video" + str(config.get("video", "device_id")) + " is the right webcam")
sys.exit()
# Make a face encoding from the loaded image
enc = face_recognition.face_encodings(ref)
# If 0 faces are detected we can't continue
if len(enc) == 0:
print("No face detected, aborting")
sys.exit()
# If more than 1 faces are detected we can't know wich one belongs to the user
if len(enc) > 1:
print("Multiple faces detected, aborting")
sys.exit()
clean_enc = []
# Copy the values into a clean array so we can export it as JSON later on
for point in enc[0]:
clean_enc.append(point)
insert_model["data"].append(clean_enc)
# The current user # The current user
user = sys.argv[1] user = sys.argv[1]
# The name of the tmp frame file to user
tmp_file = "/tmp/howdy_" + user + ".jpg"
# The permanent file to store the encoded model in # The permanent file to store the encoded model in
enc_file = path + "/../models/" + user + ".dat" enc_file = path + "/../models/" + user + ".dat"
# Known encodings # Known encodings
@ -87,6 +45,11 @@ try:
except FileNotFoundError: except FileNotFoundError:
encodings = [] encodings = []
# Print a warning if too many encodings are being added
if len(encodings) > 2:
print("WARNING: Every additional model slows down the face recognition engine")
print("Press ctrl+C to cancel")
print("Adding face model for the user account " + user) print("Adding face model for the user account " + user)
# Set the default label # Set the default label
@ -111,15 +74,53 @@ insert_model = {
"data": [] "data": []
} }
print("\nPlease look straight into the camera for 5 seconds") # Open the camera
video_capture = cv2.VideoCapture(int(config.get("video", "device_id")))
video_capture.read()
print("\nPlease look straight into the camera")
# Give the user time to read # Give the user time to read
time.sleep(2) time.sleep(2)
# Capture with 3 different delays to simulate different camera exposures # Will contain found face encodings
for delay in [30, 6, 0]: enc = []
time.sleep(.3) # Count the amount or read frames
captureFrame(delay) frames = 0
# Loop through frames till we hit a timeout
while frames < 60:
frames += 1
# Grab a single frame of video
# Don't remove ret, it doesn't work without it
ret, frame = video_capture.read()
# Get the encodings in the frame
enc = face_recognition.face_encodings(frame)
# If we've found at least one, we can continue
if len(enc) > 0:
break
# If 0 faces are detected we can't continue
if len(enc) == 0:
print("No face detected, aborting")
sys.exit()
# If more than 1 faces are detected we can't know wich one belongs to the user
if len(enc) > 1:
print("Multiple faces detected, aborting")
sys.exit()
# Totally clean array that can be exported as JSON
clean_enc = []
# Copy the values into a clean array so we can export it as JSON later on
for point in enc[0]:
clean_enc.append(point)
insert_model["data"].append(clean_enc)
# Insert full object into the list # Insert full object into the list
encodings.append(insert_model) encodings.append(insert_model)
@ -128,7 +129,6 @@ encodings.append(insert_model)
with open(enc_file, "w") as datafile: with open(enc_file, "w") as datafile:
json.dump(encodings, datafile) json.dump(encodings, datafile)
# Remove any left over temp files # Give let the user know how it went
os.remove(tmp_file) print("Scan complete")
print("\nAdded a new model to " + user)
print("Done.")

View file

@ -10,6 +10,7 @@ Commands:
add Add a new face model for the current user add Add a new face model for the current user
remove [id] Remove a specific model remove [id] Remove a specific model
clear Remove all face models for the current user clear Remove all face models for the current user
test Test the camera and recognition methods
For support please visit For support please visit
https://github.com/Boltgolt/howdy\ https://github.com/Boltgolt/howdy\

161
cli/test.py Normal file
View file

@ -0,0 +1,161 @@
# Show a windows with the video stream and testing information
# Import required modules
import face_recognition
import cv2
import configparser
import os
import sys
import json
import numpy
import time
# Get the absolute path to the current file
path = os.path.dirname(os.path.abspath(__file__))
# Read config from disk
config = configparser.ConfigParser()
config.read(path + "/../config.ini")
# Start capturing from the configured webcam
video_capture = cv2.VideoCapture(int(config.get("video", "device_id")))
# Let the user know what's up
print("""
Opening a window with a test feed
Press ctrl+C in this terminal to quit
Click on the image to enable or disable slow mode
""")
def mouse(event, x, y, flags, param):
"""Handle mouse events"""
global slow_mode
# Toggle slowmode on click
if event == cv2.EVENT_LBUTTONDOWN:
slow_mode = not slow_mode
# Open the window and attach a a mouse listener
cv2.namedWindow("Howdy Test")
cv2.setMouseCallback("Howdy Test", mouse)
# Enable a delay in the loop
slow_mode = False
# Count all frames ever
total_frames = 0
# Count all frames per second
sec_frames = 0
# Last secands FPS
fps = 0
# The current second we're counting
sec = int(time.time())
# Wrap everything in an keyboard interupt handler
try:
while True:
# Inclement the frames
total_frames += 1
sec_frames += 1
# Id we've entered a new second
if sec != int(time.time()):
# Set the last seconds FPS
fps = sec_frames
# Set the new second and reset the counter
sec = int(time.time())
sec_frames = 0
# Grab a single frame of video
ret, frame = (video_capture.read())
# Make a frame to put overlays in
overlay = frame.copy()
# Fetch the frame height and width
height, width = frame.shape[:2]
# Create a histogram of the image with 8 values
hist = cv2.calcHist([frame], [0], None, [8], [0, 256])
# All values combined for percentage calculation
hist_total = int(sum(hist)[0])
# Fill with the overal containing percentage
hist_perc = []
# Loop though all values to calculate a pensentage and add it to the overlay
for index, value in enumerate(hist):
value_perc = float(value[0]) / hist_total * 100
hist_perc.append(value_perc)
# Top left pont, 10px margins
p1 = (20 + (10 * index), 10)
# Bottom right point makes the bar 10px thick, with an height of half the percentage
p2 = (10 + (10 * index), int(value_perc / 2 + 10))
# Draw the bar in green
cv2.rectangle(overlay, p1, p2, (0, 200, 0), thickness=cv2.FILLED)
# Draw a stripe indicating the dark threshold
cv2.rectangle(overlay, (8, 35), (20, 36), (255, 0, 0), thickness=cv2.FILLED)
def print_text(line_number, text):
"""Print the status text by line number"""
cv2.putText(overlay, text, (10, height - 10 - (10 * line_number)), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA)
# Print the statis in the bottom left
print_text(0, "RESOLUTION: " + str(height) + "x" + str(width))
print_text(1, "FPS: " + str(fps))
print_text(2, "FRAMES: " + str(total_frames))
# Show that slow mode is on, if it's on
if slow_mode:
cv2.putText(overlay, "SLOW MODE", (width - 66, height - 10), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA)
# Ignore dark frames
if hist_perc[0] > 50:
# Show that this is an ignored frame in the top right
cv2.putText(overlay, "DARK FRAME", (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 0, 255), 0, cv2.LINE_AA)
else:
# SHow that this is an active frame
cv2.putText(overlay, "SCAN FRAME", (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA)
# Get the locations of all faces and their locations
face_locations = face_recognition.face_locations(frame)
# Loop though all faces and paint a circle around them
for loc in face_locations:
# Get the center X and Y from the rectangular points
x = int((loc[1] - loc[3]) / 2) + loc[3]
y = int((loc[2] - loc[0]) / 2) + loc[0]
# Get the raduis from the with of the square
r = (loc[1] - loc[3]) / 2
# Add 20% padding
r = int(r + (r * 0.2))
# Draw the Circle in green
cv2.circle(overlay, (x, y), r, (0, 0, 230), 2)
# Add the overlay to the frame with some transparency
alpha = 0.65
cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)
# Show the image in a window
cv2.imshow("Howdy Test", frame)
# Quit on any keypress
if cv2.waitKey(1) != -1:
raise KeyboardInterrupt()
# Delay the frame if slowmode is on
if slow_mode:
time.sleep(.55)
# On ctrl+C
except KeyboardInterrupt:
# Let the user know we're stopping
print("\nClosing window")
# Release handle to the webcam
video_capture.release()
cv2.destroyAllWindows()

View file

@ -1,18 +1,20 @@
# Compare incomming video with known faces # Compare incomming video with known faces
# Running in a local python instance to get around PATH issues # Running in a local python instance to get around PATH issues
# Import time so we can start timing asap
import time
# Start timing
timings = [time.time()]
# Import required modules # Import required modules
import cv2 import cv2
import sys import sys
import os import os
import json import json
import time
import math import math
import configparser import configparser
# Start timing
timings = [time.time()]
# Read config from disk # Read config from disk
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
@ -37,6 +39,8 @@ models = []
encodings = [] encodings = []
# Amount of frames already matched # Amount of frames already matched
tries = 0 tries = 0
# Amount of ingnored dark frames
dark_tries = 0
# Try to load the face model from the models folder # Try to load the face model from the models folder
try: try:
@ -52,13 +56,21 @@ if len(models) < 1:
for model in models: for model in models:
encodings += model["data"] encodings += model["data"]
# Import face recognition, takes some time # Add the time needed to start the script
timings.append(time.time())
import face_recognition
timings.append(time.time()) timings.append(time.time())
# Start video capture on the IR camera # Start video capture on the IR camera
video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) video_capture = cv2.VideoCapture(int(config.get("video", "device_id")))
# 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.read()
# Note the time it took to open the camera
timings.append(time.time())
# Import face recognition, takes some time
import face_recognition
timings.append(time.time()) timings.append(time.time())
# Fetch the max frame height # Fetch the max frame height
@ -67,12 +79,23 @@ max_height = int(config.get("video", "max_height"))
# Start the read loop # Start the read loop
frames = 0 frames = 0
while True: while True:
# Increment the frame count every loop
frames += 1 frames += 1
# Grab a single frame of video # Grab a single frame of video
# Don't remove ret, it doesn't work without it # Don't remove ret, it doesn't work without it
ret, frame = video_capture.read() ret, frame = video_capture.read()
# Create a histogram of the image with 8 values
hist = cv2.calcHist([frame], [0], None, [8], [0, 256])
# All values combined for percentage calculation
hist_total = int(sum(hist)[0])
# Scrip the frame if it exceeds the threshold
if float(hist[0]) / hist_total * 100 > float(config.get("video", "dark_threshold")):
dark_tries += 1
continue
# Get the height and with of the image # Get the height and with of the image
height, width = frame.shape[:2] height, width = frame.shape[:2]
@ -114,21 +137,24 @@ while True:
print("Time spend") print("Time spend")
print_timing("Starting up", 0) print_timing("Starting up", 0)
print_timing("Importing face_recognition", 1) print_timing("Opening the camera", 1)
print_timing("Opening the camera", 2) print_timing("Importing face_recognition", 2)
print_timing("Searching for known face", 3) print_timing("Searching for known face", 3)
print("\nResolution") print("\nResolution")
print(" Native: " + str(height) + "x" + str(width)) print(" Native: " + str(height) + "x" + str(width))
print(" Used: " + str(scale_height) + "x" + str(scale_width)) print(" Used: " + str(scale_height) + "x" + str(scale_width))
print("\nFrames searched: " + str(frames) + " (" + str(round(float(frames) / (timings[4] - timings[2]), 2)) + " fps)") # Show the total number of frames and calculate the FPS by deviding it by the total scan time
print("\nFrames searched: " + str(frames) + " (" + str(round(float(frames) / (timings[4] - timings[3]), 2)) + " fps)")
print("Dark frames ignored: " + str(dark_tries))
print("Certainty of winning frame: " + str(round(match * 10, 3))) print("Certainty of winning frame: " + str(round(match * 10, 3)))
exposures = ["long", "medium", "short"] # Catch older 3-encoding models
model_id = math.floor(float(match_index) / 3) if not match_index in models:
match_index = 0
print("Winning model: " + str(model_id) + " (\"" + models[model_id]["label"] + "\") using " + exposures[match_index % 3] + " exposure\n") print("Winning model: " + str(match_index) + " (\"" + models[match_index]["label"] + "\")")
# End peacegully # End peacegully
stop(0) stop(0)

View file

@ -22,6 +22,12 @@ device_id = 1
# Speeds up face recognition but can make it less precise # Speeds up face recognition but can make it less precise
max_height = 320 max_height = 320
# Because of flashing IR emitters, some frames can be completely unlit
# Skip the frame if the lowest 1/8 of the histogram is above this percentage
# of the total
# The lower this setting is, the more dark frames are ignored
dark_threshold = 50
[debug] [debug]
# Show a short but detailed diagnostic report in console # Show a short but detailed diagnostic report in console
end_report = false end_report = false

View file

@ -36,7 +36,7 @@ time.sleep(.5)
log("Installing required apt packages") log("Installing required apt packages")
# Install packages though apt # Install packages though apt
handleStatus(subprocess.call(["apt", "install", "-y", "libpam-python", "fswebcam", "libopencv-dev", "python-opencv"])) handleStatus(subprocess.call(["apt", "install", "-y", "git", "libpam-python", "fswebcam", "libopencv-dev", "python-opencv"]))
log("Starting camera check") log("Starting camera check")