From 140b4f3d6741778e8f9b774d0933af23c0915ebf Mon Sep 17 00:00:00 2001 From: Anthony Wharton Date: Sun, 12 May 2019 18:40:28 +0100 Subject: [PATCH 01/26] Add VideoCapture class --- src/recorders/video_capture.py | 116 +++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/recorders/video_capture.py diff --git a/src/recorders/video_capture.py b/src/recorders/video_capture.py new file mode 100644 index 0000000..d488ebe --- /dev/null +++ b/src/recorders/video_capture.py @@ -0,0 +1,116 @@ +# Top level class for a video capture providing simplified API's for common +# functions + +# Import required modules +import configparser +import cv2 +import os +import sys + +""" +Class to provide boilerplate code to build a video recorder with the +correct settings from the config file. + +The internal recorder can be accessed with 'video_capture.internal' +""" +class VideoCapture: + + """ + Creates a new VideoCapture instance depending on the settings in the + provided config file. + + Config can either be a string to the path, or a pre-setup configparser. + """ + def __init__(self, config): + if isinstance(config, str): + self.config = configparser.ConfigParser() + self.config.read(config) + else: + self.config = config + + # Check device path + if not os.path.exists(config.get("video", "device_path")): + print("Camera path is not configured correctly, " + + "please edit the 'device_path' config value.") + sys.exit(1) + + # Create reader + self.internal = None # The internal video recorder + self.fw = None # The frame width + self.fh = None # The frame height + self._create_reader() + + # Request a frame to wake the camera up + self.internal.grab() + + """ + Frees resources when destroyed + """ + def __del__(self): + if self is not None: + self.internal.release() + + """ + Reads a frame, returns the frame and an attempted grayscale conversion of + the frame in a tuple: + + (frame, grayscale_frame) + + If the grayscale conversion fails, both items in the tuple are identical. + """ + def read_frame(self): + # Grab a single frame of video + # Don't remove ret, it doesn't work without it + ret, frame = self.internal.read() + if not ret: + print("Failed to read camera specified in your 'device_path', " + + "aborting") + sys.exit(1) + + 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("\nAn error occurred in OpenCV\n") + raise + return frame, gsframe + + """ + Sets up the video reader instance + """ + def _create_reader(self): + if self.config.get("video", "recording_plugin") == "ffmpeg": + # Set the capture source for ffmpeg + from recorders.ffmpeg_reader import ffmpeg_reader + self.internal = ffmpeg_reader( + self.config.get("video", "device_path"), + self.config.get("video", "device_format") + ) + elif self.config.get("video", "recording_plugin") == "pyv4l2": + # Set the capture source for pyv4l2 + from recorders.pyv4l2_reader import pyv4l2_reader + self.internal = pyv4l2_reader( + self.config.get("video", "device_path"), + self.config.get("video", "device_format") + ) + else: + # Start video capture on the IR camera through OpenCV + self.internal = cv2.VideoCapture( + self.config.get("video", "device_path") + ) + + # Force MJPEG decoding if true + if self.config.getboolean("video", "force_mjpeg", fallback=False): + # Set a magic number, will enable MJPEG but is badly documentated + self.internal.set(cv2.CAP_PROP_FOURCC, 1196444237) + + # Set the frame width and height if requested + self.fw = self.config.getint("video", "frame_width", fallback=-1) + self.fh = self.config.getint("video", "frame_height", fallback=-1) + if self.fw != -1: + self.internal.set(cv2.CAP_PROP_FRAME_WIDTH, self.fw) + if self.fh != -1: + self.internal.set(cv2.CAP_PROP_FRAME_HEIGHT, self.fh) From 1555ffe82fbee074090b8ccc6a417f6647a0958e Mon Sep 17 00:00:00 2001 From: Anthony Wharton Date: Sun, 12 May 2019 18:57:26 +0100 Subject: [PATCH 02/26] Refactor compare.py to use VideoCapture --- src/compare.py | 67 +++++++------------------------------------------- 1 file changed, 9 insertions(+), 58 deletions(-) diff --git a/src/compare.py b/src/compare.py index 98c6b72..17e1524 100644 --- a/src/compare.py +++ b/src/compare.py @@ -18,6 +18,7 @@ import cv2 import dlib import numpy as np import _thread as thread +from recorders.video_capture import VideoCapture def init_detector(lock): @@ -46,13 +47,6 @@ def init_detector(lock): 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) @@ -113,36 +107,7 @@ 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")) - -# 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() +video_capture = VideoCapture(config) # Read exposure from config to use in the main loop exposure = config.getint("video", "exposure", fallback=-1) @@ -158,7 +123,7 @@ 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 +height = video_capture.internal.get(cv2.CAP_PROP_FRAME_HEIGHT) or 1 # Calculate the amount the image has to shrink scaling_factor = (max_height / height) or 1 @@ -178,24 +143,10 @@ while True: # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: - stop(11) + sys.exit(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 + frame, gsframe = video_capture.read_frame() # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) @@ -255,7 +206,7 @@ while True: print_timing("Total time", "tt") print("\nResolution") - width = video_capture.get(cv2.CAP_PROP_FRAME_WIDTH) or 1 + width = video_capture.fw or 1 print(" Native: %dx%d" % (height, width)) # Save the new size for diagnostics scale_height, scale_width = frame.shape[:2] @@ -269,7 +220,7 @@ while True: print("Winning model: %d (\"%s\")" % (match_index, models[match_index]["label"])) # End peacefully - stop(0) + sys.exit(0) if exposure != -1: # For a strange reason on some cameras (e.g. Lenoxo X1E) @@ -277,5 +228,5 @@ while True: # 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)) + video_capture.intenal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual + video_capture.intenal.set(cv2.CAP_PROP_EXPOSURE, float(exposure)) From 804016d1a07739dc651575d6bb7c8195db4194e4 Mon Sep 17 00:00:00 2001 From: Anthony Wharton Date: Sun, 12 May 2019 18:40:59 +0100 Subject: [PATCH 03/26] Refactor add.py to use VideoCapture --- src/cli/add.py | 44 ++++---------------------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/src/cli/add.py b/src/cli/add.py index 3106055..a345037 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -9,6 +9,7 @@ import configparser import builtins import cv2 import numpy as np +from recorders.video_capture import VideoCapture # Try to import dlib and give a nice error if we can't # Add should be the first point where import issues show up @@ -35,10 +36,6 @@ if not os.path.isfile(path + "/../dlib-data/shape_predictor_5_face_landmarks.dat config = configparser.ConfigParser() config.read(path + "/../config.ini") -if not os.path.exists(config.get("video", "device_path")): - print("Camera path is not configured correctly, please edit the 'device_path' config value.") - sys.exit(1) - use_cnn = config.getboolean("core", "use_cnn", fallback=False) if use_cnn: face_detector = dlib.cnn_face_detection_model_v1(path + "/../dlib-data/mmod_human_face_detector.dat") @@ -98,36 +95,8 @@ insert_model = { "data": [] } -# 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")) - -# Force MJPEG decoding if true -if config.getboolean("video", "force_mjpeg", fallback=False): - # Set a magic number, will enable MJPEG but is badly documentated - 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) - -# Request a frame to wake the camera up -video_capture.grab() - +# Set up video_capture +video_capture = VideoCapture(config) print("\nPlease look straight into the camera") # Give the user time to read @@ -141,10 +110,7 @@ dark_threshold = config.getfloat("video", "dark_threshold") # Loop through frames till we hit a timeout while frames < 60: - # Grab a single frame of video - # Don't remove ret, it doesn't work without it - ret, frame = video_capture.read() - gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + frame, gsframe = video_capture.read_frame() # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) @@ -165,8 +131,6 @@ while frames < 60: if face_locations: break -video_capture.release() - # If more than 1 faces are detected we can't know wich one belongs to the user if len(face_locations) > 1: print("Multiple faces detected, aborting") From 200e536b5eacb72f9fe4a7409958f803540e29b7 Mon Sep 17 00:00:00 2001 From: Anthony Wharton Date: Sun, 12 May 2019 18:48:50 +0100 Subject: [PATCH 04/26] Refactor test.py to use VideoCapture --- src/cli/test.py | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/cli/test.py b/src/cli/test.py index 16fadb1..de92ce3 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -7,6 +7,7 @@ import sys import time import cv2 import dlib +from recorders.video_capture import VideoCapture # Get the absolute path to the current file path = os.path.dirname(os.path.abspath(__file__)) @@ -20,22 +21,7 @@ if config.get("video", "recording_plugin") != "opencv": print("Aborting") sys.exit(12) -# Start capturing from the configured webcam -video_capture = cv2.VideoCapture(config.get("video", "device_path")) - -# Force MJPEG decoding if true -if config.getboolean("video", "force_mjpeg", fallback=False): - # Set a magic number, will enable MJPEG but is badly documented - 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) +video_capture = VideoCapture(config) # Read exposure and dark_thresholds from config to use in the main loop exposure = config.getint("video", "exposure", fallback=-1) @@ -109,17 +95,7 @@ try: sec_frames = 0 # Grab a single frame of video - ret, frame = video_capture.read() - - try: - # Convert from color to grayscale - # First processing of frame, so frame errors show up here - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - except RuntimeError: - pass - except cv2.error: - print("\nUnknown camera, please check your 'device_path' config value.\n") - raise + _, frame = video_capture.read_frame() frame = clahe.apply(frame) # Make a frame to put overlays in @@ -211,8 +187,8 @@ try: # 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)) + video_capture.intenal.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual + video_capture.intenal.set(cv2.CAP_PROP_EXPOSURE, float(exposure)) # On ctrl+C except KeyboardInterrupt: @@ -220,5 +196,4 @@ except KeyboardInterrupt: print("\nClosing window") # Release handle to the webcam - video_capture.release() cv2.destroyAllWindows() From 4d614ca44370eccb8dbddfbe4fbdd46828615d3d Mon Sep 17 00:00:00 2001 From: Karl-Philipp Richter Date: Tue, 14 May 2019 15:47:02 +0200 Subject: [PATCH 05/26] Expanded Python versions in .travis.yml --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6f7d486..847bbae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,11 @@ sudo: required +dist: xenial language: python -python: "3.6" +python: + - "3.4" + - "3.6" + - "3.7" + - "3.8-dev" script: # Build the binary (.deb) From 67bc656801411eeced33e1bced8f44a1c30fcd0c Mon Sep 17 00:00:00 2001 From: Alan Rager Date: Sat, 29 Jun 2019 07:21:26 -0700 Subject: [PATCH 06/26] use ffmpeg instead of streamer for checking camera --- debian/control | 2 +- debian/preinst | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 2b4262b..9f65b8d 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: all -Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, cmake, streamer +Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, cmake, ffmpeg Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. diff --git a/debian/preinst b/debian/preinst index e355a15..3a5ce2c 100755 --- a/debian/preinst +++ b/debian/preinst @@ -73,7 +73,12 @@ for dev in devices: print("Trying " + device_name) # Let fswebcam keep the camera open in the background - sub = subprocess.Popen(["streamer -t 1:0:0 -c /dev/v4l/by-path/" + dev + " -b 16 -f rgb24 -o /dev/null 1>/dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid) + sub = subprocess.Popen( + "ffmpeg -f v4l2 -framerate 25 -video_size 640_480 -i /dev/v4l/by-path/%s -f null -loglevel -8 /dev/null" % dev, + shell=True, + preexec_fn=os.setsid, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) try: # Ask the user if this is the right one From 0eb26c044d54bd289c1bc4efdbfe0722f9ce0bfa Mon Sep 17 00:00:00 2001 From: Alan Rager Date: Sat, 6 Jul 2019 13:02:59 -0700 Subject: [PATCH 07/26] use fswebcam instead --- debian/control | 2 +- debian/preinst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 9f65b8d..efb16a1 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: all -Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, cmake, ffmpeg +Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, cmake, fswebcam Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. diff --git a/debian/preinst b/debian/preinst index 3a5ce2c..8430d4a 100755 --- a/debian/preinst +++ b/debian/preinst @@ -74,7 +74,7 @@ for dev in devices: # Let fswebcam keep the camera open in the background sub = subprocess.Popen( - "ffmpeg -f v4l2 -framerate 25 -video_size 640_480 -i /dev/v4l/by-path/%s -f null -loglevel -8 /dev/null" % dev, + "fswebcam -l 1 -d /dev/v4l/by-path/%s" % dev, shell=True, preexec_fn=os.setsid, stdout=subprocess.PIPE, From 983ab0a9d0e408046601b644d83b09ee02875d17 Mon Sep 17 00:00:00 2001 From: ajayTDM <35770004+Ajayneethikannan@users.noreply.github.com> Date: Sat, 28 Sep 2019 15:28:30 +0530 Subject: [PATCH 08/26] Fix small typo in compare.py --- src/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compare.py b/src/compare.py index 98c6b72..6f71606 100644 --- a/src/compare.py +++ b/src/compare.py @@ -94,7 +94,7 @@ 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) +timeout = config.getint("video", "timeout", 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) From de8aaf5aeeb3eee622409439c0b3a2dbe963ba6b Mon Sep 17 00:00:00 2001 From: Rushabh Vasani <46084304+RushabhVasani@users.noreply.github.com> Date: Fri, 18 Oct 2019 16:54:39 +0530 Subject: [PATCH 09/26] Update postinst --- debian/postinst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/postinst b/debian/postinst index 7ab3b89..4ede83c 100755 --- a/debian/postinst +++ b/debian/postinst @@ -109,6 +109,12 @@ log("Upgrading pip to the latest version") # Update pip handleStatus(sc(["pip3", "install", "--upgrade", "pip"])) + +log("Upgrading numpy to the lateset version") + +# Update numpy +handleStatus(subprocess.call(["pip3", "install", "--upgrade", "numpy"])) + log("Downloading and unpacking data files") # Run the bash script to download and unpack the .dat files needed From 0444ca933fbb08ebfb892b313972b42064193519 Mon Sep 17 00:00:00 2001 From: Peter-Simon Dieterich Date: Fri, 1 Nov 2019 11:49:19 +0100 Subject: [PATCH 10/26] Fix missing colors in overlay --- src/cli/test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cli/test.py b/src/cli/test.py index 16fadb1..02a557c 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -124,6 +124,7 @@ try: frame = clahe.apply(frame) # Make a frame to put overlays in overlay = frame.copy() + overlay = cv2.cvtColor(overlay, cv2.COLOR_GRAY2BGR) # Fetch the frame height and width height, width = frame.shape[:2] @@ -190,6 +191,7 @@ try: # Add the overlay to the frame with some transparency alpha = 0.65 + frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR) cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame) # Show the image in a window From 799628eddf64a5264b7d76ac2eb263990dbd3f59 Mon Sep 17 00:00:00 2001 From: Peter-Simon Dieterich Date: Fri, 1 Nov 2019 11:53:17 +0100 Subject: [PATCH 11/26] Apply CLAHE to frames in add.py and compare.py --- src/cli/add.py | 3 +++ src/compare.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/cli/add.py b/src/cli/add.py index 3106055..44b4480 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -139,12 +139,15 @@ enc = [] frames = 0 dark_threshold = config.getfloat("video", "dark_threshold") +clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + # Loop through frames till we hit a timeout while frames < 60: # Grab a single frame of video # Don't remove ret, it doesn't work without it ret, frame = video_capture.read() gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + gsframe = clahe.apply(gsframe) # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) diff --git a/src/compare.py b/src/compare.py index 98c6b72..228f227 100644 --- a/src/compare.py +++ b/src/compare.py @@ -172,6 +172,8 @@ end_report = config.getboolean("debug", "end_report") frames = 0 timings["fr"] = time.time() +clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + while True: # Increment the frame count every loop frames += 1 @@ -197,6 +199,8 @@ while True: print("\nUnknown camera, please check your 'device_path' config value.\n") raise + gsframe = clahe.apply(gsframe) + # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) # All values combined for percentage calculation From b7f2b665925a90b7bcb55fa54e2b12069f981019 Mon Sep 17 00:00:00 2001 From: Musikid Date: Sun, 1 Mar 2020 10:37:13 +0100 Subject: [PATCH 12/26] Fix the fedora .spec file --- fedora/howdy.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedora/howdy.spec b/fedora/howdy.spec index bfd7f63..2541943 100644 --- a/fedora/howdy.spec +++ b/fedora/howdy.spec @@ -32,7 +32,7 @@ Requires: python3dist(v4l2) Requires: python3-face_recognition Supplements: python3-face_recognition_models Requires: python3-opencv -Requires: python3-pam +Requires: pam_python %endif From e7b5262683e3425bc8850bcc180842d16d735394 Mon Sep 17 00:00:00 2001 From: Arthur Bols Date: Sun, 3 May 2020 16:16:27 +0200 Subject: [PATCH 13/26] Fix fedora dependencies --- fedora/howdy.spec | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fedora/howdy.spec b/fedora/howdy.spec index 2541943..c558f35 100644 --- a/fedora/howdy.spec +++ b/fedora/howdy.spec @@ -9,7 +9,7 @@ Version: 2.5.1 %if %{with_snapshot} Release: 0.1.git.%{date}%{shortcommit}%{?dist} %else -Release: 3%{?dist} +Release: 4%{?dist} %endif Summary: Windows Helloâ„¢ style authentication for Linux @@ -27,10 +27,7 @@ BuildRequires: polkit-devel %if 0%{?fedora} # We need python3-devel for pathfix.py BuildRequires: python3-devel -Requires: python3dist(dlib) >= 6.0 -Requires: python3dist(v4l2) -Requires: python3-face_recognition -Supplements: python3-face_recognition_models +Requires: python3dist(dlib) >= 6.0 Requires: python3-opencv Requires: pam_python %endif From 20974697429a2cd1a1d922bdefb9b2c5cc418bfc Mon Sep 17 00:00:00 2001 From: Andrew Villeneuve Date: Mon, 8 Jun 2020 20:55:26 -0700 Subject: [PATCH 14/26] Detect and report errors caused by incorrectly set dark_threshold --- src/compare.py | 25 +++++++++++++++++++++++-- src/pam.py | 4 ++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/compare.py b/src/compare.py index c316fef..3bc9d03 100644 --- a/src/compare.py +++ b/src/compare.py @@ -66,6 +66,8 @@ user = sys.argv[1] 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 @@ -170,7 +172,9 @@ 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 @@ -178,6 +182,11 @@ while True: # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: + average_darkness = dark_running_total / frames + 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 @@ -202,9 +211,20 @@ while True: # All values combined for percentage calculation hist_total = np.sum(hist) - # If the image is fully black or the frame exceeds threshold, + # 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 (hist[0] / hist_total * 100 > dark_threshold): + 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 @@ -263,6 +283,7 @@ while True: # 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, )) diff --git a/src/pam.py b/src/pam.py index 3d44114..ea8ae16 100644 --- a/src/pam.py +++ b/src/pam.py @@ -50,6 +50,10 @@ def doAuth(pamh): # Status 12 means we aborted elif status == 12: return pamh.PAM_AUTH_ERR + # Status 13 means the image was too dark + elif status == 13: + pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection image too dark")) + return pamh.PAM_AUTH_ERR # Status 0 is a successful exit elif status == 0: # Show the success message if it isn't suppressed From f6629776d840b08fae55cabae7bfcac4e149b5e5 Mon Sep 17 00:00:00 2001 From: Andrew Villeneuve Date: Tue, 9 Jun 2020 12:30:45 -0700 Subject: [PATCH 15/26] Added darkness detection to face registration logic --- src/cli/add.py | 46 +++++++++++++++++++++++++++++++++++++--------- src/compare.py | 1 - 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/cli/add.py b/src/cli/add.py index e97c627..8b489b3 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -135,12 +135,22 @@ time.sleep(2) # Will contain found face encodings enc = [] -# Count the amount or read frames +# Count the number of read frames frames = 0 +# Count the number of illuminated read frames +valid_frames = 0 +# Count the number of illuminated frames that +# were rejected for being too dark +dark_tries = 0 +# Track the running darkness total +dark_running_total = 0 +face_locations = None + dark_threshold = config.getfloat("video", "dark_threshold") # 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() @@ -151,12 +161,23 @@ while frames < 60: # All values combined for percentage calculation hist_total = np.sum(hist) - # If the image is fully black or the frame exceeds threshold, + # 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 (hist[0] / hist_total * 100 > dark_threshold): + if (hist_total == 0) or (darkness == 100): continue - frames += 1 + # Include this frame in calculating our average session brightness + 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 # Get all faces from that frame as encodings face_locations = face_detector(gsframe, 1) @@ -167,12 +188,19 @@ while frames < 60: video_capture.release() -# If more than 1 faces are detected we can't know wich one belongs to the user -if len(face_locations) > 1: - print("Multiple faces detected, aborting") +# If we've found no faces, try to determine why +if face_locations is None or not face_locations: + if valid_frames == 0: + print("Camera saw only black frames - is IR emitter working?") + elif valid_frames == dark_tries: + 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)) + else: + print("No face detected, aborting") sys.exit(1) -elif not face_locations: - print("No face detected, aborting") +# If more than 1 faces are detected we can't know wich one belongs to the user +elif len(face_locations) > 1: + print("Multiple faces detected, aborting") sys.exit(1) face_location = face_locations[0] diff --git a/src/compare.py b/src/compare.py index 3bc9d03..8f3356d 100644 --- a/src/compare.py +++ b/src/compare.py @@ -182,7 +182,6 @@ while True: # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: - average_darkness = dark_running_total / frames 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)) From f826d91a30c2f199e1c1408f8f715fe07eaacf2a Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Sat, 20 Jun 2020 12:34:51 -0400 Subject: [PATCH 16/26] Inaccurate cli message fixed --- src/cli/list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/list.py b/src/cli/list.py index 08a1e9f..2cc9b9c 100644 --- a/src/cli/list.py +++ b/src/cli/list.py @@ -14,7 +14,7 @@ user = builtins.howdy_user # Check if the models file has been created yet if not os.path.exists(path + "/models"): print("Face models have not been initialized yet, please run:") - print("\n\thowdy " + user + " add\n") + print("\n\tsudo howdy -U " + user + " add\n") sys.exit(1) # Path to the models file From a04b33d01bc0378455e33226ab3d25b9b11b497f Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 21 Jun 2020 17:40:44 +0200 Subject: [PATCH 17/26] Reenfocing code style --- src/cli/disable.py | 1 - src/cli/test.py | 2 + src/compare.py | 9 +++-- src/config.ini | 2 +- src/recorders/video_capture.py | 70 ++++++++++++++++++---------------- 5 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/cli/disable.py b/src/cli/disable.py index cba8931..5f15d5b 100644 --- a/src/cli/disable.py +++ b/src/cli/disable.py @@ -3,7 +3,6 @@ # Import required modules import sys import os -import json import builtins import fileinput import configparser diff --git a/src/cli/test.py b/src/cli/test.py index fa2d7cd..21cb2db 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -49,7 +49,9 @@ 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) + use_cnn = config.getboolean('core', 'use_cnn', fallback=False) + if use_cnn: face_detector = dlib.cnn_face_detection_model_v1( path + '/../dlib-data/mmod_human_face_detector.dat' diff --git a/src/compare.py b/src/compare.py index bef7df5..8ddd0c7 100644 --- a/src/compare.py +++ b/src/compare.py @@ -47,6 +47,7 @@ def init_detector(lock): timings["ll"] = time.time() - timings["ll"] lock.release() + # Make sure we were given an username to tast against if len(sys.argv) < 2: sys.exit(12) @@ -149,17 +150,17 @@ while True: # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: - if (dark_tries == valid_frames ): + 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)) - sys.exit(13) + sys.exit(13) else: - sys.exit(11) + sys.exit(11) # Grab a single frame of video frame, gsframe = video_capture.read_frame() - gsframe = clahe.apply(gsframe) + gsframe = clahe.apply(gsframe) # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) diff --git a/src/config.ini b/src/config.ini index 1183f48..a6a0fa3 100644 --- a/src/config.ini +++ b/src/config.ini @@ -37,7 +37,7 @@ timeout = 4 # The path of the device to capture frames from # Should be set automatically by an installer if your distro has one -device_path = none +device_path = "/dev/video1" # Scale down the video feed to this maximum height # Speeds up face recognition but can make it less precise diff --git a/src/recorders/video_capture.py b/src/recorders/video_capture.py index d488ebe..af64ba2 100644 --- a/src/recorders/video_capture.py +++ b/src/recorders/video_capture.py @@ -7,21 +7,22 @@ import cv2 import os import sys -""" -Class to provide boilerplate code to build a video recorder with the -correct settings from the config file. -The internal recorder can be accessed with 'video_capture.internal' -""" +# Class to provide boilerplate code to build a video recorder with the +# correct settings from the config file. +# +# The internal recorder can be accessed with 'video_capture.internal' + + class VideoCapture: - """ - Creates a new VideoCapture instance depending on the settings in the - provided config file. - - Config can either be a string to the path, or a pre-setup configparser. - """ def __init__(self, config): + """ + Creates a new VideoCapture instance depending on the settings in the + provided config file. + + Config can either be a string to the path, or a pre-setup configparser. + """ if isinstance(config, str): self.config = configparser.ConfigParser() self.config.read(config) @@ -30,41 +31,43 @@ class VideoCapture: # Check device path if not os.path.exists(config.get("video", "device_path")): - print("Camera path is not configured correctly, " + - "please edit the 'device_path' config value.") + print("Camera path is not configured correctly, please edit the 'device_path' config value.") sys.exit(1) # Create reader - self.internal = None # The internal video recorder - self.fw = None # The frame width - self.fh = None # The frame height + # The internal video recorder + self.internal = None + # The frame width + self.fw = None + # The frame height + self.fh = None self._create_reader() # Request a frame to wake the camera up self.internal.grab() - """ - Frees resources when destroyed - """ def __del__(self): + """ + Frees resources when destroyed + """ if self is not None: self.internal.release() - """ - Reads a frame, returns the frame and an attempted grayscale conversion of - the frame in a tuple: - - (frame, grayscale_frame) - - If the grayscale conversion fails, both items in the tuple are identical. - """ def read_frame(self): + """ + Reads a frame, returns the frame and an attempted grayscale conversion of + the frame in a tuple: + + (frame, grayscale_frame) + + If the grayscale conversion fails, both items in the tuple are identical. + """ + # Grab a single frame of video # Don't remove ret, it doesn't work without it ret, frame = self.internal.read() if not ret: - print("Failed to read camera specified in your 'device_path', " + - "aborting") + print("Failed to read camera specified in your 'device_path', aborting") sys.exit(1) try: @@ -78,10 +81,11 @@ class VideoCapture: raise return frame, gsframe - """ - Sets up the video reader instance - """ def _create_reader(self): + """ + Sets up the video reader instance + """ + if self.config.get("video", "recording_plugin") == "ffmpeg": # Set the capture source for ffmpeg from recorders.ffmpeg_reader import ffmpeg_reader @@ -89,6 +93,7 @@ class VideoCapture: self.config.get("video", "device_path"), self.config.get("video", "device_format") ) + elif self.config.get("video", "recording_plugin") == "pyv4l2": # Set the capture source for pyv4l2 from recorders.pyv4l2_reader import pyv4l2_reader @@ -96,6 +101,7 @@ class VideoCapture: self.config.get("video", "device_path"), self.config.get("video", "device_format") ) + else: # Start video capture on the IR camera through OpenCV self.internal = cv2.VideoCapture( From 6e9169e87c40ffdf2702c491a2484c7e3e719000 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 21 Jun 2020 18:12:45 +0200 Subject: [PATCH 18/26] Added loging to auth.log, removed sys.exit from pam handler causing err --- src/pam.py | 20 ++++++++++++++++---- src/recorders/video_capture.py | 5 +++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/pam.py b/src/pam.py index ea8ae16..fda74a9 100644 --- a/src/pam.py +++ b/src/pam.py @@ -2,9 +2,9 @@ # Import required modules import subprocess -import sys import os import glob +import syslog # pam-python is running python 2, so we use the old module here import ConfigParser @@ -19,22 +19,24 @@ def doAuth(pamh): # Abort is Howdy is disabled if config.getboolean("core", "disabled"): - sys.exit(0) + return pamh.PAM_AUTHINFO_UNAVAIL # Abort if we're in a remote SSH env if config.getboolean("core", "ignore_ssh"): if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: - sys.exit(0) + return pamh.PAM_AUTHINFO_UNAVAIL # Abort if lid is closed if config.getboolean("core", "ignore_closed_lid"): if any("closed" in open(f).read() for f in glob.glob("/proc/acpi/button/lid/*/state")): - sys.exit(0) + return pamh.PAM_AUTHINFO_UNAVAIL # Alert the user that we are doing face detection if config.getboolean("core", "detection_notice"): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Attempting facial authentication for user " + pamh.get_user()) + # Run compare as python3 subprocess to circumvent python version and import issues status = subprocess.call(["/usr/bin/python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()]) @@ -42,16 +44,24 @@ def doAuth(pamh): if status == 10: if not config.getboolean("core", "suppress_unknown"): pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "No face model known")) + + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Failure, no face model known") return pamh.PAM_USER_UNKNOWN + # Status 11 means we exceded the maximum retry count elif status == 11: pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection timeout reached")) + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Failure, timeout reached") return pamh.PAM_AUTH_ERR + # Status 12 means we aborted elif status == 12: + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Failure, general abort") return pamh.PAM_AUTH_ERR + # Status 13 means the image was too dark elif status == 13: + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Failure, image too dark") pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection image too dark")) return pamh.PAM_AUTH_ERR # Status 0 is a successful exit @@ -60,10 +70,12 @@ def doAuth(pamh): if not config.getboolean("core", "no_confirmation"): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Login approved") return pamh.PAM_SUCCESS # Otherwise, we can't discribe what happend but it wasn't successful pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Unknown error: " + str(status))) + syslog.syslog(syslog.LOG_AUTH, "[HOWDY] Failure, unknown error" + str(status)) return pamh.PAM_SYSTEM_ERR diff --git a/src/recorders/video_capture.py b/src/recorders/video_capture.py index af64ba2..6c852c4 100644 --- a/src/recorders/video_capture.py +++ b/src/recorders/video_capture.py @@ -15,7 +15,6 @@ import sys class VideoCapture: - def __init__(self, config): """ Creates a new VideoCapture instance depending on the settings in the @@ -23,6 +22,8 @@ class VideoCapture: Config can either be a string to the path, or a pre-setup configparser. """ + + # Parse config from string if nedded if isinstance(config, str): self.config = configparser.ConfigParser() self.config.read(config) @@ -30,7 +31,7 @@ class VideoCapture: self.config = config # Check device path - if not os.path.exists(config.get("video", "device_path")): + if not os.path.exists(self.config.get("video", "device_path")): print("Camera path is not configured correctly, please edit the 'device_path' config value.") sys.exit(1) From 51e17420d7a0271a03395cb4e2936e3fd0908e3d Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 21 Jun 2020 21:40:47 +0200 Subject: [PATCH 19/26] Added the option to save snapshots --- .gitignore | 3 +++ src/compare.py | 49 +++++++++++++++++++++++++++++++++++----- src/config.ini | 11 ++++++++- src/logo.png | Bin 0 -> 3068 bytes src/snapshot.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/logo.png create mode 100644 src/snapshot.py diff --git a/.gitignore b/.gitignore index 58f2dae..0b8e60a 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,9 @@ ENV/ # generated models /src/models +# snapshots +/src/snapshots + # build files debian/howdy.substvars debian/files diff --git a/src/compare.py b/src/compare.py index 8ddd0c7..658f94f 100644 --- a/src/compare.py +++ b/src/compare.py @@ -16,6 +16,8 @@ import json import configparser import cv2 import dlib +import datetime +import snapshot import numpy as np import _thread as thread from recorders.video_capture import VideoCapture @@ -48,6 +50,18 @@ def init_detector(lock): lock.release() +def make_snapshot(type): + """Generate snapshot after detection""" + snapshot.generate(snapframes, [ + type + " LOGIN", + "Date: " + datetime.datetime.utcnow().strftime("%Y/%m/%d %H:%M:%S UTC"), + "Scan time: " + str(round(time.time() - timings["fr"], 2)) + "s", + "Frames: " + str(frames) + " (" + str(round(frames / (time.time() - timings["fr"]), 2)) + "FPS)", + "Hostname: " + os.uname().nodename, + "Best certainty value: " + str(round(lowest_certainty * 10, 1)) + ]) + + # Make sure we were given an username to tast against if len(sys.argv) < 2: sys.exit(12) @@ -67,7 +81,11 @@ black_tries = 0 dark_tries = 0 # Total amount of frames captured frames = 0 -# face recognition/detection instances +# Captured frames for snapshot capture +snapframes = [] +# Tracks the lowest certainty value in the loop +lowest_certainty = 10 +# Face recognition/detection instances face_detector = None pose_predictor = None face_encoder = None @@ -95,6 +113,8 @@ timeout = config.getint("video", "timeout", 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) +capture_failed = config.getboolean("snapshots", "capture_failed", fallback=False) +capture_successful = config.getboolean("snapshots", "capture_successful", fallback=False) # Save the time needed to start the script timings["in"] = time.time() - timings["st"] @@ -150,7 +170,11 @@ while True: # Stop if we've exceded the time limit if time.time() - timings["fr"] > timeout: - if (dark_tries == valid_frames): + # Create a timeout snapshot if enabled + if capture_failed: + make_snapshot("FAILED") + + 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)) sys.exit(13) @@ -159,9 +183,14 @@ while True: # Grab a single frame of video frame, gsframe = video_capture.read_frame() - gsframe = clahe.apply(gsframe) + # If snapshots have been turned on + if capture_failed or capture_successful: + # Start capturing frames for the snapshot + if len(snapframes) < 3: + snapframes.append(frame) + # Create a histogram of the image with 8 values hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) # All values combined for percentage calculation @@ -210,10 +239,14 @@ while True: match_index = np.argmin(matches) match = matches[match_index] + # Update certainty if we have a new low + if lowest_certainty > match: + lowest_certainty = match + # 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"] + timings["fl"] = time.time() - timings["fr"] # If set to true in the config, print debug text if end_report: @@ -227,7 +260,7 @@ while True: 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("Searching for known face", "fl") print_timing("Total time", "tt") print("\nResolution") @@ -238,13 +271,17 @@ while True: 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("\nFrames searched: %d (%.2f fps)" % (frames, frames / timings["fl"])) 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"])) + # Make snapshot if enabled + if capture_successful: + make_snapshot("SUCCESSFUL") + # End peacefully sys.exit(0) diff --git a/src/config.ini b/src/config.ini index a6a0fa3..7ff7695 100644 --- a/src/config.ini +++ b/src/config.ini @@ -30,6 +30,7 @@ use_cnn = false [video] # The certainty of the detected face belonging to the user of the account # On a scale from 1 to 10, values above 5 are not recommended +# Lower is better certainty = 3.5 # The number of seconds to search before timing out @@ -37,7 +38,7 @@ timeout = 4 # The path of the device to capture frames from # Should be set automatically by an installer if your distro has one -device_path = "/dev/video1" +device_path = none # Scale down the video feed to this maximum height # Speeds up face recognition but can make it less precise @@ -73,6 +74,14 @@ force_mjpeg = false # OPENCV only. exposure = -1 +[snapshots] +# Capture snapshots of failed login attempts and save them to disk with metadata +# Snapshots are saved to the "snapshots" folder +capture_failed = true + +# Do the same as the option above but for successful attemtps +capture_successful = true + [debug] # Show a short but detailed diagnostic report in console # Enabling this can cause some UI apps to fail, only enable it to debug diff --git a/src/logo.png b/src/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f9fe9ca74129bfcaa4f40a6cc6068f5d5f60c565 GIT binary patch literal 3068 zcmVl-IyyQrF){i1`ThOJUl$Mwzi(0 zo?cyDi;Ig!Mn*tDK)=7gmX?-QR#rqrM8UzqQc_Y$Nl89FKGW0FhK7c2Zf=2rfoN!G zk&%#UYHE{{lYD%9`}_Nvnwnu@Vcgu@ySux&xw&?BcJ%c0TU%RYWo21eSx-+-&(F_) ze}8#-d31Di+1lC0#>S40j_>d9s;a7EV`Jy%=iuPr$;rvX!^76r*6Zu*_V)IJgM)E# zapdIW%gf7%iHTV@43z)?3fW0SK~!ko&6x>zvN{lkL)bSFL;*$WhJyRPw_2;V{r`XH zvP7XmYkPao>3zTR6{8dK%w#eN3hoQT3Cm(h@IMnanzsT?f|kXZW|;tiNUmOMGJ@o) z{c#o`ae?`~RsTy*h&}JRisHI$k22TbJ#Be)hyiS<3tPQ*wySeh5%|6p+7Eqe#bur| z;O|6CO%nHo0n3$}Ba`C{yV=w0irgF*NMyv#C1HpXCQw-`g1-~-P)4t^jL6~POMJrb zo0mq&N>rZhSUXetTWAhtu*zG}+jw&J7pw9A4!S50?<`rBVH417hK!pOvhm7OC>Ap; zc4=nV50=&kih^%O;(FRP5Y*OhiUtc@g z+6>cdz7%5lW>5FA7-h6+9e|10^VF3YeQsFch~2|H2GBZ`GPlsgy-5+zx6s8Y6l0-Q zDQApqXG;hnMR|WZYCzC*C_9Ro3>_uvH=_qN0C;#2{iH4YIpYrSkiS}S4{#T@h!ui1 zd)YhCtTKJ$Y0ME{g zz;CxwZ~6q`3t#j-9nkvKQ9Ww3gF(sSO#2BmIV97dDFi*+(91=Z^sA2jMh_Z(aXQcs z*97cuX!>b%b>Yd;Z~#hlCy%o%n=KCY9Xv{`_sIkOdPyT=38ztcz>SLVynkEWPmfx> z`2{rBabB;vkXt&he$w#p*qaR8y0ijyOEZ5!pUU60j(Y#HMV|^M?W#n)D<&7g-;v$nvW8-83xcD1RZA6-uoPX zhG<5HJE)~|YzL;MCbA@;pLIaQXMZQ?8F;Vaz6JL>So_5%uc z=qTVi$6S>j%&sozAOYQ?2NmB|fG^b`@=#)f&V9L#OuiTCRtTTd1*VRF4n5z%R=neg z3dUbR@8T=-3?GR64p9K*ILIZTCmTl$(&7nf7GO2_+74H(Tm?Ud4!kX&1SNx@u>^FJ z8WbflWdap0ytj(xs0x(4r_YOv<{%{u~-%bBt6B zpv4&UdjO4z^o#FNe0&KHX8JJk`&JyP4lr;2tAPuzPytD3d+D#|PLMqqGsh zPoX1aMG^O*ciKhWZU-XhCoA3&e+ZyK3l0{(4>CM>PMIDo=2@>PYLo&^PGPunkPXUN zQpd}_Qsv2i4-N9YoDc0d@GAOKXu*e`JV6onE<4D0muweKv))C9ODhff0iuHp_eML^ zl0)yC@;CJHGl@U(CjjVCc5y=3vu6}=6;A9w7&q&4rH*j^f*8b z5!E&{Rk=E|X7W+sRQzX=2I9( zR~j>3>JVwrkI~x0pAj??_Q*dsP4Hu3)t-|1Y;&1(pcXgg;j8y{SaUaqc-bbv;svhj zj#I;Kq0u(Fh@jtYhR-o?z1oER2{K{J`#%2Zjz|j5CYB%O6TKp?J2ta~s{%?euL4WZ zKYJ#j%S)%jC7~l_=EAp$)-tFipv(GDLan?}>qBDhAH+If0?IYKyCwLt{Ex&s{-MutM6C6v*vh4l>GSm0;T z@s3k=FwiS+#w;)aQZXMY5Gs+&-Yn3NhR$XwEaHV2*I?l_>!Z5&f1JTT)`O8d`q$9B4gP0}pfNUYMFGnc@hfya zr3NB_A1H5LJO2_IJ0WPSF3M63k!Dmua3|}8R+zE;I59mnS#JiK`~bS&2w7QvU;ZWZ8GR|b_N%?UC4w3td7Ol> z0AV9RMph~;=?e${0vZhM-$r`0wZcgITYmO2xLvmGUxr4wV@Sm$^ejrtpF;x-{8FYg zxib|!o`kOb2Kx6ZFrzzPsyD!WXe-xW7;JQ;!SdCGl@3i9!wl-w2IS;CA6|UHho}oO zFi-xW^?7D1HYVKZxvBf%9r@n(E%|W zvLyr`WF$2~Fc*n0XM!)MK+it37u~9NkD@Lh)A55;$39Pw3WJ5vC0+Gm$_dTOPzYKK zp$SIB{iyO*GO1e z4AdxPqQHoCb^(t7trm_1cELnlA$ZvM97Wd`O``%dgh{h&Sdk8u1iMHeu|TW^XJ&PrxvaWwKfj0_C?iT(kb>N??W!Nnc`0000< KMNUMnLSTY4lg-Nj literal 0 HcmV?d00001 diff --git a/src/snapshot.py b/src/snapshot.py new file mode 100644 index 0000000..20f1f54 --- /dev/null +++ b/src/snapshot.py @@ -0,0 +1,59 @@ +# Create and save snapshots of auth attemtps + +# Import modules +import cv2 +import os +import datetime +import numpy as np + + +def generate(frames, text_lines): + """Generate a shapshot from given frames""" + + # Don't execute if no frames were given + if len(frames) == 0: + return + + # Get the path to the containing folder + abpath = os.path.dirname(os.path.abspath(__file__)) + # Get frame dimentions + frame_height, frame_width, cc = frames[0].shape + # Spread the given frames out horizontally + snap = np.concatenate(frames, axis=1) + + # Create colors + pad_color = [44, 44, 44] + text_color = [255, 255, 255] + + # Add a gray square at the bottom of the image + snap = cv2.copyMakeBorder(snap, 0, len(text_lines) * 20 + 40, 0, 0, cv2.BORDER_CONSTANT, value=pad_color) + + # Add the Howdy logo if there's space to do so + if len(frames) > 1: + # Load the logo from file + logo = cv2.imread(abpath + "/logo.png") + # Calculate the position of the logo + logo_y = frame_height + 20 + logo_x = frame_width * len(frames) - 210 + + # Overlay the logo on top of the image + snap[logo_y:logo_y+57, logo_x:logo_x+180] = logo + + # Go through each line + line_number = 0 + for line in text_lines: + # Calculate how far the line should be from the top + padding_top = frame_height + 30 + (line_number * 20) + # Print the line onto the image + cv2.putText(snap, line, (30, padding_top), cv2.FONT_HERSHEY_SIMPLEX, .4, text_color, 0, cv2.LINE_AA) + + line_number += 1 + + # Made sure a snapshot folder exist + if not os.path.exists(abpath + "/snapshots"): + os.makedirs(abpath + "/snapshots") + + # Generate a filename based on the current time + filename = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%S.jpg") + # Write the image to that file + cv2.imwrite(abpath + "/snapshots/" + filename, snap) From fff9f3a4abf79c70d9319bb8019ed16dceaef613 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 21 Jun 2020 22:17:48 +0200 Subject: [PATCH 20/26] Added snapshot command --- src/cli.py | 6 ++++-- src/cli/snap.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ src/snapshot.py | 3 +++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/cli/snap.py diff --git a/src/cli.py b/src/cli.py index 5906f4d..78e251a 100755 --- a/src/cli.py +++ b/src/cli.py @@ -34,9 +34,9 @@ parser = argparse.ArgumentParser(description="Command line interface for Howdy f # Add an argument for the command parser.add_argument("command", - help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.", + help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove, test or snapshot.", metavar="command", - choices=["add", "clear", "config", "disable", "list", "remove", "test"]) + choices=["add", "clear", "config", "disable", "list", "remove", "test", "snapshot"]) # Add an argument for the extra arguments of diable and remove parser.add_argument("argument", @@ -99,3 +99,5 @@ elif args.command == "remove": import cli.remove elif args.command == "test": import cli.test +elif args.command == "snapshot": + import cli.snap diff --git a/src/cli/snap.py b/src/cli/snap.py new file mode 100644 index 0000000..7dce8f5 --- /dev/null +++ b/src/cli/snap.py @@ -0,0 +1,51 @@ +# Create a snapshot + +# Import required modules +import os +import configparser +import datetime +import snapshot +from recorders.video_capture import VideoCapture + +# Get the absolute path to the current directory +path = os.path.abspath(__file__ + "/..") + +# Read the config +config = configparser.ConfigParser() +config.read(path + "/../config.ini") + +# Start video capture +video_capture = VideoCapture(config) + +# Read a frame to activate emitters +video_capture.read_frame() + +# Read exposure and dark_thresholds from config to use in the main loop +exposure = config.getint("video", "exposure", fallback=-1) +dark_threshold = config.getfloat("video", "dark_threshold") + +# COllection of recorded frames +frames = [] + +while True: + # Grab a single frame of video + frame, gsframe = video_capture.read_frame() + + # Add the frame to the list + frames.append(frame) + + # Stop the loop if we have 4 frames + if len(frames) >= 4: + break + +# Generate a snapshot image from the frames +file = snapshot.generate(frames, [ + "GENERATED SNAPSHOT", + "Date: " + datetime.datetime.utcnow().strftime("%Y/%m/%d %H:%M:%S UTC"), + "Dark threshold config: " + str(config.getfloat("video", "dark_threshold", fallback=50.0)), + "Certainty config: " + str(config.getfloat("video", "certainty", fallback=3.5)) +]) + +# Show the file location in console +print("Generated snapshot saved as") +print(file) diff --git a/src/snapshot.py b/src/snapshot.py index 20f1f54..a2d8d8c 100644 --- a/src/snapshot.py +++ b/src/snapshot.py @@ -57,3 +57,6 @@ def generate(frames, text_lines): filename = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%S.jpg") # Write the image to that file cv2.imwrite(abpath + "/snapshots/" + filename, snap) + + # Return the saved file location + return abpath + "/snapshots/" + filename From 80433c70ff19fdeb5112c30732a124ffd5eb7f67 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Sun, 21 Jun 2020 22:22:23 +0200 Subject: [PATCH 21/26] Fixed missing release method in video capture class --- src/recorders/video_capture.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/recorders/video_capture.py b/src/recorders/video_capture.py index 6c852c4..6cf03d5 100644 --- a/src/recorders/video_capture.py +++ b/src/recorders/video_capture.py @@ -54,6 +54,13 @@ class VideoCapture: if self is not None: self.internal.release() + def release(self): + """ + Release cameras + """ + if self is not None: + self.internal.release() + def read_frame(self): """ Reads a frame, returns the frame and an attempted grayscale conversion of From f62de10972440d843a6c2822dea06eddb62bea94 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 22 Jun 2020 13:58:17 +0200 Subject: [PATCH 22/26] Added version command --- src/cli.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cli.py b/src/cli.py index 78e251a..66793ce 100755 --- a/src/cli.py +++ b/src/cli.py @@ -34,9 +34,9 @@ parser = argparse.ArgumentParser(description="Command line interface for Howdy f # Add an argument for the command parser.add_argument("command", - help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove, test or snapshot.", + help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove, snapshot, test or version.", metavar="command", - choices=["add", "clear", "config", "disable", "list", "remove", "test", "snapshot"]) + choices=["add", "clear", "config", "disable", "list", "remove", "snapshot", "test", "version"]) # Add an argument for the extra arguments of diable and remove parser.add_argument("argument", @@ -97,7 +97,9 @@ elif args.command == "list": import cli.list elif args.command == "remove": import cli.remove -elif args.command == "test": - import cli.test elif args.command == "snapshot": import cli.snap +elif args.command == "test": + import cli.test +else: + print("Howdy 2.5.1") From 23810ef3c9b7448c458cbf7d5b324a1e0afdb597 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 22 Jun 2020 17:58:09 +0200 Subject: [PATCH 23/26] Added certainty question, fixed streamer dependency --- debian/control | 4 +- debian/postinst | 6 +- debian/preinst | 129 +++++++++++++++++++++++++-------------- debian/source/options | 1 + src/cli.py | 50 ++++++++------- src/cli/list.py | 2 +- src/dlib-data/install.sh | 2 +- 7 files changed, 121 insertions(+), 73 deletions(-) diff --git a/debian/control b/debian/control index efb16a1..0d2ae80 100644 --- a/debian/control +++ b/debian/control @@ -9,8 +9,8 @@ Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: all -Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, cmake, fswebcam -Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev +Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, libopencv-dev, cmake +Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev, streamer Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. Use your built-in IR emitters and camera in combination with face recognition diff --git a/debian/postinst b/debian/postinst index 4ede83c..40fc93e 100755 --- a/debian/postinst +++ b/debian/postinst @@ -196,10 +196,14 @@ handleStatus(subprocess.call(["pip3", "install", "--no-cache-dir", "opencv-pytho log("Configuring howdy") +campath = picked.split(";")[0] +cert = picked.split(";")[1] + # Manually change the camera id to the one picked for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace=1): - line = line.replace("device_path = none", "device_path = " + picked) + line = line.replace("device_path = none", "device_path = " + campath) line = line.replace("use_cnn = false", "use_cnn = " + str(cuda_used).lower()) + line = line.replace("certainty = 3.5", "certainty = " + cert) print(line, end="") diff --git a/debian/preinst b/debian/preinst index 8430d4a..9bb5aad 100755 --- a/debian/preinst +++ b/debian/preinst @@ -7,6 +7,7 @@ def col(id): if id == 1: return "\033[32m" if id == 2: return "\033[33m" if id == 3: return "\033[31m" + if id == 4: return "\033[1m" return "\033[0m" @@ -38,7 +39,6 @@ if "install" not in sys.argv: # The default picked video device id picked = "none" -print(col(1) + "Starting IR camera check...\n" + col(0)) # If prompting has been disabled, skip camera check if "HOWDY_NO_PROMPT" in os.environ: @@ -46,67 +46,104 @@ if "HOWDY_NO_PROMPT" in os.environ: # Write the default device to disk and exit with open("/tmp/howdy_picked_device", "w") as out_file: - out_file.write("none") + out_file.write("none;3.5") sys.exit(0) -# Get all devices -devices = os.listdir("/dev/v4l/by-path") +fscheck = subprocess.call(["which", "streamer"], stdout=subprocess.PIPE) -# Loop though all devices -for dev in devices: - time.sleep(.5) +if fscheck == 1: + print(col(2) + "\nWARNING: Could not automatically find the right webcam, manual configuration after installation required\n" + col(0)) +else: + print(col(1) + "Starting IR camera check...\n" + col(0)) - # The full path to the device is the default name - device_name = "/dev/v4l/by-path/" + dev - # Get the udevadm details to try to get a better name - udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8") + # Get all devices + devices = os.listdir("/dev/v4l/by-path") - # Loop though udevadm to search for a better name - for line in udevadm.split("\n"): - # Match it and encase it in quotes - re_name = re.search('product.*=(.*)$', line, re.IGNORECASE) - if re_name: - device_name = '"' + re_name.group(1) + '"' + # Loop though all devices + for dev in devices: + time.sleep(.5) - # Show what device we're using - print("Trying " + device_name) + # The full path to the device is the default name + device_name = "/dev/v4l/by-path/" + dev + # Get the udevadm details to try to get a better name + udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8") - # Let fswebcam keep the camera open in the background - sub = subprocess.Popen( - "fswebcam -l 1 -d /dev/v4l/by-path/%s" % dev, - shell=True, - preexec_fn=os.setsid, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE) + # Loop though udevadm to search for a better name + for line in udevadm.split("\n"): + # Match it and encase it in quotes + re_name = re.search('product.*=(.*)$', line, re.IGNORECASE) + if re_name: + device_name = '"' + re_name.group(1) + '"' - try: - # Ask the user if this is the right one - print(col(2) + "One of your cameras should now be on." + col(0)) - ans = input("Did your IR emitters turn on? [y/N]: ") - except KeyboardInterrupt: - # Kill fswebcam if the user aborts + # Show what device we're using + print("Trying " + device_name) + + # Let fswebcam keep the camera open in the background + sub = subprocess.Popen( + ["streamer -t 1:0:0 -c /dev/v4l/by-path/" + dev + " -b 16 -f rgb24 -o /dev/null 1>/dev/null 2>/dev/null"], + shell=True, + preexec_fn=os.setsid, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) + + try: + # Ask the user if this is the right one + print(col(2) + "One of your cameras should now be on." + col(0)) + ans = input("Did your IR emitters turn on? [y/N]: ") + except KeyboardInterrupt: + # Kill fswebcam if the user aborts + os.killpg(os.getpgid(sub.pid), signal.SIGTERM) + raise + + # The user has answered, kill fswebcam os.killpg(os.getpgid(sub.pid), signal.SIGTERM) - raise - # The user has answered, kill fswebcam - os.killpg(os.getpgid(sub.pid), signal.SIGTERM) + # Set this camera as picked if the answer was yes, go to the next one if no + if ans.lower().strip() == "y" or ans.lower().strip() == "yes": + picked = dev + break + else: + print("Interpreting as a " + col(3) + "\"NO\"\n" + col(0)) - # Set this camera as picked if the answer was yes, go to the next one if no - if ans.lower().strip() == "y" or ans.lower().strip() == "yes": - picked = dev - break - else: - print("Interpreting as a " + col(3) + "\"NO\"\n" + col(0)) + # Abort if no camera was picked + if picked == "none": + print(col(3) + "No suitable IR camera found, aborting install." + col(0)) + sys.exit(23) -# Abort if no camera was picked -if picked == "none": - print(col(3) + "No suitable IR camera found, aborting install." + col(0)) - sys.exit(23) +cert = 3.5 + +# Give time to read +time.sleep(.5) + +print(col(1) + "\nStarting certainty auto config..." + col(0)) + +# Give more time to read +time.sleep(.5) + +print("\n\nAfter detection, Howdy knows how certain it is that the match is correct.") +print("How certain Howdy needs to be before authenticating you can be customized.") + +print(col(4) + "\nF: Fast." + col(0)) +print("Allows more fuzzy matches, but speeds up the scanning process greatly.") +print(col(4) + "\nB: Balanced." + col(0)) +print("Still relatively quick detection, but might not log you in when further away.") +print(col(4) + "\nS: Secure." + col(0)) +print("The safest option, but will take much longer to authenticate you.") + +print("\nYou can always change this setting in the config.") +prof = input("What profile would you like to use? [f/b/s]: ") + +if prof.lower().strip() == "f" or prof.lower().strip() == "fast": + cert = 1.5 +elif prof.lower().strip() == "b" or prof.lower().strip() == "balanced": + cert = 2.8 +elif prof.lower().strip() == "s" or prof.lower().strip() == "secure": + cert = 4 # Write the result to disk so postinst can have a look at it with open("/tmp/howdy_picked_device", "w") as out_file: - out_file.write("/dev/v4l/by-path/" + picked) + out_file.write("/dev/v4l/by-path/" + picked + ";" + str(cert)) # Add a line break print("") diff --git a/debian/source/options b/debian/source/options index 1216d00..994eed9 100644 --- a/debian/source/options +++ b/debian/source/options @@ -2,6 +2,7 @@ tar-ignore = ".git" tar-ignore = ".gitignore" tar-ignore = ".github" tar-ignore = "models" +tar-ignore = "snapshots" tar-ignore = "tests" tar-ignore = "README.md" tar-ignore = ".travis.yml" diff --git a/src/cli.py b/src/cli.py index 66793ce..29dadac 100755 --- a/src/cli.py +++ b/src/cli.py @@ -26,38 +26,44 @@ if user == "root" or user is None: user = env_user # Basic command setup -parser = argparse.ArgumentParser(description="Command line interface for Howdy face authentication.", - formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False, - prog="howdy", - epilog="For support please visit\nhttps://github.com/boltgolt/howdy") +parser = argparse.ArgumentParser( + description="Command line interface for Howdy face authentication.", + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + prog="howdy", + epilog="For support please visit\nhttps://github.com/boltgolt/howdy") # Add an argument for the command -parser.add_argument("command", - help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove, snapshot, test or version.", - metavar="command", - choices=["add", "clear", "config", "disable", "list", "remove", "snapshot", "test", "version"]) +parser.add_argument( + "command", + help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove, snapshot, test or version.", + metavar="command", + choices=["add", "clear", "config", "disable", "list", "remove", "snapshot", "test", "version"]) # Add an argument for the extra arguments of diable and remove -parser.add_argument("argument", - help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.", - nargs="?") +parser.add_argument( + "argument", + help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.", + nargs="?") # Add the user flag -parser.add_argument("-U", "--user", - default=user, - help="Set the user account to use.") +parser.add_argument( + "-U", "--user", + default=user, + help="Set the user account to use.") # Add the -y flag -parser.add_argument("-y", - help="Skip all questions.", - action="store_true") +parser.add_argument( + "-y", + help="Skip all questions.", + action="store_true") # Overwrite the default help message so we can use a uppercase S -parser.add_argument("-h", "--help", - action="help", - default=argparse.SUPPRESS, - help="Show this help message and exit.") +parser.add_argument( + "-h", "--help", + action="help", + default=argparse.SUPPRESS, + help="Show this help message and exit.") # If we only have 1 argument we print the help text if len(sys.argv) < 2: diff --git a/src/cli/list.py b/src/cli/list.py index 2cc9b9c..fe05d6e 100644 --- a/src/cli/list.py +++ b/src/cli/list.py @@ -25,7 +25,7 @@ try: encodings = json.load(open(enc_file)) except FileNotFoundError: print("No face model known for the user " + user + ", please run:") - print("\n\thowdy " + user + " add\n") + print("\n\tsudo howdy -U " + user + " add\n") sys.exit(1) # Print a header diff --git a/src/dlib-data/install.sh b/src/dlib-data/install.sh index 7fcabe6..5698f01 100755 --- a/src/dlib-data/install.sh +++ b/src/dlib-data/install.sh @@ -22,4 +22,4 @@ fi # Uncompress the data files and delete the original archive echo "Unpacking..." -bzip2 -d *.bz2 +bzip2 -d -f *.bz2 From 2890457fa585d0fde4636201ee53f54bec744b98 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 22 Jun 2020 18:00:33 +0200 Subject: [PATCH 24/26] Fix editor prio as per #333 --- src/cli/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/config.py b/src/cli/config.py index 4f6caca..764833d 100644 --- a/src/cli/config.py +++ b/src/cli/config.py @@ -11,10 +11,10 @@ print("Opening config.ini in the default editor") editor = "/bin/nano" # Use the user preferred editor if available -if os.path.isfile("/etc/alternatives/editor"): - editor = "/etc/alternatives/editor" -elif "EDITOR" in os.environ: +if "EDITOR" in os.environ: editor = os.environ["EDITOR"] +elif os.path.isfile("/etc/alternatives/editor"): + editor = "/etc/alternatives/editor" # Open the editor as a subprocess and fork it subprocess.call([editor, os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"]) From 414c3a0e75decdaa846476dfb47f173b49117cf0 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 22 Jun 2020 18:37:08 +0200 Subject: [PATCH 25/26] Ready for 2.6.0 --- debian/changelog | 24 ++++++++++++++++++++++++ src/cli.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 338f9d4..00d388b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,27 @@ +howdy (2.6.0) xenial; urgency=medium + +* Added new options to capture a snapshot of failed or even successful logins +* Added command that creates a new snapshot and saves it +* Added version command +* Added question to automatically set certainty value on installation +* Added automatic logging to system-wide auth.log +* Added clearer feedback when login is rejected due to dark frames (thanks @andrewmv!) +* Refactored video capture logic (thanks @AnthonyWharton!) +* Reordered the editor priorities for the config command +* Fixed gstreamer warnings showing up in console (thanks @ajnart!) +* Fixed issue where add command would never end +* Fixed test command overlay not being in color (thanks @PetePriority!) +* Fixed typo preventing timeout config option from working (thanks @Ajayneethikannan!) +* Fixed old numpy installation failure (thanks @rushabh-v!) +* Fixed issue where no PAM response would be returned +* Fixed CLAHE not being applied equally to all video commands (thanks @PetePriority!) +* Fixed an incorrect suggested command (thanks @TheButlah!) +* Fixed missing release method in video capture class +* Removed deprecated dlib flags (thanks @rhysperry111!) +* Removed streamer as a required dependency + + -- boltgolt Mon, 22 Jun 2020 16:11:46 +0200 + howdy (2.5.1) xenial; urgency=medium * Removed dismiss_lockscreen as it could lock users out of their system (thanks @ujjwalbe, @ju916 and many others!) diff --git a/src/cli.py b/src/cli.py index 29dadac..863ac25 100755 --- a/src/cli.py +++ b/src/cli.py @@ -108,4 +108,4 @@ elif args.command == "snapshot": elif args.command == "test": import cli.test else: - print("Howdy 2.5.1") + print("Howdy 2.6.0") From c3b11d14e65ce8d37aef9d80046a19f1d9733f3e Mon Sep 17 00:00:00 2001 From: boltgolt Date: Mon, 22 Jun 2020 18:49:04 +0200 Subject: [PATCH 26/26] Gotta fix those typos --- src/config.ini | 2 +- src/snapshot.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.ini b/src/config.ini index 7ff7695..4b694bf 100644 --- a/src/config.ini +++ b/src/config.ini @@ -79,7 +79,7 @@ exposure = -1 # Snapshots are saved to the "snapshots" folder capture_failed = true -# Do the same as the option above but for successful attemtps +# Do the same as the option above but for successful attempts capture_successful = true [debug] diff --git a/src/snapshot.py b/src/snapshot.py index a2d8d8c..c853ffa 100644 --- a/src/snapshot.py +++ b/src/snapshot.py @@ -1,4 +1,4 @@ -# Create and save snapshots of auth attemtps +# Create and save snapshots of auth attempts # Import modules import cv2 @@ -16,7 +16,7 @@ def generate(frames, text_lines): # Get the path to the containing folder abpath = os.path.dirname(os.path.abspath(__file__)) - # Get frame dimentions + # Get frame dimensions frame_height, frame_width, cc = frames[0].shape # Spread the given frames out horizontally snap = np.concatenate(frames, axis=1)