From 140b4f3d6741778e8f9b774d0933af23c0915ebf Mon Sep 17 00:00:00 2001 From: Anthony Wharton Date: Sun, 12 May 2019 18:40:28 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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()