diff --git a/src/cli/add.py b/src/cli/add.py index 10c7330..b2f6e96 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -77,14 +77,15 @@ insert_model = { "data": [] } +# Check if the user explicitly set ffmpeg as recorder if config.get("video", "recording_plugin") == "ffmpeg": from ffmpeg_reader import ffmpeg_reader # Set the capture source for ffmpeg - video_capture = ffmpeg_reader(config.get("video", "device_name"), config.get("video", "device_format"), 10) + video_capture = ffmpeg_reader(config.get("video", "device_path"), config.get("video", "device_format")) else: - # Start video capture on the IR camera + # Start video capture on the IR camera through OpenCV video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true diff --git a/src/cli/test.py b/src/cli/test.py index cceb1a4..3faa5b6 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -3,6 +3,7 @@ # Import required modules import configparser import os +import sys import time import cv2 import face_recognition @@ -14,6 +15,11 @@ path = os.path.dirname(os.path.abspath(__file__)) config = configparser.ConfigParser() config.read(path + "/../config.ini") +if config.get("video", "recording_plugin") == "ffmpeg": + print("Howdy has been configured to use ffmpeg as recorder, which doesn't support the test command yet") + print("Aborting") + sys.exit(12) + # Start capturing from the configured webcam video_capture = cv2.VideoCapture(config.get("video", "device_path")) diff --git a/src/compare.py b/src/compare.py index 98df282..0b7c283 100644 --- a/src/compare.py +++ b/src/compare.py @@ -28,7 +28,7 @@ try: if not isinstance(sys.argv[1], str): sys.exit(1) except IndexError: - sys.exit(1) + sys.exit(12) # The username of the authenticating user user = sys.argv[1] @@ -56,14 +56,15 @@ for model in models: # Add the time needed to start the script timings.append(time.time()) +# Check if the user explicitly set ffmpeg as recorder if config.get("video", "recording_plugin") == "ffmpeg": from ffmpeg_reader import ffmpeg_reader # Set the capture source for ffmpeg - video_capture = ffmpeg_reader(config.get("video", "device_name"), config.get("video", "device_format"), 10) + video_capture = ffmpeg_reader(config.get("video", "device_path"), config.get("video", "device_format")) else: - # Start video capture on the IR camera + # Start video capture on the IR camera through OpenCV video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true diff --git a/src/config.ini b/src/config.ini index df2907f..4500e95 100644 --- a/src/config.ini +++ b/src/config.ini @@ -22,21 +22,6 @@ dismiss_lockscreen = false disabled = false [video] -# -# opencv or ffmpeg. Choose ffmpeg if your IR device is a grayscale only. -# Run this: v4l2-ctl -d /dev/video2 --list-formats -recording_plugin = opencv - -# -# format to use for ffmpeg. Could be vfwcap or v4l2 or some other thing... -# -device_format = v4l2 - -# -# device name if using ffmpeg. ex: /dev/video2 -# -device_name = none - # 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 certainty = 3.5 @@ -45,17 +30,13 @@ certainty = 3.5 timeout = 4 # The path of the device to capture frames from -# Should be set automatically by the installer +# Should be set automatically by an installer if your distro has one device_path = none # Scale down the video feed to this maximum height # Speeds up face recognition but can make it less precise max_height = 320 -# Force the use of Motion JPEG when decoding frames, fixes issues with -# YUYV raw frame decoding -force_mjpeg = false - # Set the camera input profile to this width and height # The largest profile will be used if set to -1 # Automatically ignored if not a valid profile @@ -68,6 +49,19 @@ frame_height = -1 # The lower this setting is, the more dark frames are ignored dark_threshold = 50 +# The recorder to use. Can be either opencv or ffmpeg. +# Switching from the default opencv to ffmpeg can help with grayscale issues. +recording_plugin = opencv + +# Video format used by ffmpeg. Options include vfwcap or v4l2. +# FFMPEG only. +device_format = v4l2 + +# Force the use of Motion JPEG when decoding frames, fixes issues with YUYV +# raw frame decoding. +# OPENCV only. +force_mjpeg = false + [debug] # Show a short but detailed diagnostic report in console end_report = false diff --git a/src/ffmpeg_reader.py b/src/ffmpeg_reader.py index bf18b44..658b447 100755 --- a/src/ffmpeg_reader.py +++ b/src/ffmpeg_reader.py @@ -1,18 +1,26 @@ -import ffmpeg +# Class that simulates the functionality of opencv so howdy can use ffmpeg seamlessly + +# Import required modules import numpy +import sys +import re +from subprocess import Popen, PIPE from cv2 import CAP_PROP_FRAME_WIDTH from cv2 import CAP_PROP_FRAME_HEIGHT from cv2 import CAP_PROP_FOURCC -import sys -from subprocess import Popen, PIPE -import re + +try: + import ffmpeg +except ImportError: + print("Missing ffmpeg module, please run:") + print(" pip3 install ffmpeg-python\n") + sys.exit(12) class ffmpeg_reader: """ This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. """ - # Init - def __init__(self, device_name, device_format, numframes): - self.device_name = device_name + def __init__(self, device_path, device_format, numframes=10): + self.device_path = device_path self.device_format = device_format self.numframes = numframes self.video = () @@ -20,7 +28,6 @@ class ffmpeg_reader: self.height = 0 self.width = 0 self.init_camera = True - self.force_jpeg = 0 def set(self, prop, setting): """ Setter method for height and width """ @@ -28,9 +35,6 @@ class ffmpeg_reader: self.width = setting elif prop == CAP_PROP_FRAME_HEIGHT: self.height = setting - elif prop == CAP_PROP_FOURCC: - self.force_jpeg = setting - def get(self, prop): """ Getter method for height and width """ @@ -38,34 +42,32 @@ class ffmpeg_reader: return self.width elif prop == CAP_PROP_FRAME_HEIGHT: return self.height - elif prop == CAP_PROP_FOURCC: - return self.force_jpeg - + def probe(self): """ Probe the video device to get height and width info """ - # Running this command on ffmpeg unfortunatly returns with an exit code of 1, which is silly. + # Running this command on ffmpeg unfortunatly returns with an exit code of 1, which is silly. # Returns an error code of 1 and this text: "/dev/video2: Immediate exit requested" - args = ["ffmpeg", "-f", self.device_format, "-list_formats", "all", "-i", self.device_name] + args = ["ffmpeg", "-f", self.device_format, "-list_formats", "all", "-i", self.device_path] process = Popen(args, stdout=PIPE, stderr=PIPE) out, err = process.communicate() return_code = process.poll() - # worst case scenario, err will equal en empty byte string, b'', so probe will get set to [] here. + # Worst case scenario, err will equal en empty byte string, b'', so probe will get set to [] here. regex = re.compile('\s\d{3,4}x\d{3,4}') probe = regex.findall(str(err.decode("utf-8"))) - if not return_code == 1 or len(probe) < 1: + if not return_code == 1 or len(probe) < 1: # Could not determine the resolution from ffmpeg call. Reverting to ffmpeg.probe() - probe = ffmpeg.probe(self.device_name) + probe = ffmpeg.probe(self.device_path) height = probe['streams'][0]['height'] width = probe['streams'][0]['width'] else: (height, width) = [x.strip() for x in probe[0].split('x')] + # Set height and width from probe if they haven't been set already if height.isdigit() and self.get(CAP_PROP_FRAME_HEIGHT) == 0: self.set(CAP_PROP_FRAME_HEIGHT, int(height)) - if width.isdigit() and self.get(CAP_PROP_FRAME_WIDTH) == 0: self.set(CAP_PROP_FRAME_WIDTH, int(width)) @@ -80,9 +82,10 @@ class ffmpeg_reader: # Ensure num_frames_read is reset to 0 self.num_frames_read = 0 + # Record a predetermined amount of frames from the camera stream, ret = ( ffmpeg - .input(self.device_name, format=self.device_format) + .input(self.device_path, format=self.device_format) .output('pipe:', format='rawvideo', pix_fmt='rgb24', vframes=numframes) .run(capture_stdout=True, quiet=True) ) diff --git a/src/pam.py b/src/pam.py index 6de8f19..2e0dbfa 100644 --- a/src/pam.py +++ b/src/pam.py @@ -33,11 +33,14 @@ def doAuth(pamh): pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "No face model known")) return pamh.PAM_USER_UNKNOWN # Status 11 means we exceded the maximum retry count - if status == 11: + elif status == 11: pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection timeout reached")) return pamh.PAM_AUTH_ERR + # Status 12 means we aborted + elif status == 12: + return pamh.PAM_AUTH_ERR # Status 0 is a successful exit - if status == 0: + elif status == 0: # Show the success message if it isn't suppressed if not config.getboolean("core", "no_confirmation"): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user()))