From 0c4992ffaf51e281a7c11e2dfd66aa71de85fec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Aceda=C5=84ski?= Date: Mon, 11 Mar 2019 00:21:54 +0100 Subject: [PATCH 1/9] Manual exposure support for OpenCV --- src/cli/test.py | 11 +++++++++++ src/compare.py | 11 +++++++++++ src/config.ini | 5 +++++ 3 files changed, 27 insertions(+) diff --git a/src/cli/test.py b/src/cli/test.py index c3cbebb..346e465 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -37,6 +37,17 @@ if fw != -1: if fh != -1: video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh) +# Disable autoexposure +exposure = config.getint("video", "exposure", fallback=-1) +if exposure != -1: + # For a strange reason on some cameras (e.g. Lenoxo X1E) + # setting manual exposure works only after a couple frames + # are captured. + for i in range(6): + video_capture.grab() + video_capture.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual + video_capture.set(cv2.CAP_PROP_EXPOSURE, 167.0) + # Let the user know what's up print(""" Opening a window with a test feed diff --git a/src/compare.py b/src/compare.py index 3f472b8..97d1027 100644 --- a/src/compare.py +++ b/src/compare.py @@ -144,6 +144,17 @@ if fh != -1: # This will let the camera adjust its light levels while we're importing for faster scanning video_capture.grab() +# Disable autoexposure +exposure = config.getint("video", "exposure", fallback=-1) +if exposure != -1: + # For a strange reason on some cameras (e.g. Lenoxo X1E) + # setting manual exposure works only after a couple frames + # are captured. + for i in range(5): + video_capture.grab() + video_capture.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual + video_capture.set(cv2.CAP_PROP_EXPOSURE, 167.0) + # Note the time it took to open the camera timings["ic"] = time.time() - timings["ic"] diff --git a/src/config.ini b/src/config.ini index 34d5685..c306410 100644 --- a/src/config.ini +++ b/src/config.ini @@ -70,6 +70,11 @@ device_format = v4l2 # OPENCV only. force_mjpeg = false +# Specify exposure value explicitly. This disables autoexposure. +# Use qv4l2 to determine an appropriate value. +# OPENCV only. +exposure = -1 + [debug] # Show a short but detailed diagnostic report in console end_report = false From f921f4a5f80d2cef9ba4f3a0abbddcc391d32009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Aceda=C5=84ski?= Date: Mon, 11 Mar 2019 00:32:15 +0100 Subject: [PATCH 2/9] Disable Howdy if lid is closed --- src/config.ini | 3 +++ src/pam.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/config.ini b/src/config.ini index 34d5685..d8a40a9 100644 --- a/src/config.ini +++ b/src/config.ini @@ -15,6 +15,9 @@ suppress_unknown = false # Disable Howdy in remote shells ignore_ssh = true +# Disable Howdy if lid is closed +ignore_closed_lid = true + # Auto dismiss lock screen on confirmation # Will run loginctl unlock-sessions after every auth # Experimental, can behave incorrectly on some systems diff --git a/src/pam.py b/src/pam.py index da98cf1..f539b32 100644 --- a/src/pam.py +++ b/src/pam.py @@ -4,6 +4,7 @@ import subprocess import sys import os +import glob # pam-python is running python 2, so we use the old module here import ConfigParser @@ -25,6 +26,11 @@ def doAuth(pamh): if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: sys.exit(0) + # 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) + # Alert the user that we are doing face detection if config.get("core", "detection_notice") == "true": pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) From f1678de32424810ef12d5e8afd084463c376579e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Aceda=C5=84ski?= Date: Mon, 11 Mar 2019 23:30:31 +0100 Subject: [PATCH 3/9] Set exposure in the main loop and at every frame --- src/cli/test.py | 19 ++++++++++--------- src/compare.py | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/cli/test.py b/src/cli/test.py index 346e465..b748140 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -37,16 +37,8 @@ if fw != -1: if fh != -1: video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh) -# Disable autoexposure +# Read exposure from config to use in the main loop exposure = config.getint("video", "exposure", fallback=-1) -if exposure != -1: - # For a strange reason on some cameras (e.g. Lenoxo X1E) - # setting manual exposure works only after a couple frames - # are captured. - for i in range(6): - video_capture.grab() - video_capture.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual - video_capture.set(cv2.CAP_PROP_EXPOSURE, 167.0) # Let the user know what's up print(""" @@ -204,6 +196,15 @@ try: if slow_mode: time.sleep(.5 - frame_time) + if exposure != -1: + # For a strange reason on some cameras (e.g. Lenoxo X1E) + # setting manual exposure works only after a couple frames + # 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)) + # On ctrl+C except KeyboardInterrupt: # Let the user know we're stopping diff --git a/src/compare.py b/src/compare.py index 97d1027..17ac2a7 100644 --- a/src/compare.py +++ b/src/compare.py @@ -144,16 +144,8 @@ if fh != -1: # This will let the camera adjust its light levels while we're importing for faster scanning video_capture.grab() -# Disable autoexposure +# Read exposure from config to use in the main loop exposure = config.getint("video", "exposure", fallback=-1) -if exposure != -1: - # For a strange reason on some cameras (e.g. Lenoxo X1E) - # setting manual exposure works only after a couple frames - # are captured. - for i in range(5): - video_capture.grab() - video_capture.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1.0) # 1 = Manual - video_capture.set(cv2.CAP_PROP_EXPOSURE, 167.0) # Note the time it took to open the camera timings["ic"] = time.time() - timings["ic"] @@ -272,3 +264,12 @@ while True: # End peacefully stop(0) + + if exposure != -1: + # For a strange reason on some cameras (e.g. Lenoxo X1E) + # setting manual exposure works only after a couple frames + # 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)) From fd300e52e34fc35bc4e5215f75c105fb4aad34c3 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Thu, 14 Mar 2019 15:53:48 +0100 Subject: [PATCH 4/9] Misc fixes for #139, #132 and #130 --- debian/prerm | 2 +- src/cli/test.py | 16 ++++++++++++++-- src/compare.py | 2 ++ src/pam.py | 16 ++++++++-------- src/recorders/pyv4l2_reader.py | 2 +- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/debian/prerm b/debian/prerm index 150e7b8..e7c2469 100755 --- a/debian/prerm +++ b/debian/prerm @@ -12,7 +12,7 @@ from shutil import rmtree if "remove" not in sys.argv and "purge" not in sys.argv: sys.exit(0) -# Don't try running this if it's already gome +# Don't try running this if it's already gone if not os.path.exists("/lib/security/howdy/cli"): sys.exit(0) diff --git a/src/cli/test.py b/src/cli/test.py index b748140..d15c6b9 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -1,4 +1,4 @@ -# Show a windows with the video stream and testing information +# Show a window with the video stream and testing information # Import required modules import configparser @@ -109,6 +109,17 @@ try: # 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 = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frame = clahe.apply(frame) # Make a frame to put overlays in @@ -159,7 +170,8 @@ try: rec_tm = time.time() # Get the locations of all faces and their locations - face_locations = face_detector(frame, 1) # upsample 1 time + # Upsample it once + face_locations = face_detector(frame, 1) rec_tm = time.time() - rec_tm # Loop though all faces and paint a circle around them diff --git a/src/compare.py b/src/compare.py index 17ac2a7..e26e14e 100644 --- a/src/compare.py +++ b/src/compare.py @@ -187,6 +187,8 @@ while True: # 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 diff --git a/src/pam.py b/src/pam.py index f539b32..b127cc6 100644 --- a/src/pam.py +++ b/src/pam.py @@ -18,21 +18,21 @@ def doAuth(pamh): """Starts authentication in a seperate process""" # Abort is Howdy is disabled - if config.getboolean("core", "disabled"): + if config.getboolean("core", "disabled", fallback=False): sys.exit(0) # Abort if we're in a remote SSH env - if config.getboolean("core", "ignore_ssh"): + if config.getboolean("core", "ignore_ssh", fallback=True): if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: sys.exit(0) # 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')): + if config.getboolean("core", "ignore_closed_lid", fallback=True): + if any("closed" in open(f).read() for f in glob.glob("/proc/acpi/button/lid/*/state")): sys.exit(0) # Alert the user that we are doing face detection - if config.get("core", "detection_notice") == "true": + if config.getboolean("core", "detection_notice", fallback=False): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) # Run compare as python3 subprocess to circumvent python version and import issues @@ -40,7 +40,7 @@ def doAuth(pamh): # Status 10 means we couldn't find any face models if status == 10: - if not config.getboolean("core", "suppress_unknown"): + if not config.getboolean("core", "suppress_unknown", fallback=False): 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 @@ -53,11 +53,11 @@ def doAuth(pamh): # Status 0 is a successful exit elif status == 0: # Show the success message if it isn't suppressed - if not config.getboolean("core", "no_confirmation"): + if not config.getboolean("core", "no_confirmation", fallback=False): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) # Try to dismiss the lock screen if enabled - if config.get("core", "dismiss_lockscreen"): + if config.getboolean("core", "dismiss_lockscreen", fallback=False): # Run it as root with a timeout of 1s, and never ask for a password through the UI subprocess.Popen(["sudo", "timeout", "1", "loginctl", "unlock-sessions", "--no-ask-password"]) diff --git a/src/recorders/pyv4l2_reader.py b/src/recorders/pyv4l2_reader.py index 6d52871..d0efcf5 100644 --- a/src/recorders/pyv4l2_reader.py +++ b/src/recorders/pyv4l2_reader.py @@ -8,7 +8,7 @@ import sys from cv2 import cvtColor, COLOR_GRAY2BGR, CAP_PROP_FRAME_WIDTH, CAP_PROP_FRAME_HEIGHT try: - from v4l2.frame import Frame + from pyv4l2.frame import Frame except ImportError: print("Missing pyv4l2 module, please run:") print(" pip3 install pyv4l2\n") From 853736615448df616a7b318199f5114f5b138d4d Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 29 Mar 2019 22:21:14 +0100 Subject: [PATCH 5/9] Remove dismiss_lockscreen config option --- debian/postinst | 7 +++++++ src/config.ini | 5 ----- src/pam.py | 5 ----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/debian/postinst b/debian/postinst index 7ceb41c..6da42db 100755 --- a/debian/postinst +++ b/debian/postinst @@ -70,6 +70,13 @@ if not os.path.exists("/tmp/howdy_picked_device"): if key == "timout": key = "timeout" + # MIGRATION 2.5.0 -> 2.5.1 + # Remove unsafe automatic dismissal of lock screen + if key == "dismiss_lockscreen": + if (value == "true"): + print("DEPRECATION: Config falue dismiss_lockscreen is no longer supported because of login loop issues.") + continue + try: newConf.set(section, key, value) # Add a new section where needed diff --git a/src/config.ini b/src/config.ini index 5274457..995516f 100644 --- a/src/config.ini +++ b/src/config.ini @@ -18,11 +18,6 @@ ignore_ssh = true # Disable Howdy if lid is closed ignore_closed_lid = true -# Auto dismiss lock screen on confirmation -# Will run loginctl unlock-sessions after every auth -# Experimental, can behave incorrectly on some systems -dismiss_lockscreen = false - # Disable howdy in the PAM # The howdy command will still function disabled = false diff --git a/src/pam.py b/src/pam.py index b127cc6..85eccdf 100644 --- a/src/pam.py +++ b/src/pam.py @@ -56,11 +56,6 @@ def doAuth(pamh): if not config.getboolean("core", "no_confirmation", fallback=False): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) - # Try to dismiss the lock screen if enabled - if config.getboolean("core", "dismiss_lockscreen", fallback=False): - # Run it as root with a timeout of 1s, and never ask for a password through the UI - subprocess.Popen(["sudo", "timeout", "1", "loginctl", "unlock-sessions", "--no-ask-password"]) - return pamh.PAM_SUCCESS # Otherwise, we can't discribe what happend but it wasn't successful From f9abedef95091edc3b3db8753efbe8fc2639756d Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 29 Mar 2019 22:30:23 +0100 Subject: [PATCH 6/9] Removed dlib --yes flag, as per #122 --- debian/postinst | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/debian/postinst b/debian/postinst index 6da42db..0f4e021 100755 --- a/debian/postinst +++ b/debian/postinst @@ -159,24 +159,6 @@ log("Building dlib") cmd = ["sudo", "python3", "setup.py", "install"] cuda_used = False -flags = "" - -# Get the CPU details -with open("/proc/cpuinfo") as info: - for line in info: - if "flags" in line: - flags = line - break - -# Use the most efficient instruction set the CPU supports -if "avx" in flags: - cmd += ["--yes", "USE_AVX_INSTRUCTIONS"] -elif "sse4" in flags: - cmd += ["--yes", "USE_SSE4_INSTRUCTIONS"] -elif "sse3" in flags: - cmd += ["--yes", "USE_SSE3_INSTRUCTIONS"] -elif "sse2" in flags: - cmd += ["--yes", "USE_SSE2_INSTRUCTIONS"] # Compile and link dlib try: From 323be9cf8db108edfcf53e163c69b419537ee041 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 29 Mar 2019 22:45:38 +0100 Subject: [PATCH 7/9] Made dark threshold dynamic in test command (#128) --- src/cli/test.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cli/test.py b/src/cli/test.py index d15c6b9..124f8eb 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -37,8 +37,9 @@ if fw != -1: if fh != -1: video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh) -# Read exposure from config to use in the main loop +# 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") # Let the user know what's up print(""" @@ -147,9 +148,6 @@ try: # 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) - # Print the statis in the bottom left print_text(0, "RESOLUTION: %dx%d" % (height, width)) print_text(1, "FPS: %d" % (fps, )) @@ -161,7 +159,7 @@ try: 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: + if hist_perc[0] > dark_threshold: # 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: From c092529a98296dfe74f44f0186ca1132ecc802cd Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 29 Mar 2019 22:51:08 +0100 Subject: [PATCH 8/9] Misc fixes for #120 and #126 --- debian/postinst | 2 +- src/compare.py | 4 ++++ src/config.ini | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/debian/postinst b/debian/postinst index 0f4e021..7ab3b89 100755 --- a/debian/postinst +++ b/debian/postinst @@ -73,7 +73,7 @@ if not os.path.exists("/tmp/howdy_picked_device"): # MIGRATION 2.5.0 -> 2.5.1 # Remove unsafe automatic dismissal of lock screen if key == "dismiss_lockscreen": - if (value == "true"): + if value == "true": print("DEPRECATION: Config falue dismiss_lockscreen is no longer supported because of login loop issues.") continue diff --git a/src/compare.py b/src/compare.py index e26e14e..98c6b72 100644 --- a/src/compare.py +++ b/src/compare.py @@ -183,6 +183,10 @@ while True: # 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 diff --git a/src/config.ini b/src/config.ini index 995516f..1183f48 100644 --- a/src/config.ini +++ b/src/config.ini @@ -75,4 +75,5 @@ exposure = -1 [debug] # Show a short but detailed diagnostic report in console +# Enabling this can cause some UI apps to fail, only enable it to debug end_report = false From a53530e54e4097c44a61a4adc7b965b63f655fb1 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 29 Mar 2019 23:13:44 +0100 Subject: [PATCH 9/9] Ready for release --- debian/changelog | 11 +++++++++++ src/cli/test.py | 1 - src/pam.py | 12 ++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/debian/changelog b/debian/changelog index 66a0ecb..338f9d4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +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!) + * Added option to disable howdy when the laptop lid is closed (thanks @accek!) + * Added automatic fallback to default frame color palette (thanks @Ethiarpus!) + * Added manual exposure setting (thanks @accek!) + * Fixed test command ignoring dark frame threshold (thanks @eduncan911!) + * Fixed import error in v4l2 recorder (thanks @timwelch!) + + -- boltgolt Fri, 29 Mar 2019 23:02:21 +0100 + howdy (2.5.0) xenial; urgency=medium * Added FFmpeg and v4l2 recorders (thanks @timwelch!) diff --git a/src/cli/test.py b/src/cli/test.py index 124f8eb..16fadb1 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -121,7 +121,6 @@ try: print("\nUnknown camera, please check your 'device_path' config value.\n") raise - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frame = clahe.apply(frame) # Make a frame to put overlays in overlay = frame.copy() diff --git a/src/pam.py b/src/pam.py index 85eccdf..3d44114 100644 --- a/src/pam.py +++ b/src/pam.py @@ -18,21 +18,21 @@ def doAuth(pamh): """Starts authentication in a seperate process""" # Abort is Howdy is disabled - if config.getboolean("core", "disabled", fallback=False): + if config.getboolean("core", "disabled"): sys.exit(0) # Abort if we're in a remote SSH env - if config.getboolean("core", "ignore_ssh", fallback=True): + 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) # Abort if lid is closed - if config.getboolean("core", "ignore_closed_lid", fallback=True): + 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) # Alert the user that we are doing face detection - if config.getboolean("core", "detection_notice", fallback=False): + if config.getboolean("core", "detection_notice"): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) # Run compare as python3 subprocess to circumvent python version and import issues @@ -40,7 +40,7 @@ def doAuth(pamh): # Status 10 means we couldn't find any face models if status == 10: - if not config.getboolean("core", "suppress_unknown", fallback=False): + if not config.getboolean("core", "suppress_unknown"): 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 @@ -53,7 +53,7 @@ def doAuth(pamh): # Status 0 is a successful exit elif status == 0: # Show the success message if it isn't suppressed - if not config.getboolean("core", "no_confirmation", fallback=False): + if not config.getboolean("core", "no_confirmation"): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) return pamh.PAM_SUCCESS