diff --git a/README.md b/README.md index 0152002..451c81f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,76 @@ wheel and is available on Linux/macOS and Windows and supports Python 2.7/3.5+ and PyPy. $ pip install CVPubSubs + +Usage +----------- + +###Video Editing and Publishing + +####Display your webcam + import cvpubsubs.webcam_pub as w + + w.VideoHandlerThread().display() + +####Change Display Arguments + import cvpubsubs.webcam_pub as w + + video_thread = w.VideoHandlerThread(video_source=0, + callbacks = w.display_callbacks, + request_size=(800, 600), + high_speed = False, + fps_limit = 8 + ) + + video_thread.display() + +####Run your own functions on the frames + import cvpubsubs.webcam_pub as w + + def redden_frame_print_spam(frame, cam_id): + frame[:, :, 0] = 0 + frame[:, :, 1] = 0 + print("Spam!") + + w.VideoHandlerThread(callbacks=[redden_frame_print_spam] + w.display_callbacks).display() + +####Display multiple windows from one source + import cvpubsubs.webcam_pub as w + from cvpubsubs.window_sub import SubscriberWindows + + def cam_handler(frame, cam_id): + SubscriberWindows.set_global_frame_dict(cam_id, frame, frame) + + t = w.VideoHandlerThread(0, [cam_handler], + request_size=(1280, 720), + high_speed=True, + fps_limit=240 + ) + + t.start() + + SubscriberWindows(window_names=['cammy', 'cammy2'], + video_sources=[str(0)] + ).loop() + + t.join() + +####Display multiple windows from multiple sources + iport cvpubsubs.webcam_pub as w + from cvpubsubs.window_sub import SubscriberWindows + + t1 = w.VideoHandlerThread(0) + t2 = w.VideoHandlerThread(1) + + t1.start() + t2.start() + + SubscriberWindows(window_names=['cammy', 'cammy2'], + video_sources=[0,1] + ).loop() + + t1.join() + t1.join() License ------- diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index e69de29..7fd229a 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -0,0 +1 @@ +__version__ = '0.2.0' diff --git a/cvpubsubs/webcam_pub/__init__.py b/cvpubsubs/webcam_pub/__init__.py index 6fff6cb..7392a0f 100644 --- a/cvpubsubs/webcam_pub/__init__.py +++ b/cvpubsubs/webcam_pub/__init__.py @@ -1,4 +1,4 @@ from .camctrl import CamCtrl -from .frame_handler import frame_handler_thread +from .frame_handler import VideoHandlerThread, display_callbacks from .get_cam_ids import get_cam_ids from .pub_cam import pub_cam_thread diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index 89069fa..826b7ca 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -7,35 +7,78 @@ from cvpubsubs.listen_default import listen_default from .pub_cam import pub_cam_thread if False: - from typing import Union, Tuple, Any, Callable + from typing import Union, Tuple, Any, Callable, List + +from cvpubsubs.window_sub import SubscriberWindows -def frame_handler_loop(cam_id, # type: Union[int, str] - frame_handler, # type: Callable[[np.ndarray, int], Any] - request_size=(1280, 720), # type: Tuple[int, int] - high_speed=False, # type: bool - fps_limit=240 # type: float - ): - t = pub_cam_thread(cam_id, request_size, high_speed, fps_limit) - sub_cam = pubsub.subscribe("CVCams." + str(cam_id) + ".Vid") - sub_owner = pubsub.subscribe("CVCamHandlers." + str(cam_id) + ".Cmd") - msg_owner = '' - while msg_owner != 'quit': - frame = listen_default(sub_cam, timeout=.1) # type: np.ndarray - if frame is not None: - frame = frame[0] - frame_handler(frame, cam_id) - msg_owner = listen_default(sub_owner, block=False, empty='') - pubsub.publish("CVCams." + str(cam_id) + ".Cmd", 'quit') - t.join() +def global_cv_display_callback(frame, # type: np.ndarray + cam_id # type: Union[int, str] + ): + """Default callback for sending frames to the global frame dictionary. + :param frame: The video or image frame + :type frame: np.ndarray + :param cam_id: The video or image source + :type cam_id: Union[int, str] + """ + SubscriberWindows.frame_dict[str(cam_id) + "frame"] = (frame,) -def frame_handler_thread(cam_id, # type: Union[int, str] - frame_handler, # type: Callable[[int, np.ndarray], Any] - request_size=(1280, 720), # type: Tuple[int, int] - high_speed=False, # type: bool - fps_limit=240 # type: float - ): # type: (...) -> threading.Thread - t = threading.Thread(target=frame_handler_loop, args=(cam_id, frame_handler, request_size, high_speed, fps_limit)) - t.start() - return t +display_callbacks = [global_cv_display_callback] + +class VideoHandlerThread(threading.Thread): + "Thread for publishing frames from a video source." + + def __init__(self, video_source=0, # type: Union[int, str] + callbacks=(global_cv_display_callback,), # type: List[Callable[[np.ndarray, int], Any]] + request_size=(1280, 720), # type: Tuple[int, int] + high_speed=True, # type: bool + fps_limit=240 # type: float + ): + """Sets up the main thread loop. + + :param video_source: The video or image source. Integers typically access webcams, while strings access files. + :type video_source: Union[int, str] + :param callbacks: A list of operations to be performed on every frame, including publishing. + :type callbacks: List[Callable[[np.ndarray, int], Any]] + :param request_size: Requested video size. Actual size may vary, since this is requesting from the hardware. + :type request_size: Tuple[int, int] + :param high_speed: If true, use compression to increase speed. + :type high_speed: bool + :param fps_limit: Limits frames per second. + :type fps_limit: float + """ + super(VideoHandlerThread, self).__init__(target=self.loop, args=()) + self.cam_id = video_source + self.callbacks = callbacks + self.request_size = request_size + self.high_speed = high_speed + self.fps_limit = fps_limit + + def loop(self): + """Continually gets frames from the video publisher, runs callbacks on them, and listens to commands.""" + t = pub_cam_thread(self.cam_id, self.request_size, self.high_speed, self.fps_limit) + sub_cam = pubsub.subscribe("CVCams." + str(self.cam_id) + ".Vid") + sub_owner = pubsub.subscribe("CVCamHandlers." + str(self.cam_id) + ".Cmd") + msg_owner = '' + while msg_owner != 'quit': + frame = listen_default(sub_cam, timeout=.1) # type: np.ndarray + if frame is not None: + frame = frame[0] + for c in self.callbacks: + c(frame, self.cam_id) + msg_owner = listen_default(sub_owner, block=False, empty='') + pubsub.publish("CVCams." + str(self.cam_id) + ".Cmd", 'quit') + t.join() + + def display(self, + callbacks=() # type: List[Callable[[List[np.ndarray]], Any]] + ): + """Default display operation. For multiple video sources, please use something outside of this class. + + :param callbacks: List of callbacks to be run on frames before displaying to the screen. + :type callbacks: List[Callable[[List[np.ndarray]], Any]] + """ + self.start() + SubscriberWindows(video_sources=[self.cam_id], callbacks=callbacks).loop() + self.join() diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 725b15c..8566eb9 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -8,7 +8,8 @@ from ..listen_default import listen_default from ..webcam_pub.camctrl import CamCtrl if False: - from typing import List + from typing import List, Union, Callable, Any + import numpy as np class SubscriberWindows(object): @@ -17,15 +18,23 @@ class SubscriberWindows(object): esc_key_codes = [27] # ESC key on most keyboards def __init__(self, - window_names, # type: List[str] - input_vid_global_names, # type: List[str] - callbacks=(None,), - input_cams=(0,) + window_names=('cvpubsubs',), # type: List[str] + video_sources=(0,), # type: List[Union[str,int]] + callbacks=(None,), # type: List[Callable[[List[np.ndarray]], Any]] ): self.window_names = window_names - self.input_vid_global_names = input_vid_global_names + self.input_vid_global_names = [str(name) + "frame" for name in video_sources] self.callbacks = callbacks - self.input_cams = input_cams + self.input_cams = video_sources + + + @staticmethod + def set_global_frame_dict(name, *args): + SubscriberWindows.frame_dict[str(name)+"frame"] = [*args] + + def __stop_all_cams(self): + for c in self.input_cams: + CamCtrl.stop_cam(c) def handle_keys(self, key_input, # type: int @@ -33,8 +42,7 @@ class SubscriberWindows(object): if key_input in self.esc_key_codes: for name in self.window_names: cv2.destroyWindow(name + " (press ESC to quit)") - for c in self.input_cams: - CamCtrl.stop_cam(c) + self.__stop_all_cams() WinCtrl.quit() elif key_input not in [-1, 0]: try: @@ -45,15 +53,17 @@ class SubscriberWindows(object): ) def update_window_frames(self): + win_num = 0 for i in range(len(self.input_vid_global_names)): if self.input_vid_global_names[i] in self.frame_dict and self.frame_dict[ self.input_vid_global_names[i]] is not None: - if self.callbacks[i % len(self.callbacks)] is not None: + if len(self.callbacks)>0 and self.callbacks[i % len(self.callbacks)] is not None: frames = self.callbacks[i % len(self.callbacks)](self.frame_dict[self.input_vid_global_names[i]]) else: frames = self.frame_dict[self.input_vid_global_names[i]] for f in range(len(frames)): - cv2.imshow(self.window_names[f % len(self.window_names)] + " (press ESC to quit)", frames[f]) + cv2.imshow(self.window_names[win_num % len(self.window_names)] + " (press ESC to quit)", frames[f]) + win_num += 1 # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 def loop(self): @@ -64,3 +74,4 @@ class SubscriberWindows(object): self.handle_keys(cv2.waitKey(1)) msg_cmd = listen_default(sub_cmd, block=False, empty='') pubsub.publish("CVWinCmd", 'quit') + self.__stop_all_cams() diff --git a/setup.py b/setup.py index 9e82fc6..5b1ed0c 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ setup( version=version, description='', long_description=readme, + long_description_content_type='text/markdown', author='SimLeek', author_email='josh.miklos@gmail.com', maintainer='SimLeek', diff --git a/tests/test_pub_cam.py b/tests/test_pub_cam.py index e2f6e9f..4646bcf 100644 --- a/tests/test_pub_cam.py +++ b/tests/test_pub_cam.py @@ -14,7 +14,7 @@ class TestFrameHandler(ut.TestCase): print(frame.shape) self.i += 1 - w.frame_handler_thread(0, test_frame_handler, + w.VideoHandlerThread(0, [test_frame_handler], request_size=(1280, 720), high_speed=True, fps_limit=240) diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index a54a5ba..6582ff9 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -7,6 +7,9 @@ import cvpubsubs.webcam_pub as w from cvpubsubs.listen_default import listen_default from cvpubsubs.window_sub import SubscriberWindows +if False: + import numpy as np + def print_keys_thread(): sub_key = pubsub.subscribe("CVKeyStroke") @@ -29,40 +32,54 @@ def start_print_keys_thread(): # type: (...) -> threading.Thread class TestSubWin(ut.TestCase): def test_sub(self): - def cam_handler(frame, cam_id): - SubscriberWindows.frame_dict[str(cam_id) + "Frame"] = (frame, frame) + w.VideoHandlerThread().display() - t = w.frame_handler_thread(0, cam_handler, - request_size=(1280, 720), - high_speed=True, - fps_limit=240 - ) + def test_sub_with_args(self): + video_thread = w.VideoHandlerThread(video_source=0, + callbacks=w.display_callbacks, + request_size=(800, 600), + high_speed=False, + fps_limit=8 + ) + + video_thread.display() + + def test_sub_with_callback(self): + def redden_frame_print_spam(frame, cam_id): + frame[:, :, 0] = 0 + frame[:, :, 1] = 0 + print("Spam!") + + w.VideoHandlerThread(callbacks=[redden_frame_print_spam] + w.display_callbacks).display() + + def test_multi_cams_one_source(self): + def cam_handler(frame, cam_id): + SubscriberWindows.set_global_frame_dict(cam_id, frame, frame) + + t = w.VideoHandlerThread(0, [cam_handler], + request_size=(1280, 720), + high_speed=True, + fps_limit=240 + ) + + t.start() SubscriberWindows(window_names=['cammy', 'cammy2'], - input_vid_global_names=[str(0) + "Frame"] + video_sources=[str(0)] ).loop() - w.CamCtrl.stop_cam(0) - t.join() - def test_key_sub(self): - def cam_handler(frame, cam_id): - SubscriberWindows.frame_dict[str(cam_id) + "Frame"] = (frame, frame) + def test_multi_cams_multi_source(self): + t1 = w.VideoHandlerThread(0) + t2 = w.VideoHandlerThread(1) - t = w.frame_handler_thread(0, cam_handler, - request_size=(1280, 720), - high_speed=True, - fps_limit=240 - ) - - kt = start_print_keys_thread() + t1.start() + t2.start() SubscriberWindows(window_names=['cammy', 'cammy2'], - input_vid_global_names=[str(0) + "Frame"] + video_sources=[0,1] ).loop() - w.CamCtrl.stop_cam(0) - - t.join() - kt.join() + t1.join() + t1.join()