From 767ebc4381db397253d8546a495f2d1d218d80cd Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 01:28:48 -0700 Subject: [PATCH 1/3] callbacks: Moved global_cv_display_callback to callbacks file. Added gpu-like function_display_callback. Removed need for wp.display_callback when using .display(). Added tests and examples. --- README.md | 21 +++++++++ cvpubsubs/webcam_pub/callbacks.py | 63 +++++++++++++++++++++++++++ cvpubsubs/webcam_pub/frame_handler.py | 37 ++++++++-------- cvpubsubs/window_sub/cv_window_sub.py | 2 +- tests/test_sub_win.py | 23 +++++++++- 5 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 cvpubsubs/webcam_pub/callbacks.py diff --git a/README.md b/README.md index 1426acb..b6366c7 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,27 @@ Python 2.7/3.5+ and PyPy. t1.join() t1.join() + +#### Run a function on each pixel + from cvpubsubs.webcam_pub import VideoHandlerThread + from cvpubsubs.webcam_pub.callbacks import function_display_callback + img = np.zeros((50, 50, 1)) + img[0:5, 0:5, :] = 1 + + def conway_game_of_life(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 = max(neighbors - np.sum(array[coords[0:2]]), 0.0) + if array[coords] == 1.0: + if neighbors < 2 or neighbors > 3: + array[coords] = 0.0 + elif 2 <= neighbors <= 3: + array[coords] = 1.0 + else: + if neighbors == 3: + array[coords] = 1.0 + + VideoHandlerThread(video_source=img, callbacks=function_display_callback(conway_game_of_life)).display() ## License diff --git a/cvpubsubs/webcam_pub/callbacks.py b/cvpubsubs/webcam_pub/callbacks.py new file mode 100644 index 0000000..3e47234 --- /dev/null +++ b/cvpubsubs/webcam_pub/callbacks.py @@ -0,0 +1,63 @@ +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): + 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: + :param finish_function: + """ + self.looping = True + self.first_call = True + + def _display_internal(self, frame, cam_id, *args, **kwargs): + finished = True + if self.first_call: + 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 + if not callable(finish_function): + WinCtrl.quit() + else: + finished = finish_function(frame, Ellipsis, finished, *args, **kwargs) + if finished: + WinCtrl.quit() + + self.inner_function = _display_internal + + def __call__(self, *args, **kwargs): + return self.inner_function(self, *args, **kwargs) diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index 2eb1372..1ca7c66 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -2,33 +2,24 @@ import threading import numpy as np -from .pub_cam import pub_cam_thread +from cvpubsubs.webcam_pub.pub_cam import pub_cam_thread from cvpubsubs.webcam_pub.camctrl import CamCtrl + if False: - from typing import Union, Tuple, Any, Callable, List + from typing import Union, Tuple, Any, Callable, List, Optional -from cvpubsubs.window_sub import SubscriberWindows + FrameCallable = Callable[[np.ndarray, int], Optional[np.ndarray]] - -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 +from cvpubsubs.webcam_pub.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] - callbacks=(global_cv_display_callback,), # type: List[Callable[[np.ndarray, int], Any]] + def __init__(self, video_source=0, # type: Union[int, str, np.ndarray] + callbacks=(global_cv_display_callback,), # type: Union[List[FrameCallable], FrameCallable] request_size=(-1, -1), # type: Tuple[int, int] high_speed=True, # type: bool fps_limit=240 # type: float @@ -55,7 +46,10 @@ class VideoHandlerThread(threading.Thread): raise TypeError( "Only strings or ints representing cameras, or numpy arrays representing pictures supported.") self.video_source = video_source - self.callbacks = 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 @@ -85,11 +79,18 @@ class VideoHandlerThread(threading.Thread): 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]] """ + if global_cv_display_callback not in self.callbacks: + if isinstance(self.callbacks, tuple): + self.callbacks = self.callbacks + (global_cv_display_callback,) + else: + self.callbacks.append(global_cv_display_callback) 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 da1533e..08b1562 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -4,7 +4,7 @@ import cv2 import numpy as np from .winctrl import WinCtrl -from ..webcam_pub.camctrl import CamCtrl +from cvpubsubs.webcam_pub.camctrl import CamCtrl from localpubsub import NoData if False: diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 7adf8a4..a2f478c 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -55,7 +55,7 @@ class TestSubWin(ut.TestCase): frame[:, :, 0] = 0 frame[:, :, 2] = 0 - w.VideoHandlerThread(callbacks=[redden_frame_print_spam] + w.display_callbacks).display() + w.VideoHandlerThread(callbacks=redden_frame_print_spam).display() def test_multi_cams_one_source(self): def cam_handler(frame, cam_id): @@ -103,3 +103,24 @@ class TestSubWin(ut.TestCase): ).loop() v.join() + + def test_conway_life(self): + from cvpubsubs.webcam_pub import VideoHandlerThread + from cvpubsubs.webcam_pub.callbacks import function_display_callback + 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 = max(neighbors - np.sum(array[coords[0:2]]), 0.0) + if array[coords] == 1.0: + if neighbors < 2 or neighbors > 3: + array[coords] = 0.0 + elif 2 <= neighbors <= 3: + array[coords] = 1.0 + else: + if neighbors == 3: + array[coords] = 1.0 + + VideoHandlerThread(video_source=img, callbacks=function_display_callback(conway)).display() From 82e81fc32b9e968ee2b6821aa6129dfdbb12a9b0 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 01:42:27 -0700 Subject: [PATCH 2/3] callbacks: Fixed some sonarlint problems. Added test for image with opencv scaling args. --- cvpubsubs/webcam_pub/callbacks.py | 18 +++++++++++------- cvpubsubs/webcam_pub/frame_handler.py | 1 - cvpubsubs/webcam_pub/np_cam.py | 20 ++++++++++++-------- cvpubsubs/webcam_pub/pub_cam.py | 1 - tests/test_sub_win.py | 4 ++++ 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cvpubsubs/webcam_pub/callbacks.py b/cvpubsubs/webcam_pub/callbacks.py index 3e47234..767aa0f 100644 --- a/cvpubsubs/webcam_pub/callbacks.py +++ b/cvpubsubs/webcam_pub/callbacks.py @@ -19,7 +19,7 @@ def global_cv_display_callback(frame, # type: np.ndarray SubscriberWindows.frame_dict[str(cam_id) + "frame"] = frame -class function_display_callback(object): +class function_display_callback(object): # NOSONAR def __init__(self, display_function, finish_function=None): """Used for running arbitrary functions on pixels. @@ -37,9 +37,18 @@ class function_display_callback(object): 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, cam_id, *args, **kwargs): finished = True if self.first_call: + # return to display initial frame self.first_call = False return if self.looping: @@ -50,12 +59,7 @@ class function_display_callback(object): it.iternext() if finished: self.looping = False - if not callable(finish_function): - WinCtrl.quit() - else: - finished = finish_function(frame, Ellipsis, finished, *args, **kwargs) - if finished: - WinCtrl.quit() + _run_finisher(self, frame, finished, *args, **kwargs) self.inner_function = _display_internal diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index 1ca7c66..efed326 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -65,7 +65,6 @@ class VideoHandlerThread(threading.Thread): while msg_owner != 'quit': frame = sub_cam.get(blocking=True, timeout=1.0) # type: np.ndarray if frame is not None: - frame = frame for c in self.callbacks: frame_c = c(frame, self.cam_id) if frame_c is not None: diff --git a/cvpubsubs/webcam_pub/np_cam.py b/cvpubsubs/webcam_pub/np_cam.py index cdd8e87..34517f8 100644 --- a/cvpubsubs/webcam_pub/np_cam.py +++ b/cvpubsubs/webcam_pub/np_cam.py @@ -14,6 +14,14 @@ class NpCam(object): self.__wait_for_ratio = False + def __handler_ratio(self): + if self.__width <= 0 or not isinstance(self.__width, int): + self.__width = int(self.__ratio * self.__height) + elif self.__height <= 0 or not isinstance(self.__height, int): + self.__height = int(self.__width / self.__ratio) + if self.__width > 0 and self.__height > 0: + self.__img = cv2.resize(self.__img, (self.__width, self.__height)) + def set(self, *args, **kwargs): if args[0] in [cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT]: self.__wait_for_ratio = not self.__wait_for_ratio @@ -22,21 +30,17 @@ class NpCam(object): else: self.__height = args[1] if not self.__wait_for_ratio: - if self.__width <= 0 or not isinstance(self.__width, int): - self.__width = int(self.__ratio * self.__height) - elif self.__height <= 0 or not isinstance(self.__height, int): - self.__height = int(self.__width / self.__ratio) - if self.__width>0 and self.__height>0: - self.__img = cv2.resize(self.__img, (self.__width, self.__height)) + self.__handler_ratio() - def get(self, *args, **kwargs): + @staticmethod + def get(*args, **kwargs): if args[0] == cv2.CAP_PROP_FRAME_COUNT: return float("inf") def read(self): return (True, self.__img) - def isOpened(self): + def isOpened(self): # NOSONAR return self.__is_opened def release(self): diff --git a/cvpubsubs/webcam_pub/pub_cam.py b/cvpubsubs/webcam_pub/pub_cam.py index bc85149..2808c4c 100644 --- a/cvpubsubs/webcam_pub/pub_cam.py +++ b/cvpubsubs/webcam_pub/pub_cam.py @@ -1,7 +1,6 @@ import threading import time - import cv2 import numpy as np diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index a2f478c..9fac5e6 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -40,6 +40,10 @@ class TestSubWin(ut.TestCase): img = np.random.uniform(0, 1, (300, 300, 3)) w.VideoHandlerThread(video_source=img).display() + def test_image_args(self): + img = np.random.uniform(0, 1, (30, 30, 3)) + w.VideoHandlerThread(video_source=img, request_size=(300, -1)).display() + def test_sub_with_args(self): video_thread = w.VideoHandlerThread(video_source=0, callbacks=w.display_callbacks, From c171895833878de99a50961606cf5bbc1cb9965d Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 01:43:09 -0700 Subject: [PATCH 3/3] callbacks: Grew minor due to added features. --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index f0ede3d..2b8877c 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.4.1' +__version__ = '0.5.0'