diff --git a/README.md b/README.md index a6fda35..3645b24 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cli.py b/cli.py index e820426..8e9e6ab 100755 --- a/cli.py +++ b/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 ") - 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 ") + else: + print('Unknown command "' + cmd + '"') + import cli.help diff --git a/cli/__init__.py b/cli/__init__.py index e69de29..6a31920 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -0,0 +1 @@ +# Marks this folder as importable diff --git a/cli/add.py b/cli/add.py index 9b2e6d9..427ce4d 100644 --- a/cli/add.py +++ b/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) diff --git a/cli/help.py b/cli/help.py index 2b09848..2bbd879 100644 --- a/cli/help.py +++ b/cli/help.py @@ -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\ diff --git a/cli/test.py b/cli/test.py new file mode 100644 index 0000000..191ce66 --- /dev/null +++ b/cli/test.py @@ -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() diff --git a/compare.py b/compare.py index 101f9fe..db1dde9 100644 --- a/compare.py +++ b/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) diff --git a/config.ini b/config.ini index a019e35..8e92636 100644 --- a/config.ini +++ b/config.ini @@ -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 diff --git a/installer.py b/installer.py index 52be40f..a9a889b 100644 --- a/installer.py +++ b/installer.py @@ -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")