diff --git a/.coveragerc b/.coveragerc index fcae781..bd0b0e5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,10 +1,10 @@ [run] source = - cvpubsubs + displayarray tests branch = True omit = - cvpubsubs/cli.py + displayarray/cli.py [report] exclude_lines = diff --git a/.travis.yml b/.travis.yml index 2a2448e..e9a355c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,14 +4,12 @@ sudo: true cache: pip python: - - '2.7' - '3.5' - '3.6' - '3.7' install: - pip install -r requirements.txt script: - - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then export CIBW_BUILD='cp27*'; fi - if [[ $TRAVIS_PYTHON_VERSION == 3.5 ]]; then export CIBW_BUILD='cp35*'; fi - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then export CIBW_BUILD='cp36*'; fi - if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then export CIBW_BUILD='cp37*'; fi diff --git a/README.md b/README.md index 7107d37..518eb0b 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,26 @@ -# CVPubSubs +# displayarray A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. ## Installation -CVPubSubs is distributed on `PyPI `_ as a universal +displayarray is distributed on `PyPI `_ as a universal wheel and is available on Linux/macOS and Windows and supports Python 2.7/3.5+ and PyPy. - $ pip install CVPubSubs + $ pip install displayarray ## Usage ### Video Editing and Publishing #### Display your webcam - import cvpubsubs.webcam_pub as w + import displayarray.webcam_pub as w w.VideoHandlerThread().display() #### Change Display Arguments - import cvpubsubs.webcam_pub as w + import displayarray.webcam_pub as w video_thread = w.VideoHandlerThread(video_source=0, callbacks = w.display_callbacks, @@ -30,8 +30,8 @@ Python 2.7/3.5+ and PyPy. ) #### handle mouse input - import cvpubsubs.webcam_pub as w - from cvpubsubs.input import mouse_loop + import displayarray.webcam_pub as w + from displayarray.input import mouse_loop @mouse_loop def print_mouse(mouse_event): @@ -40,8 +40,8 @@ Python 2.7/3.5+ and PyPy. w.VideoHandlerThread().display() #### take in key input - import cvpubsubs.webcam_pub as w - from cvpubsubs.input import key_loop + import displayarray.webcam_pub as w + from displayarray.input import key_loop @key_loop def print_key_thread(key_chr): @@ -50,7 +50,7 @@ Python 2.7/3.5+ and PyPy. w.VideoHandlerThread().display() #### Run your own functions on the frames - import cvpubsubs.webcam_pub as w + import displayarray.webcam_pub as w def redden_frame_print_spam(frame, cam_id): frame[:, :, 0] = 0 @@ -70,8 +70,8 @@ Python 2.7/3.5+ and PyPy. t.display() #### Display multiple windows from one source - import cvpubsubs.webcam_pub as w - from cvpubsubs.window_sub import SubscriberWindows + import displayarray.webcam_pub as w + from displayarray.window_sub import SubscriberWindows def cam_handler(frame, cam_id): SubscriberWindows.set_global_frame_dict(cam_id, frame, frame) @@ -91,8 +91,8 @@ Python 2.7/3.5+ and PyPy. t.join() #### Display multiple windows from multiple sources - iport cvpubsubs.webcam_pub as w - from cvpubsubs.window_sub import SubscriberWindows + iport displayarray.webcam_pub as w + from displayarray.window_sub import SubscriberWindows t1 = w.VideoHandlerThread(0) t2 = w.VideoHandlerThread(1) @@ -108,8 +108,8 @@ Python 2.7/3.5+ and PyPy. t1.join() #### Run a function on each pixel - from cvpubsubs.webcam_pub import VideoHandlerThread - from cvpubsubs.webcam_pub.callbacks import function_display_callback + from displayarray.webcam_pub import VideoHandlerThread + from displayarray.webcam_pub.callbacks import function_display_callback img = np.zeros((50, 50, 1)) img[0:5, 0:5, :] = 1 @@ -130,7 +130,7 @@ Python 2.7/3.5+ and PyPy. ## License -CVPubSubs is distributed under the terms of both +displayarray is distributed under the terms of both - `MIT License `_ - `Apache License, Version 2.0 `_ diff --git a/__init__.py b/__init__.py index b826e76..77c0b32 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ # redirection, so we can use subtree like pip -from cvpubsubs import webcam_pub, window_sub +from displayarray import frame_publising, subscriber_window diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py deleted file mode 100644 index b2741c7..0000000 --- a/cvpubsubs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__version__ = '0.6.5' - -from .window_sub.cv_window_sub import display diff --git a/cvpubsubs/callbacks.py b/cvpubsubs/callbacks.py deleted file mode 100644 index a8d6753..0000000 --- a/cvpubsubs/callbacks.py +++ /dev/null @@ -1,67 +0,0 @@ -from cvpubsubs.window_sub.winctrl import WinCtrl -import numpy as np - -if False: - from typing import Union - - -def global_cv_display_callback(frame, # type: np.ndarray - cam_id # type: Union[int, str] - ): - from cvpubsubs.window_sub import SubscriberWindows - """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 - - -class function_display_callback(object): # NOSONAR - def __init__(self, display_function, finish_function=None): - """Used for running arbitrary functions on pixels. - - >>> import random - >>> from cvpubsubs.webcam_pub import VideoHandlerThread - >>> img = np.zeros((300, 300, 3)) - >>> def fun(array, coords, finished): - ... r,g,b = random.random()/20.0, random.random()/20.0, random.random()/20.0 - ... array[coords[0:2]] = (array[coords[0:2]] + [r,g,b])%1.0 - >>> VideoHandlerThread(video_source=img, callbacks=function_display_callback(fun)).display() - - :param display_function: a function to run on the input image. - :param finish_function: a function to run on the input image when the other function finishes. - """ - self.looping = True - self.first_call = True - - def _run_finisher(self, frame, finished, *args, **kwargs): - if not callable(finish_function): - WinCtrl.quit() - else: - finished = finish_function(frame, Ellipsis, finished, *args, **kwargs) - if finished: - WinCtrl.quit() - - def _display_internal(self, frame, *args, **kwargs): - finished = True - if self.first_call: - # return to display initial frame - self.first_call = False - return - if self.looping: - it = np.nditer(frame, flags=['multi_index']) - while not it.finished: - x, y, c = it.multi_index - finished = display_function(frame, (x, y, c), finished, *args, **kwargs) - it.iternext() - if finished: - self.looping = False - _run_finisher(self, frame, finished, *args, **kwargs) - - self.inner_function = _display_internal - - def __call__(self, *args, **kwargs): - return self.inner_function(self, *args, **kwargs) diff --git a/cvpubsubs/input.py b/cvpubsubs/input.py deleted file mode 100644 index d341731..0000000 --- a/cvpubsubs/input.py +++ /dev/null @@ -1,95 +0,0 @@ -from cvpubsubs.window_sub.winctrl import WinCtrl -import threading -import time - -if False: - from typing import Callable - from cvpubsubs.window_sub.mouse_event import MouseEvent - - -class mouse_thread(object): # NOSONAR - - def __init__(self, f): - self.f = f - self.sub_mouse = WinCtrl.mouse_pub.make_sub() - - def __call__(self, *args, **kwargs): - self.f(self.sub_mouse, *args, **kwargs) - - -class mouse_loop_thread(object): # NOSONAR - - def __init__(self, f, run_when_no_events=False, fps=60): - self.f = f - self.sub_mouse = WinCtrl.mouse_pub.make_sub() - self.sub_cmd = WinCtrl.win_cmd_pub.make_sub() - self.sub_cmd.return_on_no_data = '' - self.run_when_no_events = run_when_no_events - self.fps = fps - - def __call__(self, *args, **kwargs): - msg_cmd = '' - while msg_cmd != 'quit': - mouse_xyzclick = self.sub_mouse.get(blocking=True) # type: MouseEvent - if mouse_xyzclick is not self.sub_mouse.return_on_no_data: - self.f(mouse_xyzclick, *args, **kwargs) - elif self.run_when_no_events: - self.f(None, *args, **kwargs) - msg_cmd = self.sub_cmd.get() - time.sleep(1.0 / self.fps) - WinCtrl.quit(force_all_read=False) - - -class mouse_loop(object): # NOSONAR - - def __init__(self, f, run_when_no_events=False): - self.t = threading.Thread(target=mouse_loop_thread(f, run_when_no_events)) - self.t.start() - - def __call__(self, *args, **kwargs): - return self.t - - -class key_thread(object): # NOSONAR - - def __init__(self, f): - self.f = f - self.sub_key = WinCtrl.key_pub.make_sub() - - def __call__(self, *args, **kwargs): - self.f(self.sub_key, *args, **kwargs) - - -class key_loop_thread(object): # NOSONAR - - def __init__(self, f, run_when_no_events=False, fps=60): - self.f = f - self.sub_key = WinCtrl.key_pub.make_sub() - self.sub_cmd = WinCtrl.win_cmd_pub.make_sub() - self.sub_cmd.return_on_no_data = '' - self.run_when_no_events = run_when_no_events - self.fps = fps - - def __call__(self, *args, **kwargs): - msg_cmd = '' - while msg_cmd != 'quit': - key_chr = self.sub_key.get() # type: chr - if key_chr is not self.sub_key.return_on_no_data: - self.f(key_chr, *args, **kwargs) - elif self.run_when_no_events: - self.f(None, *args, **kwargs) - msg_cmd = self.sub_cmd.get() - time.sleep(1.0 / self.fps) - WinCtrl.quit(force_all_read=False) - - -class key_loop(object): # NOSONAR - - def __init__(self, - f, # type: Callable[[chr],None] - run_when_no_events=False): - self.t = threading.Thread(target=key_loop_thread(f, run_when_no_events)) - self.t.start() - - def __call__(self, *args, **kwargs): - return self.t diff --git a/cvpubsubs/serialize.py b/cvpubsubs/serialize.py deleted file mode 100644 index 0db2aab..0000000 --- a/cvpubsubs/serialize.py +++ /dev/null @@ -1,12 +0,0 @@ -import numpy as np -from collections import Hashable - - -def uid_for_source(video_source): - if len(str(video_source)) <= 1000: - uid = str(video_source) - elif isinstance(video_source, Hashable): - uid = str(hash(video_source)) - else: - uid = str(hash(str(video_source))) - return uid diff --git a/cvpubsubs/webcam_pub/__init__.py b/cvpubsubs/webcam_pub/__init__.py deleted file mode 100644 index c3b2eb5..0000000 --- a/cvpubsubs/webcam_pub/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .camctrl import CamCtrl -from .frame_handler import VideoHandlerThread, display_callbacks -from .get_cam_ids import get_cam_ids -from .pub_cam import pub_cam_thread -from .np_cam import NpCam \ No newline at end of file diff --git a/cvpubsubs/webcam_pub/camctrl.py b/cvpubsubs/webcam_pub/camctrl.py deleted file mode 100644 index dee6ea6..0000000 --- a/cvpubsubs/webcam_pub/camctrl.py +++ /dev/null @@ -1,68 +0,0 @@ -from threading import Lock -from localpubsub import VariablePub, VariableSub - -if False: - from typing import Union, Dict - - -class CamHandler(object): - def __init__(self, name, sub): - self.name = name - self.cmd = None - self.sub = sub # type: VariableSub - self.pub = VariablePub() - self.cmd_pub = VariablePub() - - -class Cam(object): - def __init__(self, name): - self.name = name - self.cmd = None - self.frame_pub = VariablePub() - self.cmd_pub = VariablePub() - self.status_pub = VariablePub() - - -class CamCtrl(object): - cv_cam_handlers_dict = {} # type: Dict[str, CamHandler] - cv_cams_dict = {} # type: Dict[str, Cam] - - @staticmethod - def register_cam(cam_id): - cam = Cam(str(cam_id)) - CamCtrl.cv_cams_dict[str(cam_id)] = cam - CamCtrl.cv_cam_handlers_dict[str(cam_id)] = CamHandler(str(cam_id), cam.frame_pub.make_sub()) - - @staticmethod - def stop_cam(cam_id # type: Union[int, str] - ): - CamCtrl.cv_cams_dict[str(cam_id)].cmd_pub.publish('quit', blocking=True) - CamCtrl.cv_cam_handlers_dict[str(cam_id)].cmd_pub.publish('quit', blocking=True) - - @staticmethod - def cam_cmd_sub(cam_id, blocking=True): - if blocking: - while cam_id not in CamCtrl.cv_cams_dict: - continue - return CamCtrl.cv_cams_dict[str(cam_id)].cmd_pub.make_sub() - - @staticmethod - def cam_frame_sub(cam_id, blocking=True): - if blocking: - while cam_id not in CamCtrl.cv_cams_dict: - continue - return CamCtrl.cv_cams_dict[str(cam_id)].frame_pub.make_sub() - - @staticmethod - def cam_status_sub(cam_id, blocking=True): - if blocking: - while cam_id not in CamCtrl.cv_cams_dict: - continue - return CamCtrl.cv_cams_dict[str(cam_id)].status_pub.make_sub() - - @staticmethod - def handler_cmd_sub(cam_id, blocking=True): - if blocking: - while cam_id not in CamCtrl.cv_cam_handlers_dict: - continue - return CamCtrl.cv_cam_handlers_dict[str(cam_id)].cmd_pub.make_sub() \ No newline at end of file diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py deleted file mode 100644 index 0f9ae0e..0000000 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ /dev/null @@ -1,101 +0,0 @@ -import threading - -import numpy as np - -from cvpubsubs.webcam_pub.pub_cam import pub_cam_thread -from cvpubsubs.webcam_pub.camctrl import CamCtrl -from cvpubsubs.window_sub.winctrl import WinCtrl -from cvpubsubs.serialize import uid_for_source - -if False: - from typing import Union, Tuple, Any, Callable, List, Optional - - FrameCallable = Callable[[np.ndarray, int], Optional[np.ndarray]] - -from cvpubsubs.callbacks import global_cv_display_callback - -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, np.ndarray] - callbacks=(), # type: Union[List[FrameCallable], FrameCallable] - request_size=(-1, -1), # 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 = uid_for_source(video_source) - self.video_source = video_source - if callable(callbacks): - self.callbacks = [callbacks] - else: - self.callbacks = callbacks - self.request_size = request_size - self.high_speed = high_speed - self.fps_limit = fps_limit - self.exception_raised = None - - def loop(self): - """Continually gets frames from the video publisher, runs callbacks on them, and listens to commands.""" - t = pub_cam_thread(self.video_source, self.request_size, self.high_speed, self.fps_limit) - while str(self.cam_id) not in CamCtrl.cv_cams_dict: - continue - sub_cam = CamCtrl.cam_frame_sub(str(self.cam_id)) - sub_owner = CamCtrl.handler_cmd_sub(str(self.cam_id)) - msg_owner = sub_owner.return_on_no_data = '' - while msg_owner != 'quit': - frame = sub_cam.get(blocking=True, timeout=1.0) # type: np.ndarray - if frame is not None: - frame_c = None - for c in self.callbacks: - try: - frame_c = c(frame) - except TypeError as te: - raise TypeError("Callback functions for cvpubsub need to accept two arguments: array and uid") - except Exception as e: - self.exception_raised = e - frame = frame_c = self.exception_raised - CamCtrl.stop_cam(self.cam_id) - WinCtrl.quit() - raise e - if frame_c is not None: - global_cv_display_callback(frame_c, self.cam_id) - else: - global_cv_display_callback(frame, self.cam_id) - msg_owner = sub_owner.get() - sub_owner.release() - sub_cam.release() - CamCtrl.stop_cam(self.cam_id) - t.join() - - def display(self, - callbacks=() # type: List[Callable[[List[np.ndarray]], Any]] - ): - from cvpubsubs.window_sub import SubscriberWindows - - """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() - if self.exception_raised is not None: - raise self.exception_raised diff --git a/cvpubsubs/window_sub/__init__.py b/cvpubsubs/window_sub/__init__.py deleted file mode 100644 index d7d6227..0000000 --- a/cvpubsubs/window_sub/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .cv_window_sub import SubscriberWindows diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py deleted file mode 100644 index 7c32e4b..0000000 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ /dev/null @@ -1,200 +0,0 @@ -import warnings - -import cv2 -import numpy as np - -from .winctrl import WinCtrl -from cvpubsubs.webcam_pub.camctrl import CamCtrl -from cvpubsubs.webcam_pub.frame_handler import VideoHandlerThread -from localpubsub import NoData -from cvpubsubs.window_sub.mouse_event import MouseEvent -from cvpubsubs.serialize import uid_for_source - -from typing import List, Union, Callable, Any, Dict -import numpy as np -from cvpubsubs.callbacks import global_cv_display_callback - - -class SubscriberWindows(object): - frame_dict = {} - - esc_key_codes = [27] # ESC key on most keyboards - - def __init__(self, - window_names=('cvpubsubs',), # type: List[str] - video_sources=(0,), # type: List[Union[str,int]] - callbacks=(None,), # type: List[Callable[[List[np.ndarray]], Any]] - ): - self.source_names = [] - self.close_threads = None - self.frames = [] - self.input_vid_global_names = [] - self.window_names = [] - self.input_cams = [] - - for name in video_sources: - self.add_source(name) - self.callbacks = callbacks - for name in window_names: - self.add_window(name) - - def add_source(self, name): - uid = uid_for_source(name) - self.source_names.append(uid) - self.input_vid_global_names.append(uid + "frame") - self.input_cams.append(name) - - def add_window(self, name): - self.window_names.append(name) - cv2.namedWindow(name + " (press ESC to quit)") - cv2.setMouseCallback(name + " (press ESC to quit)", self.handle_mouse) - - def add_callback(self, callback): - self.callbacks.append(callback) - - @staticmethod - def set_global_frame_dict(name, *args): - if len(str(name)) <= 1000: - SubscriberWindows.frame_dict[str(name) + "frame"] = list(args) - elif isinstance(name, np.ndarray): - SubscriberWindows.frame_dict[str(hash(str(name))) + "frame"] = list(args) - else: - raise ValueError("Input window name too long.") - - def __stop_all_cams(self): - for c in self.source_names: - CamCtrl.stop_cam(c) - - def handle_keys(self, - key_input, # type: int - ): - if key_input in self.esc_key_codes: - for name in self.window_names: - cv2.destroyWindow(name + " (press ESC to quit)") - WinCtrl.quit() - self.__stop_all_cams() - return 'quit' - elif key_input not in [-1, 0]: - try: - WinCtrl.key_pub.publish(chr(key_input)) - except ValueError: - warnings.warn( - RuntimeWarning("Unknown key code: [{}]. Please report to cv_pubsubs issue page.".format(key_input)) - ) - - def handle_mouse(self, event, x, y, flags, param): - mousey = MouseEvent(event, x, y, flags, param) - WinCtrl.mouse_pub.publish(mousey) - - def _display_frames(self, frames, win_num, ids=None): - if isinstance(frames, Exception): - raise frames - for f in range(len(frames)): - # detect nested: - if isinstance(frames[f], (list, tuple)) or frames[f].dtype.num == 17 or len(frames[f].shape) > 3: - win_num = self._display_frames(frames[f], win_num, ids) - else: - cv2.imshow(self.window_names[win_num % len(self.window_names)] + " (press ESC to quit)", frames[f]) - win_num += 1 - return win_num - - 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 \ - not isinstance(self.frame_dict[self.input_vid_global_names[i]], NoData): - if len(self.callbacks) > 0 and self.callbacks[i % len(self.callbacks)] is not None: - self.frames = self.callbacks[i % len(self.callbacks)]( - self.frame_dict[self.input_vid_global_names[i]]) - else: - self.frames = self.frame_dict[self.input_vid_global_names[i]] - if isinstance(self.frames, np.ndarray) and len(self.frames.shape) <= 3: - self.frames = [self.frames] - win_num = self._display_frames(self.frames, win_num) - - def update(self, arr=None, id=None): - if arr is not None and id is not None: - global_cv_display_callback(arr, id) - if id not in self.input_cams: - self.add_source(id) - self.add_window(id) - sub_cmd = WinCtrl.win_cmd_sub() - self.update_window_frames() - msg_cmd = sub_cmd.get() - key = self.handle_keys(cv2.waitKey(1)) - return msg_cmd, key - - def wait_for_init(self): - msg_cmd="" - key = "" - while msg_cmd != 'quit' and key != 'quit' and len(self.frames)==0: - msg_cmd, key = self.update() - - def end(self): - if self.close_threads is not None: - for t in self.close_threads: - t.join() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.end() - - # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 - def loop(self): - sub_cmd = WinCtrl.win_cmd_sub() - msg_cmd = '' - key = '' - while msg_cmd != 'quit' and key != 'quit': - msg_cmd, key = self.update() - sub_cmd.release() - WinCtrl.quit(force_all_read=False) - self.__stop_all_cams() - - -from cvpubsubs.callbacks import global_cv_display_callback - -from threading import Thread - - -def display(*vids, - callbacks: Union[Dict[Any, Callable], List[Callable], Callable, None] = None, - window_names=[], - blocking=False): - vid_threads = [] - if isinstance(callbacks, Dict): - for v in vids: - v_name = uid_for_source(v) - v_callbacks = [] - if v_name in callbacks: - v_callbacks.extend(callbacks[v_name]) - if v in callbacks: - v_callbacks.extend(callbacks[v]) - vid_threads.append(VideoHandlerThread(v, callbacks=v_callbacks)) - elif isinstance(callbacks, List): - for v in vids: - vid_threads.append(VideoHandlerThread(v, callbacks=callbacks)) - elif isinstance(callbacks, Callable): - for v in vids: - vid_threads.append(VideoHandlerThread(v, callbacks=[callbacks])) - else: - for v in vids: - vid_threads.append(VideoHandlerThread(v)) - for v in vid_threads: - v.start() - if len(window_names) == 0: - window_names = ["window {}".format(i) for i in range(len(vids))] - if blocking: - SubscriberWindows(window_names=window_names, - video_sources=vids - ).loop() - for v in vid_threads: - v.join() - else: - s = SubscriberWindows(window_names=window_names, - video_sources=vids - ) - s.close_threads = vid_threads - v_names = [] - for v in vids: - v_name = uid_for_source(v) - v_names.append(v_name) - return s, v_names diff --git a/cvpubsubs/window_sub/mouse_event.py b/cvpubsubs/window_sub/mouse_event.py deleted file mode 100644 index b4eb31f..0000000 --- a/cvpubsubs/window_sub/mouse_event.py +++ /dev/null @@ -1,13 +0,0 @@ -class MouseEvent(object): - def __init__(self, event, x, y, flags, param): - self.event = event - self.x = x - self.y = y - self.flags = flags - self.param = param - - def __repr__(self): - return self.__str__() - - def __str__(self): - return "event:{}\nx,y:{},{}\nflags:{}\nparam:{}\n".format(self.event, self.x, self.y, self.flags, self.param) diff --git a/cvpubsubs/window_sub/winctrl.py b/cvpubsubs/window_sub/winctrl.py deleted file mode 100644 index 4d41f05..0000000 --- a/cvpubsubs/window_sub/winctrl.py +++ /dev/null @@ -1,18 +0,0 @@ -import threading -import logging - -from localpubsub import VariablePub, VariableSub - - -class WinCtrl(object): - key_pub = VariablePub() - mouse_pub = VariablePub() - win_cmd_pub = VariablePub() - - @staticmethod - def quit(force_all_read=True): - WinCtrl.win_cmd_pub.publish('quit', force_all_read=force_all_read) - - @staticmethod - def win_cmd_sub(): # type: ()->VariableSub - return WinCtrl.win_cmd_pub.make_sub() # type: VariableSub diff --git a/displayarray/__init__.py b/displayarray/__init__.py new file mode 100644 index 0000000..3ec18ba --- /dev/null +++ b/displayarray/__init__.py @@ -0,0 +1,9 @@ +""" +Display any array, webcam, or video file. + +display is a function that displays these in their own windows. +""" + +__version__ = "0.6.6" + +from .subscriber_window.subscriber_windows import display diff --git a/displayarray/callbacks.py b/displayarray/callbacks.py new file mode 100644 index 0000000..1612a27 --- /dev/null +++ b/displayarray/callbacks.py @@ -0,0 +1,72 @@ +from displayarray.subscriber_window import window_commands +import numpy as np + +from typing import Union + + +def global_cv_display_callback(frame: np.ndarray, cam_id: Union[int, str]): + """ + Send 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] + """ + from displayarray.subscriber_window import SubscriberWindows + + SubscriberWindows.FRAME_DICT[str(cam_id) + "frame"] = frame + + +class function_display_callback(object): # NOSONAR + """ + Used for running arbitrary functions on pixels. + + >>> import random + >>> from displayarray.webcam_pub import VideoHandlerThread + >>> img = np.zeros((300, 300, 3)) + >>> def fun(array, coords, finished): + ... r,g,b = random.random()/20.0, random.random()/20.0, random.random()/20.0 + ... array[coords[0:2]] = (array[coords[0:2]] + [r,g,b])%1.0 + >>> VideoHandlerThread(video_source=img, callbacks=function_display_callback(fun)).display() + + :param display_function: a function to run on the input image. + :param finish_function: a function to run on the input image when the other function finishes. + """ + + def __init__(self, display_function, finish_function=None): + + self.looping = True + self.first_call = True + + def _run_finisher(self, frame, finished, *args, **kwargs): + if not callable(finish_function): + window_commands.quit() + else: + finished = finish_function(frame, Ellipsis, finished, *args, **kwargs) + if finished: + window_commands.quit() + + def _display_internal(self, frame, *args, **kwargs): + finished = True + if self.first_call: + # return to display initial frame + self.first_call = False + return + if self.looping: + it = np.nditer(frame, flags=["multi_index"]) + while not it.finished: + x, y, c = it.multi_index + finished = display_function( + frame, (x, y, c), finished, *args, **kwargs + ) + it.iternext() + if finished: + self.looping = False + _run_finisher(self, frame, finished, *args, **kwargs) + + self.inner_function = _display_internal + + def __call__(self, *args, **kwargs): + """Call the function "function_display_callback" was set up with.""" + return self.inner_function(self, *args, **kwargs) diff --git a/displayarray/frame_publising/__init__.py b/displayarray/frame_publising/__init__.py new file mode 100644 index 0000000..d1af476 --- /dev/null +++ b/displayarray/frame_publising/__init__.py @@ -0,0 +1,15 @@ +""" +Handles publishing arrays, videos, and cameras. + +CamCtrl handles sending and receiving commands to specific camera (or array/video) publishers +VideoHandlerThread updates the frames for the global displayer, since OpenCV can only update on the main thread +get_cam_ids gets the ids for all cameras that OpenCV can detect +pub_cam_thread continually publishes updates to arrays, videos, and cameras +np_cam simulates numpy arrays as OpenCV cameras +""" + +from . import subscriber_dictionary +from .frame_update_thread import VideoHandlerThread +from .get_frame_ids import get_cam_ids +from .np_to_opencv import NpCam +from .frame_publishing import pub_cam_thread diff --git a/cvpubsubs/webcam_pub/pub_cam.py b/displayarray/frame_publising/frame_publishing.py similarity index 53% rename from cvpubsubs/webcam_pub/pub_cam.py rename to displayarray/frame_publising/frame_publishing.py index 9f1384d..c1bfe44 100644 --- a/cvpubsubs/webcam_pub/pub_cam.py +++ b/displayarray/frame_publising/frame_publishing.py @@ -4,48 +4,50 @@ import time import cv2 import numpy as np -from cvpubsubs.webcam_pub.camctrl import CamCtrl -from .np_cam import NpCam -from cvpubsubs.serialize import uid_for_source +from displayarray.frame_publising import subscriber_dictionary +from .np_to_opencv import NpCam +from displayarray.uid import uid_for_source -if False: - from typing import Union, Tuple +from typing import Union, Tuple -def pub_cam_loop(cam_id, # type: Union[int, str] - request_size=(1280, 720), # type: Tuple[int, int] - high_speed=False, # type: bool - fps_limit=240 # type: float - ): # type: (...)->bool - """Publishes whichever camera you select to CVCams..Vid +def pub_cam_loop( + cam_id: Union[int, str], + request_size: Tuple[int, int] = (1280, 720), + high_speed: bool = False, + fps_limit: float = 240, +) -> bool: + """ + Publish whichever camera you select to CVCams..Vid. + You can send a quit command 'quit' to CVCams..Cmd Status information, such as failure to open, will be posted to CVCams..Status - :param high_speed: Selects mjpeg transferring, which most cameras seem to support, so speed isn't limited :param fps_limit: Limits the frames per second. :param cam_id: An integer representing which webcam to use, or a string representing a video file. :param request_size: A tuple with width, then height, to request the video size. :return: True if loop ended normally, False if it failed somehow. """ - name = uid_for_source(cam_id) if isinstance(cam_id, (int, str)): - cam = cv2.VideoCapture(cam_id) + cam: Union[NpCam, cv2.VideoCapture] = cv2.VideoCapture(cam_id) elif isinstance(cam_id, np.ndarray): - cam = NpCam(cam_id) # type: NpCam + cam = NpCam(cam_id) else: - raise TypeError("Only strings or ints representing cameras, or numpy arrays representing pictures supported.") + raise TypeError( + "Only strings or ints representing cameras, or numpy arrays representing pictures supported." + ) - CamCtrl.register_cam(name) + subscriber_dictionary.register_cam(name) # cam.set(cv2.CAP_PROP_CONVERT_RGB, 0) frame_counter = 0 - sub = CamCtrl.cam_cmd_sub(name) - sub.return_on_no_data = '' - msg = '' + sub = subscriber_dictionary.cam_cmd_sub(name) + sub.return_on_no_data = "" + msg = "" if high_speed: cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG) @@ -54,23 +56,23 @@ def pub_cam_loop(cam_id, # type: Union[int, str] cam.set(cv2.CAP_PROP_FRAME_HEIGHT, request_size[1]) if not cam.isOpened(): - CamCtrl.cv_cams_dict[name].status_pub.publish("failed") + subscriber_dictionary.CV_CAMS_DICT[name].status_pub.publish("failed") return False now = time.time() - while msg != 'quit': - time.sleep(1. / (fps_limit - (time.time() - now))) + while msg != "quit": + time.sleep(1.0 / (fps_limit - (time.time() - now))) now = time.time() (ret, frame) = cam.read() # type: Tuple[bool, np.ndarray ] if ret is False or not isinstance(frame, np.ndarray): cam.release() - CamCtrl.cv_cams_dict[name].status_pub.publish("failed") + subscriber_dictionary.CV_CAMS_DICT[name].status_pub.publish("failed") return False if cam.get(cv2.CAP_PROP_FRAME_COUNT) > 0: frame_counter += 1 if frame_counter >= cam.get(cv2.CAP_PROP_FRAME_COUNT): frame_counter = 0 cam = cv2.VideoCapture(cam_id) - CamCtrl.cv_cams_dict[name].frame_pub.publish(frame) + subscriber_dictionary.CV_CAMS_DICT[name].frame_pub.publish(frame) msg = sub.get() sub.release() @@ -78,12 +80,15 @@ def pub_cam_loop(cam_id, # type: Union[int, str] return True -def pub_cam_thread(cam_id, # type: Union[int, str] - request_ize=(1280, 720), # type: Tuple[int, int] - high_speed=False, # type: bool - fps_limit=240 # type: float - ): - # type: (...) -> threading.Thread - t = threading.Thread(target=pub_cam_loop, args=(cam_id, request_ize, high_speed, fps_limit)) +def pub_cam_thread( + cam_id: Union[int, str], + request_ize: Tuple[int, int] = (1280, 720), + high_speed: bool = False, + fps_limit: float = 240, +) -> threading.Thread: + """Run pub_cam_loop in a new thread.""" + t = threading.Thread( + target=pub_cam_loop, args=(cam_id, request_ize, high_speed, fps_limit) + ) t.start() return t diff --git a/displayarray/frame_publising/frame_update_thread.py b/displayarray/frame_publising/frame_update_thread.py new file mode 100644 index 0000000..8385f01 --- /dev/null +++ b/displayarray/frame_publising/frame_update_thread.py @@ -0,0 +1,100 @@ +import threading +from typing import Union, Tuple, Any, Callable, List, Optional + +import numpy as np + +from displayarray.callbacks import global_cv_display_callback +from displayarray.uid import uid_for_source +from displayarray.frame_publising import subscriber_dictionary +from displayarray.frame_publising.frame_publishing import pub_cam_thread +from displayarray.subscriber_window import window_commands + +FrameCallable = Callable[[np.ndarray], Optional[np.ndarray]] + + +class VideoHandlerThread(threading.Thread): + """Thread for publishing frames from a video source.""" + + def __init__( + self, + video_source: Union[int, str, np.ndarray] = 0, + callbacks: Optional[Union[List[FrameCallable], FrameCallable]] = None, + request_size: Tuple[int, int] = (99999, 99999), + high_speed: bool = True, + fps_limit: float = 240, + ): + super(VideoHandlerThread, self).__init__(target=self.loop, args=()) + self.cam_id = uid_for_source(video_source) + self.video_source = video_source + if callbacks is None: + callbacks = [] + if callable(callbacks): + self.callbacks = [callbacks] + else: + self.callbacks = callbacks + self.request_size = request_size + self.high_speed = high_speed + self.fps_limit = fps_limit + self.exception_raised = None + + def __wait_for_cam_id(self): + while str(self.cam_id) not in subscriber_dictionary.CV_CAMS_DICT: + continue + + def __apply_callbacks_to_frame(self, frame): + if frame is not None: + frame_c = None + for c in self.callbacks: + try: + frame_c = c(frame) + except TypeError as te: + raise TypeError( + "Callback functions for cvpubsub need to accept two arguments: array and uid" + ) + except Exception as e: + self.exception_raised = e + frame = frame_c = self.exception_raised + subscriber_dictionary.stop_cam(self.cam_id) + window_commands.quit() + raise e + if frame_c is not None: + global_cv_display_callback(frame_c, self.cam_id) + else: + global_cv_display_callback(frame, self.cam_id) + + def loop(self): + """Continually get frames from the video publisher, run callbacks on them, and listen to commands.""" + t = pub_cam_thread( + self.video_source, self.request_size, self.high_speed, self.fps_limit + ) + self.__wait_for_cam_id() + + sub_cam = subscriber_dictionary.cam_frame_sub(str(self.cam_id)) + sub_owner = subscriber_dictionary.handler_cmd_sub(str(self.cam_id)) + msg_owner = sub_owner.return_on_no_data = "" + while msg_owner != "quit": + frame = sub_cam.get(blocking=True, timeout=1.0) # type: np.ndarray + self.__apply_callbacks_to_frame(frame) + msg_owner = sub_owner.get() + sub_owner.release() + sub_cam.release() + subscriber_dictionary.stop_cam(self.cam_id) + t.join() + + def display(self, callbacks: List[Callable[[np.ndarray], Any]] = None): + """ + Start 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. + """ + from displayarray.subscriber_window import SubscriberWindows + + if callbacks is None: + callbacks = [] + self.start() + SubscriberWindows(video_sources=[self.cam_id], callbacks=callbacks).loop() + self.join() + if self.exception_raised is not None: + raise self.exception_raised diff --git a/cvpubsubs/webcam_pub/get_cam_ids.py b/displayarray/frame_publising/get_frame_ids.py similarity index 56% rename from cvpubsubs/webcam_pub/get_cam_ids.py rename to displayarray/frame_publising/get_frame_ids.py index e355951..ac95b18 100644 --- a/cvpubsubs/webcam_pub/get_cam_ids.py +++ b/displayarray/frame_publising/get_frame_ids.py @@ -1,11 +1,11 @@ import cv2 -if False: - from typing import List +from typing import List -def get_cam_ids(): # type: () -> List[int] - cam_list = [] +def get_cam_ids() -> List[int]: + """Get all cameras that OpenCV can currently detect.""" + cam_list: List[int] = [] while True: cam = cv2.VideoCapture(len(cam_list)) diff --git a/cvpubsubs/webcam_pub/np_cam.py b/displayarray/frame_publising/np_to_opencv.py similarity index 74% rename from cvpubsubs/webcam_pub/np_cam.py rename to displayarray/frame_publising/np_to_opencv.py index 34517f8..3366aed 100644 --- a/cvpubsubs/webcam_pub/np_cam.py +++ b/displayarray/frame_publising/np_to_opencv.py @@ -3,6 +3,8 @@ import cv2 class NpCam(object): + """Add OpenCV camera controls to a numpy array.""" + def __init__(self, img): assert isinstance(img, np.ndarray) self.__img = img @@ -23,6 +25,7 @@ class NpCam(object): self.__img = cv2.resize(self.__img, (self.__width, self.__height)) def set(self, *args, **kwargs): + """Set CAP_PROP_FRAME_WIDTH or CAP_PROP_FRAME_HEIGHT to scale a numpy array to that size.""" if args[0] in [cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT]: self.__wait_for_ratio = not self.__wait_for_ratio if args[0] == cv2.CAP_PROP_FRAME_WIDTH: @@ -34,14 +37,18 @@ class NpCam(object): @staticmethod def get(*args, **kwargs): + """Get OpenCV args. Currently only a fake CAP_PROP_FRAME_COUNT to fix detecting video ends.""" if args[0] == cv2.CAP_PROP_FRAME_COUNT: return float("inf") def read(self): - return (True, self.__img) + """Read back the numpy array in standard "did it work", "the array", OpenCV format.""" + return True, self.__img def isOpened(self): # NOSONAR + """Hack to tell OpenCV we're opened until we call release.""" return self.__is_opened def release(self): + """Let OpenCV know we're finished.""" self.__is_opened = False diff --git a/displayarray/frame_publising/subscriber_dictionary.py b/displayarray/frame_publising/subscriber_dictionary.py new file mode 100644 index 0000000..f47f527 --- /dev/null +++ b/displayarray/frame_publising/subscriber_dictionary.py @@ -0,0 +1,78 @@ +"""Publisher-subscriber commands to and from the camera.""" + +from localpubsub import VariablePub, VariableSub + +from typing import Union, Dict + + +class CamHandler(object): + """A camera handler instance that will send commands to and receive data from a camera.""" + + def __init__(self, name, sub): + self.name = name + self.cmd = None + self.sub: VariableSub = sub + self.pub = VariablePub() + self.cmd_pub = VariablePub() + + +class Cam(object): + """A camera publisher instance that will send frames, status, and commands out.""" + + def __init__(self, name): + self.name = name + self.cmd = None + self.frame_pub = VariablePub() + self.cmd_pub = VariablePub() + self.status_pub = VariablePub() + + +CV_CAM_HANDLERS_DICT: Dict[str, CamHandler] = {} +CV_CAMS_DICT: Dict[str, Cam] = {} + + +def register_cam(cam_id): + """Register camera "cam_id" to a global list so it can be picked up.""" + cam = Cam(str(cam_id)) + CV_CAMS_DICT[str(cam_id)] = cam + CV_CAM_HANDLERS_DICT[str(cam_id)] = CamHandler( + str(cam_id), cam.frame_pub.make_sub() + ) + + +def stop_cam(cam_id: Union[int, str]): + """Tell camera "cam_id" to end it's main loop.""" + CV_CAMS_DICT[str(cam_id)].cmd_pub.publish("quit", blocking=True) + CV_CAM_HANDLERS_DICT[str(cam_id)].cmd_pub.publish("quit", blocking=True) + + +def cam_cmd_sub(cam_id, blocking=True): + """Get a command subscriber for registered camera "cam_id".""" + if blocking: + while cam_id not in CV_CAMS_DICT: + continue + return CV_CAMS_DICT[str(cam_id)].cmd_pub.make_sub() + + +def cam_frame_sub(cam_id, blocking=True): + """Get a frame subscriber for registered camera "cam_id".""" + if blocking: + while cam_id not in CV_CAMS_DICT: + continue + return CV_CAMS_DICT[str(cam_id)].frame_pub.make_sub() + + +def cam_status_sub(cam_id, blocking=True): + """Get a status subscriber for registered camera "cam_id".""" + if blocking: + while cam_id not in CV_CAMS_DICT: + continue + return CV_CAMS_DICT[str(cam_id)].status_pub.make_sub() + + +def handler_cmd_sub(cam_id, blocking=True): + """Get a command subscriber for registered camera "cam_id" handler.""" + if blocking: + while cam_id not in CV_CAM_HANDLERS_DICT: + continue + return CV_CAM_HANDLERS_DICT[str(cam_id)].cmd_pub.make_sub() diff --git a/displayarray/input.py b/displayarray/input.py new file mode 100644 index 0000000..ff19b1f --- /dev/null +++ b/displayarray/input.py @@ -0,0 +1,122 @@ +from displayarray.subscriber_window import window_commands +import threading +import time + +from typing import Callable + + +class MouseEvent(object): + """Holds all the OpenCV mouse event information.""" + + def __init__(self, event, x, y, flags, param): + self.event = event + self.x = x + self.y = y + self.flags = flags + self.param = param + + def __repr__(self): + return self.__str__() + + def __str__(self): + return "event:{}\nx,y:{},{}\nflags:{}\nparam:{}\n".format( + self.event, self.x, self.y, self.flags, self.param + ) + + +class _mouse_thread(object): # NOSONAR + """Run a function on mouse information that is received by the window.""" + + def __init__(self, f): + self.f = f + self.sub_mouse = window_commands.mouse_pub.make_sub() + + def __call__(self, *args, **kwargs): + """Call the function this was set up with.""" + self.f(self.sub_mouse, *args, **kwargs) + + +class _mouse_loop_thread(object): # NOSONAR + """Run a function on mouse information that is received by the window, in the main loop.""" + + def __init__(self, f, run_when_no_events=False, fps=60): + self.f = f + self.sub_mouse = window_commands.mouse_pub.make_sub() + self.sub_cmd = window_commands.win_cmd_pub.make_sub() + self.sub_cmd.return_on_no_data = "" + self.run_when_no_events = run_when_no_events + self.fps = fps + + def __call__(self, *args, **kwargs): + """Run the function this was set up with in a loop.""" + msg_cmd = "" + while msg_cmd != "quit": + mouse_xyzclick = self.sub_mouse.get(blocking=True) # type: MouseEvent + if mouse_xyzclick is not self.sub_mouse.return_on_no_data: + self.f(mouse_xyzclick, *args, **kwargs) + elif self.run_when_no_events: + self.f(None, *args, **kwargs) + msg_cmd = self.sub_cmd.get() + time.sleep(1.0 / self.fps) + window_commands.quit(force_all_read=False) + + +class mouse_loop(object): # NOSONAR + """Run a function on mouse information that is received by the window, continuously in a new thread.""" + + def __init__(self, f, run_when_no_events=False): + self.t = threading.Thread(target=_mouse_loop_thread(f, run_when_no_events)) + self.t.start() + + def __call__(self, *args, **kwargs): + """Return the thread that was started with the function passed in.""" + return self.t + + +class _key_thread(object): # NOSONAR + """Run a function on mouse information that is received by the window.""" + + def __init__(self, f): + self.f = f + self.sub_key = window_commands.key_pub.make_sub() + + def __call__(self, *args, **kwargs): + """Call the function this was set up with.""" + self.f(self.sub_key, *args, **kwargs) + + +class _key_loop_thread(object): # NOSONAR + """Run a function on mouse information that is received by the window, in the main loop.""" + + def __init__(self, f, run_when_no_events=False, fps=60): + self.f = f + self.sub_key = window_commands.key_pub.make_sub() + self.sub_cmd = window_commands.win_cmd_pub.make_sub() + self.sub_cmd.return_on_no_data = "" + self.run_when_no_events = run_when_no_events + self.fps = fps + + def __call__(self, *args, **kwargs): + """Run the function this was set up with in a loop.""" + msg_cmd = "" + while msg_cmd != "quit": + key_chr = self.sub_key.get() # type: chr + if key_chr is not self.sub_key.return_on_no_data: + self.f(key_chr, *args, **kwargs) + elif self.run_when_no_events: + self.f(None, *args, **kwargs) + msg_cmd = self.sub_cmd.get() + time.sleep(1.0 / self.fps) + window_commands.quit(force_all_read=False) + + +class key_loop(object): # NOSONAR + """Run a function on mouse information that is received by the window, continuously in a new thread.""" + + def __init__(self, f: Callable[[str], None], run_when_no_events=False): + self.t = threading.Thread(target=_key_loop_thread(f, run_when_no_events)) + self.t.start() + + def __call__(self, *args, **kwargs): + """Return the thread that was started with the function passed in.""" + return self.t diff --git a/displayarray/subscriber_window/__init__.py b/displayarray/subscriber_window/__init__.py new file mode 100644 index 0000000..6df33c0 --- /dev/null +++ b/displayarray/subscriber_window/__init__.py @@ -0,0 +1,7 @@ +""" +Displays arrays. + +SubscriberWindows displays one array per window, updating it as it's changed. +""" + +from .subscriber_windows import SubscriberWindows diff --git a/displayarray/subscriber_window/subscriber_windows.py b/displayarray/subscriber_window/subscriber_windows.py new file mode 100644 index 0000000..472835c --- /dev/null +++ b/displayarray/subscriber_window/subscriber_windows.py @@ -0,0 +1,245 @@ +import warnings +from threading import Thread +from typing import List, Union, Callable, Any, Dict, Iterable, Optional + +import cv2 +import numpy as np +from localpubsub import NoData + +from displayarray.callbacks import global_cv_display_callback +from displayarray.uid import uid_for_source +from displayarray.frame_publising import subscriber_dictionary +from displayarray.frame_publising.frame_update_thread import FrameCallable +from displayarray.frame_publising.frame_update_thread import VideoHandlerThread +from displayarray.input import MouseEvent +from displayarray.subscriber_window import window_commands + + +class SubscriberWindows(object): + """Windows that subscribe to updates to cameras, videos, and arrays.""" + + FRAME_DICT: Dict[str, np.ndarray] = {} + ESC_KEY_CODES = [27] # ESC key on most keyboards + + def __init__( + self, + window_names: Iterable[str] = ("displayarray",), + video_sources: Iterable[Union[str, int]] = (0,), + callbacks: Optional[List[Callable[[np.ndarray], Any]]] = None, + ): + self.source_names: List[Union[str, int]] = [] + self.close_threads: Optional[List[Thread]] = None + self.frames: List[np.ndarray] = [] + self.input_vid_global_names: List[str] = [] + self.window_names: List[str] = [] + self.input_cams: List[str] = [] + + if callbacks is None: + callbacks = [] + for name in video_sources: + self.add_source(name) + self.callbacks = callbacks + for name in window_names: + self.add_window(name) + + def add_source(self, name): + """Add another source for this class to display.""" + uid = uid_for_source(name) + self.source_names.append(uid) + self.input_vid_global_names.append(uid + "frame") + self.input_cams.append(name) + + def add_window(self, name): + """Add another window for this class to display sources with. The name will be the title.""" + self.window_names.append(name) + cv2.namedWindow(name + " (press ESC to quit)") + cv2.setMouseCallback(name + " (press ESC to quit)", self.handle_mouse) + + def add_callback(self, callback): + """Add a callback for this class to apply to videos.""" + self.callbacks.append(callback) + + def __stop_all_cams(self): + for c in self.source_names: + subscriber_dictionary.stop_cam(c) + + def handle_keys( + self, key_input # type: int + ): + """Capture key input for the escape function and passing to key control subscriber threads.""" + if key_input in self.ESC_KEY_CODES: + for name in self.window_names: + cv2.destroyWindow(name + " (press ESC to quit)") + window_commands.quit() + self.__stop_all_cams() + return "quit" + elif key_input not in [-1, 0]: + try: + window_commands.key_pub.publish(chr(key_input)) + except ValueError: + warnings.warn( + RuntimeWarning( + "Unknown key code: [{}]. Please report to cv_pubsubs issue page.".format( + key_input + ) + ) + ) + + def handle_mouse(self, event, x, y, flags, param): + """Capture mouse input for mouse control subscriber threads.""" + mousey = MouseEvent(event, x, y, flags, param) + window_commands.mouse_pub.publish(mousey) + + def _display_frames(self, frames, win_num, ids=None): + if isinstance(frames, Exception): + raise frames + for f in range(len(frames)): + # detect nested: + if ( + isinstance(frames[f], (list, tuple)) + or frames[f].dtype.num == 17 + or len(frames[f].shape) > 3 + ): + win_num = self._display_frames(frames[f], win_num, ids) + else: + cv2.imshow( + self.window_names[win_num % len(self.window_names)] + + " (press ESC to quit)", + frames[f], + ) + win_num += 1 + return win_num + + def update_window_frames(self): + """Update the windows with the newest data for all frames.""" + 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 not isinstance( + self.FRAME_DICT[self.input_vid_global_names[i]], NoData + ): + if ( + len(self.callbacks) > 0 + and self.callbacks[i % len(self.callbacks)] is not None + ): + self.frames = self.callbacks[i % len(self.callbacks)]( + self.FRAME_DICT[self.input_vid_global_names[i]] + ) + else: + self.frames = self.FRAME_DICT[self.input_vid_global_names[i]] + if isinstance(self.frames, np.ndarray) and len(self.frames.shape) <= 3: + self.frames = [self.frames] + win_num = self._display_frames(self.frames, win_num) + + def update(self, arr=None, id=None): + """Update window frames once. Optionally add a new input and input id.""" + if arr is not None and id is not None: + global_cv_display_callback(arr, id) + if id not in self.input_cams: + self.add_source(id) + self.add_window(id) + sub_cmd = window_commands.win_cmd_sub() + self.update_window_frames() + msg_cmd = sub_cmd.get() + key = self.handle_keys(cv2.waitKey(1)) + return msg_cmd, key + + def wait_for_init(self): + """Update window frames in a loop until they're actually updated. Useful for waiting for cameras to init.""" + msg_cmd = "" + key = "" + while msg_cmd != "quit" and key != "quit" and len(self.frames) == 0: + msg_cmd, key = self.update() + + def end(self): + """Close all threads. Should be used with non-blocking mode.""" + window_commands.quit(force_all_read=False) + self.__stop_all_cams() + if self.close_threads is not None: + for t in self.close_threads: + t.join() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.end() + + def loop(self): + """Continually update window frame. OpenCV only allows this in the main thread.""" + sub_cmd = window_commands.win_cmd_sub() + msg_cmd = "" + key = "" + while msg_cmd != "quit" and key != "quit": + msg_cmd, key = self.update() + sub_cmd.release() + window_commands.quit(force_all_read=False) + self.__stop_all_cams() + + +def _get_video_callback_dict_threads( + *vids, callbacks: Optional[Dict[Any, FrameCallable]] = None +): + assert callbacks is not None + vid_threads = [] + for v in vids: + v_name = uid_for_source(v) + v_callbacks: List[Callable[[np.ndarray], Any]] = [] + if v_name in callbacks: + v_callbacks.append(callbacks[v_name]) + if v in callbacks: + v_callbacks.append(callbacks[v]) + vid_threads.append(VideoHandlerThread(v, callbacks=v_callbacks)) + return vid_threads + + +def _get_video_threads( + *vids, + callbacks: Optional[ + Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable] + ] = None +): + vid_threads: List[Thread] = [] + if isinstance(callbacks, Dict): + vid_threads = _get_video_callback_dict_threads(*vids, callbacks=callbacks) + elif isinstance(callbacks, List): + for v in vids: + vid_threads.append(VideoHandlerThread(v, callbacks=callbacks)) + elif callable(callbacks): + for v in vids: + vid_threads.append(VideoHandlerThread(v, callbacks=[callbacks])) + else: + for v in vids: + if v is not None: + vid_threads.append(VideoHandlerThread(v)) + return vid_threads + + +def display( + *vids, + callbacks: Optional[ + Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable] + ] = None, + window_names=None, + blocking=False +): + """ + Display all the arrays, cameras, and videos passed in. + + callbacks can be a dictionary linking functions to videos, or a list of function or functions operating on the video + data before displaying. + Window names end up becoming the title of the windows + """ + vid_threads = _get_video_threads(*vids, callbacks=callbacks) + for v in vid_threads: + v.start() + if window_names is None: + window_names = ["window {}".format(i) for i in range(len(vids))] + if blocking: + SubscriberWindows(window_names=window_names, video_sources=vids).loop() + for v in vid_threads: + v.join() + else: + s = SubscriberWindows(window_names=window_names, video_sources=vids) + s.close_threads = vid_threads + v_names = [] + for v in vids: + v_name = uid_for_source(v) + v_names.append(v_name) + return s, v_names diff --git a/displayarray/subscriber_window/window_commands.py b/displayarray/subscriber_window/window_commands.py new file mode 100644 index 0000000..f2015d1 --- /dev/null +++ b/displayarray/subscriber_window/window_commands.py @@ -0,0 +1,15 @@ +from localpubsub import VariablePub, VariableSub + +key_pub = VariablePub() +mouse_pub = VariablePub() +win_cmd_pub = VariablePub() + + +def quit(force_all_read=True): + """Quit the main loop displaying all the windows.""" + win_cmd_pub.publish("quit", force_all_read=force_all_read) + + +def win_cmd_sub() -> VariableSub: + """Get a subscriber to the main window loop.""" + return win_cmd_pub.make_sub() diff --git a/displayarray/uid.py b/displayarray/uid.py new file mode 100644 index 0000000..f64b6df --- /dev/null +++ b/displayarray/uid.py @@ -0,0 +1,18 @@ +from collections.abc import Hashable + + +def uid_for_source(video_source): + """Get a uid for any source so it can be passed through the publisher-subscriber system.""" + if len(str(video_source)) <= 1000: + uid = str(video_source) + elif isinstance(video_source, Hashable): + try: + uid = str(hash(video_source)) + except TypeError: + raise NotImplementedError( + "Displaying immutables filled with mutables is not allowed yet. " + "No tuples of arrays." + ) + else: + uid = str(hash(str(video_source))) + return uid diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..fddcb5d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,11 @@ +[mypy] +python_version = 3.7 + +[mypy-numpy.*] +ignore_missing_imports = True + +[mypy-cv2.*] +ignore_missing_imports = True + +[mypy-localpubsub.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f0cca88..57c4831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [metadata] -name = 'CVPubSubs' +name = 'displayarray' version = '0.0.2' description = 'Simple tool for working with multiple streams from OpenCV.' author = 'SimLeek' author_email = 'josh.miklos@gmail.com' license = 'MIT/Apache-2.0' -url = 'https://github.com/simleek/CVPubSubs' +url = 'https://github.com/simleek/displayarray' [requires] -python_version = ['2.7', '3.5', '3.6', 'pypy', 'pypy3'] +python_version = ['3.5', '3.6', 'pypy', 'pypy3'] [build-system] requires = ['setuptools', 'wheel'] diff --git a/setup.py b/setup.py index 831d843..ab8535f 100644 --- a/setup.py +++ b/setup.py @@ -2,56 +2,46 @@ from io import open from setuptools import find_packages, setup -with open('cvpubsubs/__init__.py', 'r') as f: +with open("displayarray/__init__.py", "r") as f: for line in f: - if line.startswith('__version__'): - version = line.strip().split('=')[1].strip(' \'"') + if line.startswith("__version__"): + version = line.strip().split("=")[1].strip(" '\"") break else: - version = '0.0.1' + version = "0.0.1" -with open('README.md', 'r', encoding='utf-8') as f: +with open("README.md", "r", encoding="utf-8") as f: readme = f.read() -REQUIRES = [ - 'opencv_python == 3.4.5.20', - 'localpubsub == 0.0.4', - 'numpy == 1.16.1' -] +REQUIRES = ["opencv_python == 3.4.5.20", "localpubsub == 0.0.4", "numpy == 1.16.1"] setup( - name='CVPubSubs', + name="displayarray", version=version, - description='', + description="", long_description=readme, - long_description_content_type='text/markdown', - author='SimLeek', - author_email='josh.miklos@gmail.com', - maintainer='SimLeek', - maintainer_email='josh.miklos@gmail.com', - url='https://github.com/SimLeek/CV_PubSubs', - license='MIT/Apache-2.0', - - keywords=[ - 'opencv', 'camera', - ], - + long_description_content_type="text/markdown", + author="SimLeek", + author_email="josh.miklos@gmail.com", + maintainer="SimLeek", + maintainer_email="josh.miklos@gmail.com", + url="https://github.com/SimLeek/CV_PubSubs", + license="MIT/Apache-2.0", + keywords=["opencv", "camera"], classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'License :: OSI Approved :: Apache Software License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ], - install_requires=REQUIRES, - tests_require=['coverage', 'pytest'], - + tests_require=["coverage", "pytest"], packages=find_packages(), ) diff --git a/tests/test_pub_cam.py b/tests/test_pub_cam.py index 4646bcf..fe0435f 100644 --- a/tests/test_pub_cam.py +++ b/tests/test_pub_cam.py @@ -1,4 +1,4 @@ -import cvpubsubs.webcam_pub as w +import displayarray.frame_publising as w import unittest as ut @@ -6,15 +6,17 @@ class TestFrameHandler(ut.TestCase): i = 0 def test_handler(self): - def test_frame_handler(frame, cam_id): if self.i == 200: - w.CamCtrl.stop_cam(cam_id) + w.subscriber_dictionary.stop_cam(cam_id) if self.i % 100 == 0: print(frame.shape) self.i += 1 - w.VideoHandlerThread(0, [test_frame_handler], - request_size=(1280, 720), - high_speed=True, - fps_limit=240) + w.VideoHandlerThread( + 0, + [test_frame_handler], + request_size=(1280, 720), + high_speed=True, + fps_limit=240, + ) diff --git a/tests/test_simple_api.py b/tests/test_simple_api.py index b8893d3..b75ec32 100644 --- a/tests/test_simple_api.py +++ b/tests/test_simple_api.py @@ -1,61 +1,69 @@ import unittest as ut -class TestSubWin(ut.TestCase): +class TestSubWin(ut.TestCase): def test_display_numpy(self): - from cvpubsubs import display + from displayarray import display import numpy as np - display(np.random.normal(0.5, .1, (500,500,3))) + s, vids = display(np.random.normal(0.5, 0.1, (500, 500, 3))) + s.end() + print("ended") def test_display_numpy_callback(self): - from cvpubsubs import display + from displayarray import display import numpy as np - arr = np.random.normal(0.5, .1, (500, 500, 3)) + arr = np.random.normal(0.5, 0.1, (500, 500, 3)) def fix_arr_cv(arr_in): - arr_in[:] += np.random.normal(0.01, .005, (500, 500, 3)) - arr_in%=1.0 + arr_in[:] += np.random.normal(0.01, 0.005, (500, 500, 3)) + arr_in %= 1.0 - display(arr, callbacks= fix_arr_cv, blocking=True) + display(arr, callbacks=fix_arr_cv, blocking=True) def test_display_numpy_loop(self): - from cvpubsubs import display + from displayarray import display import numpy as np - arr = np.random.normal(0.5, .1, (500, 500, 3)) + arr = np.random.normal(0.5, 0.1, (500, 500, 3)) - displayer, ids = display(arr, blocking = False) + displayer, ids = display(arr, blocking=False) while True: - arr[:] += np.random.normal(0.01, .005, (500, 500, 3)) + arr[:] += np.random.normal(0.01, 0.005, (500, 500, 3)) arr %= 1.0 displayer.update(arr, ids[0]) displayer.end() def test_display_tensorflow(self): - from cvpubsubs import display + from displayarray import display import numpy as np from tensorflow.keras import layers, models import tensorflow as tf for gpu in tf.config.experimental.list_physical_devices("GPU"): tf.compat.v2.config.experimental.set_memory_growth(gpu, True) - #tf.keras.backend.set_floatx("float16") - displayer, ids = display(0, blocking = False) + displayer, ids = display(0, blocking=False) displayer.wait_for_init() autoencoder = models.Sequential() autoencoder.add( - layers.Conv2D(20, (3, 3), activation="sigmoid", input_shape=displayer.frames[0].shape) + layers.Conv2D( + 20, (3, 3), activation="sigmoid", input_shape=displayer.frames[0].shape + ) ) autoencoder.add(layers.Conv2DTranspose(3, (3, 3), activation="sigmoid")) autoencoder.compile(loss="mse", optimizer="adam") while True: - grab = tf.convert_to_tensor(displayer.frame_dict['0frame'][np.newaxis, ...].astype(np.float32)/255.0) + grab = tf.convert_to_tensor( + displayer.FRAME_DICT["0frame"][np.newaxis, ...].astype(np.float32) + / 255.0 + ) autoencoder.fit(grab, grab, steps_per_epoch=1, epochs=1) output_image = autoencoder.predict(grab, steps=1) - displayer.update((output_image[0]*255.0).astype(np.uint8), "uid for autoencoder output") + displayer.update( + (output_image[0] * 255.0).astype(np.uint8), "uid for autoencoder output" + ) diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index e0931f9..c06538d 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -1,20 +1,14 @@ -import threading import unittest as ut -import cvpubsubs.webcam_pub as w -from cvpubsubs.window_sub import SubscriberWindows -from cvpubsubs.window_sub.winctrl import WinCtrl -from cvpubsubs import display -from cvpubsubs.input import mouse_loop, key_loop -import numpy as np +import displayarray.frame_publising as w +from displayarray.subscriber_window import SubscriberWindows +from displayarray import display +from displayarray.input import mouse_loop, key_loop, MouseEvent -if False: - import numpy as np - from cvpubsubs.window_sub.mouse_event import MouseEvent +import numpy as np class TestSubWin(ut.TestCase): - def test_mouse_loop(self): @mouse_loop def print_mouse_thread(mouse_event): @@ -41,11 +35,9 @@ class TestSubWin(ut.TestCase): w.VideoHandlerThread(video_source=img, request_size=(300, -1)).display() def test_sub_with_args(self): - video_thread = w.VideoHandlerThread(video_source=0, - request_size=(800, 600), - high_speed=False, - fps_limit=8 - ) + video_thread = w.VideoHandlerThread( + video_source=0, request_size=(800, 600), high_speed=False, fps_limit=8 + ) video_thread.display() @@ -67,10 +59,10 @@ class TestSubWin(ut.TestCase): self.assertEqual(v.exception_raised, e) def test_multi_cams_one_source(self): - display(0, window_names=['cammy','cammy2'], blocking=True) + display(0, window_names=["cammy", "cammy2"], blocking=True) def test_multi_cams_multi_source(self): - display(0, np.random.uniform(0.0, 1.0, (500,500)), blocking=True) + display(0, np.random.uniform(0.0, 1.0, (500, 500)), blocking=True) def test_nested_frames(self): def nest_frame(frame): @@ -84,28 +76,33 @@ class TestSubWin(ut.TestCase): frame = np.asarray([[[[[[frame + 1 / 0]]]]], [[[[[frame]]], [[[frame]]]]]]) return frame - v = w.VideoHandlerThread(callbacks=[nest_frame] + w.display_callbacks) + v = w.VideoHandlerThread(callbacks=[nest_frame]) v.start() with self.assertRaises(ZeroDivisionError) as e: - SubscriberWindows(window_names=[str(i) for i in range(3)], - video_sources=[str(0)] - ).loop() + SubscriberWindows( + window_names=[str(i) for i in range(3)], video_sources=[str(0)] + ).loop() self.assertEqual(v.exception_raised, e) v.join() def test_conway_life(self): - from cvpubsubs.webcam_pub import VideoHandlerThread - from cvpubsubs.callbacks import function_display_callback + from displayarray.frame_publising import VideoHandlerThread + from displayarray.callbacks import function_display_callback import numpy as np import cv2 + img = np.zeros((50, 50, 1)) img[0:5, 0:5, :] = 1 def conway(array, coords, finished): - neighbors = np.sum(array[max(coords[0] - 1, 0):min(coords[0] + 2, 50), - max(coords[1] - 1, 0):min(coords[1] + 2, 50)]) + neighbors = np.sum( + array[ + max(coords[0] - 1, 0) : min(coords[0] + 2, 50), + max(coords[1] - 1, 0) : min(coords[1] + 2, 50), + ] + ) neighbors = max(neighbors - np.sum(array[coords[0:2]]), 0.0) if array[coords] == 1.0: if neighbors < 2 or neighbors > 3: @@ -117,15 +114,26 @@ class TestSubWin(ut.TestCase): array[coords] = 1.0 @mouse_loop - def conway_add(mouse_event # type:MouseEvent - ): + def conway_add( + mouse_event # type:MouseEvent + ): if 0 <= mouse_event.x < 50 and 0 <= mouse_event.y < 50: if mouse_event.flags == cv2.EVENT_FLAG_LBUTTON: - img[mouse_event.y - 5:mouse_event.y + 10, mouse_event.x - 5:mouse_event.x + 10, :] = 0.0 + img[ + mouse_event.y - 5 : mouse_event.y + 10, + mouse_event.x - 5 : mouse_event.x + 10, + :, + ] = 0.0 elif mouse_event.flags == cv2.EVENT_FLAG_RBUTTON: - img[mouse_event.y - 5:mouse_event.y + 10, mouse_event.x - 5:mouse_event.x + 10, :] = 1.0 + img[ + mouse_event.y - 5 : mouse_event.y + 10, + mouse_event.x - 5 : mouse_event.x + 10, + :, + ] = 1.0 - VideoHandlerThread(video_source=img, callbacks=function_display_callback(conway)).display() + VideoHandlerThread( + video_source=img, callbacks=function_display_callback(conway) + ).display() def test_double_win(self): vid1 = np.ones((100, 100)) @@ -134,9 +142,9 @@ class TestSubWin(ut.TestCase): t2 = w.VideoHandlerThread(vid2) t1.start() t2.start() - SubscriberWindows(window_names=['cammy', 'cammy2'], - video_sources=[vid1, vid2] - ).loop() + SubscriberWindows( + window_names=["cammy", "cammy2"], video_sources=[vid1, vid2] + ).loop() t1.join() t1.join() diff --git a/tox.ini b/tox.ini index e25dc9a..87306ec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,41 @@ [tox] envlist = - py27, - py35, - py36, - pypy, - pypy3, + mypy, + pydocstyle, + pycodestyle [testenv] passenv = * -deps = - coverage - pytest +skipsdist = true +whitelist_externals = + mypy + pydocstyle + pycodestyle +#deps = +# coverage +# pytest +#commands = +# python setup.py --quiet clean develop +# coverage run --parallel-mode -m pytest +# coverage combine --append +# coverage report -m + +[testenv:mypy] commands = - python setup.py --quiet clean develop - coverage run --parallel-mode -m pytest - coverage combine --append - coverage report -m + mypy displayarray + +[pydocstyle] +inherit = false +ignore = D100,D203,D405,D105,D107,D212 + +[testenv:pydocstyle] +commands = + pydocstyle displayarray + +[pycodestyle] +max-line-length = 120 +statistics = True + +[testenv:pycodestyle] +commands = + pycodestyle displayarray \ No newline at end of file