Merge pull request #8 from Boltgolt/dev
A wide range of fixes and improvements
This commit is contained in:
commit
10aae8266c
9 changed files with 298 additions and 91 deletions
|
|
@ -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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
60
cli.py
60
cli.py
|
|
@ -1,41 +1,53 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# CLI directly called by running the howdy command
|
||||
|
||||
# Import required modules
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Check if the minimum of 3 arugemnts has been met and print help otherwise
|
||||
if (len(sys.argv) < 3):
|
||||
# Check if if a command has been given and print help otherwise
|
||||
if (len(sys.argv) < 2):
|
||||
print("Howdy IR face recognition help")
|
||||
import cli.help
|
||||
sys.exit()
|
||||
|
||||
# The command given
|
||||
cmd = sys.argv[2]
|
||||
cmd = sys.argv[1]
|
||||
|
||||
# Requre sudo for comamnds that need root rights to read the model files
|
||||
if cmd in ["list", "add", "remove", "clear"] and os.getenv("SUDO_USER") is None:
|
||||
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":
|
||||
# Call the right files for commands that don't need root
|
||||
if cmd == "help":
|
||||
print("Howdy IR face recognition")
|
||||
import cli.help
|
||||
elif cmd == "add":
|
||||
import cli.add
|
||||
elif cmd == "remove":
|
||||
import cli.remove
|
||||
elif cmd == "clear":
|
||||
import cli.clear
|
||||
elif cmd == "test":
|
||||
import cli.test
|
||||
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", "help"]:
|
||||
print("Usage: howdy <user> <command>")
|
||||
else:
|
||||
print('Unknown command "' + cmd + '"')
|
||||
# Check if the minimum of 3 arugemnts has been met and print help otherwise
|
||||
if (len(sys.argv) < 3):
|
||||
print("Howdy IR face recognition 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# Marks this folder as importable
|
||||
104
cli/add.py
104
cli/add.py
|
|
@ -6,9 +6,9 @@ import time
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import cv2
|
||||
import configparser
|
||||
|
||||
|
||||
# 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
|
||||
try:
|
||||
|
|
@ -27,50 +27,8 @@ path = os.path.dirname(os.path.abspath(__file__))
|
|||
config = configparser.ConfigParser()
|
||||
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
|
||||
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
|
||||
enc_file = path + "/../models/" + user + ".dat"
|
||||
# Known encodings
|
||||
|
|
@ -87,6 +45,11 @@ try:
|
|||
except FileNotFoundError:
|
||||
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)
|
||||
|
||||
# Set the default label
|
||||
|
|
@ -111,15 +74,53 @@ insert_model = {
|
|||
"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
|
||||
time.sleep(2)
|
||||
|
||||
# Capture with 3 different delays to simulate different camera exposures
|
||||
for delay in [30, 6, 0]:
|
||||
time.sleep(.3)
|
||||
captureFrame(delay)
|
||||
# Will contain found face encodings
|
||||
enc = []
|
||||
# Count the amount or read frames
|
||||
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
|
||||
encodings.append(insert_model)
|
||||
|
|
@ -128,7 +129,6 @@ encodings.append(insert_model)
|
|||
with open(enc_file, "w") as datafile:
|
||||
json.dump(encodings, datafile)
|
||||
|
||||
# Remove any left over temp files
|
||||
os.remove(tmp_file)
|
||||
|
||||
print("Done.")
|
||||
# Give let the user know how it went
|
||||
print("Scan complete")
|
||||
print("\nAdded a new model to " + user)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Commands:
|
|||
add Add a new face model for the current user
|
||||
remove [id] Remove a specific model
|
||||
clear Remove all face models for the current user
|
||||
test Test the camera and recognition methods
|
||||
|
||||
For support please visit
|
||||
https://github.com/Boltgolt/howdy\
|
||||
|
|
|
|||
161
cli/test.py
Normal file
161
cli/test.py
Normal 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()
|
||||
52
compare.py
52
compare.py
|
|
@ -1,18 +1,20 @@
|
|||
# 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 = [time.time()]
|
||||
|
||||
# Import required modules
|
||||
import cv2
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import math
|
||||
import configparser
|
||||
|
||||
# Start timing
|
||||
timings = [time.time()]
|
||||
|
||||
# Read config from disk
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
|
||||
|
|
@ -37,6 +39,8 @@ models = []
|
|||
encodings = []
|
||||
# Amount of frames already matched
|
||||
tries = 0
|
||||
# Amount of ingnored dark frames
|
||||
dark_tries = 0
|
||||
|
||||
# Try to load the face model from the models folder
|
||||
try:
|
||||
|
|
@ -52,13 +56,21 @@ if len(models) < 1:
|
|||
for model in models:
|
||||
encodings += model["data"]
|
||||
|
||||
# Import face recognition, takes some time
|
||||
timings.append(time.time())
|
||||
import face_recognition
|
||||
# Add the time needed to start the script
|
||||
timings.append(time.time())
|
||||
|
||||
# Start video capture on the IR camera
|
||||
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())
|
||||
|
||||
# Fetch the max frame height
|
||||
|
|
@ -67,12 +79,23 @@ max_height = int(config.get("video", "max_height"))
|
|||
# Start the read loop
|
||||
frames = 0
|
||||
while True:
|
||||
# Increment the frame count every loop
|
||||
frames += 1
|
||||
|
||||
# Grab a single frame of video
|
||||
# Don't remove ret, it doesn't work without it
|
||||
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
|
||||
height, width = frame.shape[:2]
|
||||
|
||||
|
|
@ -114,21 +137,24 @@ while True:
|
|||
|
||||
print("Time spend")
|
||||
print_timing("Starting up", 0)
|
||||
print_timing("Importing face_recognition", 1)
|
||||
print_timing("Opening the camera", 2)
|
||||
print_timing("Opening the camera", 1)
|
||||
print_timing("Importing face_recognition", 2)
|
||||
print_timing("Searching for known face", 3)
|
||||
|
||||
print("\nResolution")
|
||||
print(" Native: " + str(height) + "x" + str(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)))
|
||||
|
||||
exposures = ["long", "medium", "short"]
|
||||
model_id = math.floor(float(match_index) / 3)
|
||||
# Catch older 3-encoding models
|
||||
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
|
||||
stop(0)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ device_id = 1
|
|||
# Speeds up face recognition but can make it less precise
|
||||
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]
|
||||
# Show a short but detailed diagnostic report in console
|
||||
end_report = false
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ time.sleep(.5)
|
|||
log("Installing required apt packages")
|
||||
|
||||
# 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")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue