From 5b02b971cc110f1c756b98beae4ded686c491956 Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Sat, 27 Jan 2018 14:36:00 -0700 Subject: [PATCH 01/59] added license --- LICENSE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c3a8e42 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Josh Miklos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From d64d2145116eb13a1911ff41314b2db97bbfd213 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sat, 27 Jan 2018 14:40:45 -0700 Subject: [PATCH 02/59] updated download url --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9e2124d..206335b 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,6 @@ setup( author='Josh Miklos', author_email='Simulator.Leek@gmail.com', url='https://github.com/SimLeek/cv_pubsubs', - download_url='*', + download_url='https://github.com/SimLeek/cv_pubsubs/archive/0.1.tar.gz', keywords=['OpenCV', 'PubSub'] ) \ No newline at end of file From 149d15352b552538bc9a52d782d47d964d19aea7 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sat, 27 Jan 2018 18:37:21 -0700 Subject: [PATCH 03/59] Changed directory structure to support pip --- README.md | 2 +- README.txt | 1 + __init__.py | 4 ++-- cv_pubsubs/__init__.py | 0 .../cv_webcam_pub}/__init__.py | 0 .../cv_webcam_pub}/camctrl.py | 0 .../cv_webcam_pub}/frame_handler.py | 0 .../cv_webcam_pub}/get_cam_ids.py | 0 .../cv_webcam_pub}/listen_default.py | 0 .../cv_webcam_pub}/pub_cam.py | 0 cv_pubsubs/cv_window_sub/__init__.py | 1 + .../cv_window_sub/cv_window_sub.py | 4 +--- setup.py | 11 +++++++++-- 13 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 README.txt create mode 100644 cv_pubsubs/__init__.py rename {cv_webcam_pub => cv_pubsubs/cv_webcam_pub}/__init__.py (100%) rename {cv_webcam_pub => cv_pubsubs/cv_webcam_pub}/camctrl.py (100%) rename {cv_webcam_pub => cv_pubsubs/cv_webcam_pub}/frame_handler.py (100%) rename {cv_webcam_pub => cv_pubsubs/cv_webcam_pub}/get_cam_ids.py (100%) rename {cv_webcam_pub => cv_pubsubs/cv_webcam_pub}/listen_default.py (100%) rename {cv_webcam_pub => cv_pubsubs/cv_webcam_pub}/pub_cam.py (100%) create mode 100644 cv_pubsubs/cv_window_sub/__init__.py rename cv_window_sub.py => cv_pubsubs/cv_window_sub/cv_window_sub.py (93%) diff --git a/README.md b/README.md index ca64c6b..f539f9c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -A threaded PubSub OpenCV interface. Webcam and video feeds to multiple windows is supported. \ No newline at end of file +A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..f539f9c --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ +A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. \ No newline at end of file diff --git a/__init__.py b/__init__.py index 71dd419..3a5a3e6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -from .cv_webcam_pub import * -from .cv_window_sub import sub_win_loop, frame_dict +# redirection, so we can use subtree like pip +from .cv_pubsubs import cv_webcam_pub, cv_window_sub diff --git a/cv_pubsubs/__init__.py b/cv_pubsubs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cv_webcam_pub/__init__.py b/cv_pubsubs/cv_webcam_pub/__init__.py similarity index 100% rename from cv_webcam_pub/__init__.py rename to cv_pubsubs/cv_webcam_pub/__init__.py diff --git a/cv_webcam_pub/camctrl.py b/cv_pubsubs/cv_webcam_pub/camctrl.py similarity index 100% rename from cv_webcam_pub/camctrl.py rename to cv_pubsubs/cv_webcam_pub/camctrl.py diff --git a/cv_webcam_pub/frame_handler.py b/cv_pubsubs/cv_webcam_pub/frame_handler.py similarity index 100% rename from cv_webcam_pub/frame_handler.py rename to cv_pubsubs/cv_webcam_pub/frame_handler.py diff --git a/cv_webcam_pub/get_cam_ids.py b/cv_pubsubs/cv_webcam_pub/get_cam_ids.py similarity index 100% rename from cv_webcam_pub/get_cam_ids.py rename to cv_pubsubs/cv_webcam_pub/get_cam_ids.py diff --git a/cv_webcam_pub/listen_default.py b/cv_pubsubs/cv_webcam_pub/listen_default.py similarity index 100% rename from cv_webcam_pub/listen_default.py rename to cv_pubsubs/cv_webcam_pub/listen_default.py diff --git a/cv_webcam_pub/pub_cam.py b/cv_pubsubs/cv_webcam_pub/pub_cam.py similarity index 100% rename from cv_webcam_pub/pub_cam.py rename to cv_pubsubs/cv_webcam_pub/pub_cam.py diff --git a/cv_pubsubs/cv_window_sub/__init__.py b/cv_pubsubs/cv_window_sub/__init__.py new file mode 100644 index 0000000..0cab85a --- /dev/null +++ b/cv_pubsubs/cv_window_sub/__init__.py @@ -0,0 +1 @@ +from .cv_window_sub import sub_win_loop, frame_dict \ No newline at end of file diff --git a/cv_window_sub.py b/cv_pubsubs/cv_window_sub/cv_window_sub.py similarity index 93% rename from cv_window_sub.py rename to cv_pubsubs/cv_window_sub/cv_window_sub.py index e73054c..130656c 100644 --- a/cv_window_sub.py +++ b/cv_pubsubs/cv_window_sub/cv_window_sub.py @@ -1,10 +1,9 @@ import cv2 -from .cv_webcam_pub.camctrl import CamCtrl +from ..cv_webcam_pub.camctrl import CamCtrl if False: from typing import List -cvWindows = [] frame_dict = {} @@ -15,7 +14,6 @@ def sub_win_loop(*, callbacks=(None,), input_cams=(0,) ): - global cvWindows global frame_dict while True: diff --git a/setup.py b/setup.py index 206335b..77c1959 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,15 @@ setup( version='0.1', description='Pubsub interface for Python OpenCV', author='Josh Miklos', - author_email='Simulator.Leek@gmail.com', + author_email='simulatorleek@gmail.com', url='https://github.com/SimLeek/cv_pubsubs', download_url='https://github.com/SimLeek/cv_pubsubs/archive/0.1.tar.gz', - keywords=['OpenCV', 'PubSub'] + keywords=['OpenCV', 'PubSub'], + license='MIT', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + ] ) \ No newline at end of file From 113517967ec6b0d232d78d7b4fb4fbf567a5178e Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sat, 27 Jan 2018 18:59:26 -0700 Subject: [PATCH 04/59] modified package structure some more for pip --- __init__.py | 2 +- cv_pubsubs/{cv_webcam_pub => webcam_pub}/__init__.py | 0 cv_pubsubs/{cv_webcam_pub => webcam_pub}/camctrl.py | 0 cv_pubsubs/{cv_webcam_pub => webcam_pub}/frame_handler.py | 0 cv_pubsubs/{cv_webcam_pub => webcam_pub}/get_cam_ids.py | 0 cv_pubsubs/{cv_webcam_pub => webcam_pub}/listen_default.py | 0 cv_pubsubs/{cv_webcam_pub => webcam_pub}/pub_cam.py | 0 cv_pubsubs/{cv_window_sub => window_sub}/__init__.py | 0 cv_pubsubs/{cv_window_sub => window_sub}/cv_window_sub.py | 2 +- setup.py | 2 +- tests_interactive/test_pub_cam.py | 2 +- tests_interactive/test_sub_win.py | 4 ++-- 12 files changed, 6 insertions(+), 6 deletions(-) rename cv_pubsubs/{cv_webcam_pub => webcam_pub}/__init__.py (100%) rename cv_pubsubs/{cv_webcam_pub => webcam_pub}/camctrl.py (100%) rename cv_pubsubs/{cv_webcam_pub => webcam_pub}/frame_handler.py (100%) rename cv_pubsubs/{cv_webcam_pub => webcam_pub}/get_cam_ids.py (100%) rename cv_pubsubs/{cv_webcam_pub => webcam_pub}/listen_default.py (100%) rename cv_pubsubs/{cv_webcam_pub => webcam_pub}/pub_cam.py (100%) rename cv_pubsubs/{cv_window_sub => window_sub}/__init__.py (100%) rename cv_pubsubs/{cv_window_sub => window_sub}/cv_window_sub.py (96%) diff --git a/__init__.py b/__init__.py index 3a5a3e6..05bfaf5 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ # redirection, so we can use subtree like pip -from .cv_pubsubs import cv_webcam_pub, cv_window_sub +from .cv_pubsubs import webcam_pub, window_sub diff --git a/cv_pubsubs/cv_webcam_pub/__init__.py b/cv_pubsubs/webcam_pub/__init__.py similarity index 100% rename from cv_pubsubs/cv_webcam_pub/__init__.py rename to cv_pubsubs/webcam_pub/__init__.py diff --git a/cv_pubsubs/cv_webcam_pub/camctrl.py b/cv_pubsubs/webcam_pub/camctrl.py similarity index 100% rename from cv_pubsubs/cv_webcam_pub/camctrl.py rename to cv_pubsubs/webcam_pub/camctrl.py diff --git a/cv_pubsubs/cv_webcam_pub/frame_handler.py b/cv_pubsubs/webcam_pub/frame_handler.py similarity index 100% rename from cv_pubsubs/cv_webcam_pub/frame_handler.py rename to cv_pubsubs/webcam_pub/frame_handler.py diff --git a/cv_pubsubs/cv_webcam_pub/get_cam_ids.py b/cv_pubsubs/webcam_pub/get_cam_ids.py similarity index 100% rename from cv_pubsubs/cv_webcam_pub/get_cam_ids.py rename to cv_pubsubs/webcam_pub/get_cam_ids.py diff --git a/cv_pubsubs/cv_webcam_pub/listen_default.py b/cv_pubsubs/webcam_pub/listen_default.py similarity index 100% rename from cv_pubsubs/cv_webcam_pub/listen_default.py rename to cv_pubsubs/webcam_pub/listen_default.py diff --git a/cv_pubsubs/cv_webcam_pub/pub_cam.py b/cv_pubsubs/webcam_pub/pub_cam.py similarity index 100% rename from cv_pubsubs/cv_webcam_pub/pub_cam.py rename to cv_pubsubs/webcam_pub/pub_cam.py diff --git a/cv_pubsubs/cv_window_sub/__init__.py b/cv_pubsubs/window_sub/__init__.py similarity index 100% rename from cv_pubsubs/cv_window_sub/__init__.py rename to cv_pubsubs/window_sub/__init__.py diff --git a/cv_pubsubs/cv_window_sub/cv_window_sub.py b/cv_pubsubs/window_sub/cv_window_sub.py similarity index 96% rename from cv_pubsubs/cv_window_sub/cv_window_sub.py rename to cv_pubsubs/window_sub/cv_window_sub.py index 130656c..55b80d5 100644 --- a/cv_pubsubs/cv_window_sub/cv_window_sub.py +++ b/cv_pubsubs/window_sub/cv_window_sub.py @@ -1,5 +1,5 @@ import cv2 -from ..cv_webcam_pub.camctrl import CamCtrl +from ..webcam_pub.camctrl import CamCtrl if False: from typing import List diff --git a/setup.py b/setup.py index 77c1959..2217471 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from distutils.core import setup setup( name= 'cv_pubsubs', - packages = ['cv_webcam_pub', 'cv_window_sub'], + packages = ['cv_pubsubs', 'cv_pubsubs.webcam_pub', 'cv_pubsubs.window_sub'], version='0.1', description='Pubsub interface for Python OpenCV', author='Josh Miklos', diff --git a/tests_interactive/test_pub_cam.py b/tests_interactive/test_pub_cam.py index 62e7c93..4da4d45 100644 --- a/tests_interactive/test_pub_cam.py +++ b/tests_interactive/test_pub_cam.py @@ -1,4 +1,4 @@ -import cv_pubsubs.cv_webcam_pub as w +import cv_pubsubs.webcam_pub as w import unittest as ut diff --git a/tests_interactive/test_sub_win.py b/tests_interactive/test_sub_win.py index 497bfac..c64f3c4 100644 --- a/tests_interactive/test_sub_win.py +++ b/tests_interactive/test_sub_win.py @@ -1,6 +1,6 @@ import unittest as ut -import cv_pubsubs.cv_webcam_pub as w -from cv_pubsubs.cv_window_sub import frame_dict, sub_win_loop +import cv_pubsubs.webcam_pub as w +from cv_pubsubs.window_sub import frame_dict, sub_win_loop class TestSubWin(ut.TestCase): From 9f3063c1c49586c715b6e16fb5adadc96f7510ed Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sat, 27 Jan 2018 19:03:50 -0700 Subject: [PATCH 05/59] added relative imports for pip --- cv_pubsubs/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cv_pubsubs/__init__.py b/cv_pubsubs/__init__.py index e69de29..02df0f8 100644 --- a/cv_pubsubs/__init__.py +++ b/cv_pubsubs/__init__.py @@ -0,0 +1 @@ +from . import window_sub, webcam_pub From 5d7fd03656867baab76f8ec640ceabca3f516891 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 27 Mar 2018 11:07:43 -0700 Subject: [PATCH 06/59] ORB - added cam video looping, added python 2.7 compatibility. Made test view work off of video. Added pose cells and initial view cells. --- cv_pubsubs/webcam_pub/camctrl.py | 5 +++++ cv_pubsubs/webcam_pub/pub_cam.py | 7 ++++++- cv_pubsubs/window_sub/cv_window_sub.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cv_pubsubs/webcam_pub/camctrl.py b/cv_pubsubs/webcam_pub/camctrl.py index 349b16c..8fdff7d 100644 --- a/cv_pubsubs/webcam_pub/camctrl.py +++ b/cv_pubsubs/webcam_pub/camctrl.py @@ -9,3 +9,8 @@ class CamCtrl: def stop_cam(cam_id # type: Union[int, str] ): pubsub.publish("cvcamhandlers." + str(cam_id) + ".cmd", 'q') + + @staticmethod + def reset_vid(cam_id # type: Union[int, str] + ): + pubsub.publish("cvcamhandlers." + str(cam_id) + ".cmd", 'r') diff --git a/cv_pubsubs/webcam_pub/pub_cam.py b/cv_pubsubs/webcam_pub/pub_cam.py index 3511603..81a53f5 100644 --- a/cv_pubsubs/webcam_pub/pub_cam.py +++ b/cv_pubsubs/webcam_pub/pub_cam.py @@ -29,6 +29,7 @@ def pub_cam_loop(cam_id, # type: Union[int, str] msg = '' cam = cv2.VideoCapture(cam_id) # cam.set(cv2.CAP_PROP_CONVERT_RGB, 0) + frame_counter = 0 if high_speed: cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG) @@ -48,10 +49,14 @@ def pub_cam_loop(cam_id, # type: Union[int, str] cam.release() pubsub.publish("cvcams." + str(cam_id) + ".status", "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) pubsub.publish("cvcams." + str(cam_id) + ".vid", (frame,)) msg = listen_default(sub, block=False, empty='') - pass cam.release() return True diff --git a/cv_pubsubs/window_sub/cv_window_sub.py b/cv_pubsubs/window_sub/cv_window_sub.py index 55b80d5..49ac64b 100644 --- a/cv_pubsubs/window_sub/cv_window_sub.py +++ b/cv_pubsubs/window_sub/cv_window_sub.py @@ -8,7 +8,7 @@ frame_dict = {} # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 -def sub_win_loop(*, +def sub_win_loop( names, # type: List[str] input_vid_global_names, # type: List[str] callbacks=(None,), @@ -31,3 +31,4 @@ def sub_win_loop(*, for c in input_cams: CamCtrl.stop_cam(c) return + From 3e05ed05035527f9ba9f7830e81d31c9722c5f62 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 28 Mar 2018 06:30:01 -0700 Subject: [PATCH 07/59] ORB - Fixed crashing on blank images (need to report bug to OpenCV). Added view correlation class and test. --- cv_pubsubs/window_sub/cv_window_sub.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cv_pubsubs/window_sub/cv_window_sub.py b/cv_pubsubs/window_sub/cv_window_sub.py index 49ac64b..bc42703 100644 --- a/cv_pubsubs/window_sub/cv_window_sub.py +++ b/cv_pubsubs/window_sub/cv_window_sub.py @@ -25,6 +25,7 @@ def sub_win_loop( frames = frame_dict[input_vid_global_names[i]] for f in range(len(frames)): cv2.imshow(names[f % len(names)], frames[f]) + if cv2.waitKey(1) & 0xFF == ord('q'): for name in names: cv2.destroyWindow(name) From ba4c01cb52dd7684a231ab5c98f077012b46de5a Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 2 Apr 2018 18:24:53 -0700 Subject: [PATCH 08/59] Add initial rgc no_cl code, added 'press q to quit' to windows --- cv_pubsubs/window_sub/cv_window_sub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cv_pubsubs/window_sub/cv_window_sub.py b/cv_pubsubs/window_sub/cv_window_sub.py index bc42703..c1b2b6f 100644 --- a/cv_pubsubs/window_sub/cv_window_sub.py +++ b/cv_pubsubs/window_sub/cv_window_sub.py @@ -24,7 +24,7 @@ def sub_win_loop( else: frames = frame_dict[input_vid_global_names[i]] for f in range(len(frames)): - cv2.imshow(names[f % len(names)], frames[f]) + cv2.imshow(names[f % len(names)]+" (press q to quit)", frames[f]) if cv2.waitKey(1) & 0xFF == ord('q'): for name in names: From 746a9cfab0be742f3b47e8bffe67f46abf4df410 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 3 Apr 2018 19:39:18 -0700 Subject: [PATCH 09/59] I have added functionality of entering letters during runtime, they are displayed for you viewing pleasure. Additionally, I have began working making the item listed above be pubsub'd and there more useful to all of mankind --- cv_pubsubs/webcam_pub/camctrl.py | 8 ++++++- cv_pubsubs/window_sub/cv_window_sub.py | 31 +++++++++++++++++++++++++- tests_interactive/test_sub_win.py | 9 ++++++++ testt.py | 8 +++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 testt.py diff --git a/cv_pubsubs/webcam_pub/camctrl.py b/cv_pubsubs/webcam_pub/camctrl.py index 8fdff7d..541ac5c 100644 --- a/cv_pubsubs/webcam_pub/camctrl.py +++ b/cv_pubsubs/webcam_pub/camctrl.py @@ -1,4 +1,5 @@ import pubsub + if False: from typing import Union @@ -12,5 +13,10 @@ class CamCtrl: @staticmethod def reset_vid(cam_id # type: Union[int, str] - ): + ): pubsub.publish("cvcamhandlers." + str(cam_id) + ".cmd", 'r') + + @staticmethod + def key_stroke(key_entered): + pubsub.publish("cvKeyStroke.", key_entered) + diff --git a/cv_pubsubs/window_sub/cv_window_sub.py b/cv_pubsubs/window_sub/cv_window_sub.py index c1b2b6f..ec3b054 100644 --- a/cv_pubsubs/window_sub/cv_window_sub.py +++ b/cv_pubsubs/window_sub/cv_window_sub.py @@ -1,11 +1,24 @@ import cv2 from ..webcam_pub.camctrl import CamCtrl + if False: from typing import List frame_dict = {} +def triangle_seen(): + print("a triangle was seen") +def square_seen(): + print("a square was seen") +def nothing_seen(): + print("nothing was seen") + +command_dict = { + "t": triangle_seen, + "s": square_seen, + " ": nothing_seen +} # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 def sub_win_loop( @@ -25,11 +38,27 @@ def sub_win_loop( frames = frame_dict[input_vid_global_names[i]] for f in range(len(frames)): cv2.imshow(names[f % len(names)]+" (press q to quit)", frames[f]) + if cv2.getWindowProperty(names[f % len(names)]+" (press q to quit)", 0) != 0: + print("X was pressed") + for name in names: + cv2.destroyWindow(name) + for c in input_cams: + CamCtrl.stop_cam(c) - if cv2.waitKey(1) & 0xFF == ord('q'): + + key_criteria = cv2.waitKey(1) & 0xFF + + if key_criteria == ord("q"): for name in names: cv2.destroyWindow(name) for c in input_cams: CamCtrl.stop_cam(c) return + if chr(key_criteria) in command_dict: + command_dict[chr(key_criteria)]() + CamCtrl.key_stroke(chr(key_criteria)) + elif chr(key_criteria) != "ÿ": + print(chr(key_criteria)) + + diff --git a/tests_interactive/test_sub_win.py b/tests_interactive/test_sub_win.py index c64f3c4..e3d4960 100644 --- a/tests_interactive/test_sub_win.py +++ b/tests_interactive/test_sub_win.py @@ -2,6 +2,15 @@ import unittest as ut import cv_pubsubs.webcam_pub as w from cv_pubsubs.window_sub import frame_dict, sub_win_loop +def subscribe_to_key_command(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 class TestSubWin(ut.TestCase): diff --git a/testt.py b/testt.py new file mode 100644 index 0000000..eb39d42 --- /dev/null +++ b/testt.py @@ -0,0 +1,8 @@ +def hi(): + print("hi") + +if __name__ == '__main__': + dic = {} + dic['a'] = hi + + dic['a']() \ No newline at end of file From 15f14f8f5b41a8089f13b2ca0c0366184e49c356 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 8 Apr 2018 14:12:02 -0700 Subject: [PATCH 10/59] Updated major version due to breaking changes. Reformatted files and imports. Removed unused ctrl commands. Made all pubsub channels camel case. changed sub_win_loop to class. Removed implementation specific code. Removed unreliable 'x to quit' code. Made windows quit from pubsub. Added WinCtrl class for window specific commands. Added threaded interactive key press test. Removed testt file. --- cv_pubsubs/{webcam_pub => }/listen_default.py | 0 cv_pubsubs/webcam_pub/__init__.py | 5 +- cv_pubsubs/webcam_pub/camctrl.py | 12 +-- cv_pubsubs/webcam_pub/frame_handler.py | 16 +-- cv_pubsubs/webcam_pub/pub_cam.py | 28 +++--- cv_pubsubs/window_sub/__init__.py | 2 +- cv_pubsubs/window_sub/cv_window_sub.py | 98 ++++++++++--------- cv_pubsubs/window_sub/winctrl.py | 12 +++ setup.py | 7 +- tests_interactive/__init__.py | 0 tests_interactive/test_sub_win.py | 60 +++++++++--- testt.py | 8 -- 12 files changed, 143 insertions(+), 105 deletions(-) rename cv_pubsubs/{webcam_pub => }/listen_default.py (100%) create mode 100644 cv_pubsubs/window_sub/winctrl.py delete mode 100644 tests_interactive/__init__.py delete mode 100644 testt.py diff --git a/cv_pubsubs/webcam_pub/listen_default.py b/cv_pubsubs/listen_default.py similarity index 100% rename from cv_pubsubs/webcam_pub/listen_default.py rename to cv_pubsubs/listen_default.py diff --git a/cv_pubsubs/webcam_pub/__init__.py b/cv_pubsubs/webcam_pub/__init__.py index 8e41c0e..6fff6cb 100644 --- a/cv_pubsubs/webcam_pub/__init__.py +++ b/cv_pubsubs/webcam_pub/__init__.py @@ -1,5 +1,4 @@ -from .listen_default import listen_default +from .camctrl import CamCtrl +from .frame_handler import frame_handler_thread from .get_cam_ids import get_cam_ids from .pub_cam import pub_cam_thread -from .frame_handler import frame_handler_thread -from .camctrl import CamCtrl diff --git a/cv_pubsubs/webcam_pub/camctrl.py b/cv_pubsubs/webcam_pub/camctrl.py index 541ac5c..04efd32 100644 --- a/cv_pubsubs/webcam_pub/camctrl.py +++ b/cv_pubsubs/webcam_pub/camctrl.py @@ -9,14 +9,4 @@ class CamCtrl: @staticmethod def stop_cam(cam_id # type: Union[int, str] ): - pubsub.publish("cvcamhandlers." + str(cam_id) + ".cmd", 'q') - - @staticmethod - def reset_vid(cam_id # type: Union[int, str] - ): - pubsub.publish("cvcamhandlers." + str(cam_id) + ".cmd", 'r') - - @staticmethod - def key_stroke(key_entered): - pubsub.publish("cvKeyStroke.", key_entered) - + pubsub.publish("CVCamHandlers." + str(cam_id) + ".Cmd", 'quit') diff --git a/cv_pubsubs/webcam_pub/frame_handler.py b/cv_pubsubs/webcam_pub/frame_handler.py index 8af1f2c..a0a3cbb 100644 --- a/cv_pubsubs/webcam_pub/frame_handler.py +++ b/cv_pubsubs/webcam_pub/frame_handler.py @@ -1,7 +1,9 @@ -import pubsub -import numpy as np import threading -from .listen_default import listen_default + +import numpy as np +import pubsub + +from cv_pubsubs.listen_default import listen_default from .pub_cam import pub_cam_thread if False: @@ -15,16 +17,16 @@ def frame_handler_loop(cam_id, # type: Union[int, str] 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") + sub_cam = pubsub.subscribe("CVCams." + str(cam_id) + ".Vid") + sub_owner = pubsub.subscribe("CVCamHandlers." + str(cam_id) + ".Cmd") msg_owner = '' - while msg_owner != 'q': + 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", 'q') + pubsub.publish("CVCams." + str(cam_id) + ".Cmd", 'quit') t.join() diff --git a/cv_pubsubs/webcam_pub/pub_cam.py b/cv_pubsubs/webcam_pub/pub_cam.py index 81a53f5..91ce302 100644 --- a/cv_pubsubs/webcam_pub/pub_cam.py +++ b/cv_pubsubs/webcam_pub/pub_cam.py @@ -1,9 +1,11 @@ -import pubsub +import threading +import time + import cv2 import numpy as np -import time -import threading -from .listen_default import listen_default +import pubsub + +from cv_pubsubs.listen_default import listen_default if False: from typing import Union, Tuple @@ -14,9 +16,9 @@ def pub_cam_loop(cam_id, # type: Union[int, str] high_speed=False, # type: bool fps_limit=240 # type: float ): # type: (...)->bool - """Publishes whichever camera you select to cvcams..vid - You can send a quit command 'q' to cvcams..cmd - Status information, such as failure to open, will be posted to cvcams..status + """Publishes 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 @@ -25,7 +27,7 @@ def pub_cam_loop(cam_id, # type: Union[int, str] :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. """ - sub = pubsub.subscribe("cvcams." + str(cam_id) + ".cmd") + sub = pubsub.subscribe("CVCams." + str(cam_id) + ".Cmd") msg = '' cam = cv2.VideoCapture(cam_id) # cam.set(cv2.CAP_PROP_CONVERT_RGB, 0) @@ -38,23 +40,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(): - pubsub.publish("cvcams." + str(cam_id) + ".status", "failed") + pubsub.publish("CVCams." + str(cam_id) + ".Status", "failed") return False now = time.time() - while msg != 'q': + while msg != 'quit': time.sleep(1. / (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() - pubsub.publish("cvcams." + str(cam_id) + ".status", "failed") + pubsub.publish("CVCams." + str(cam_id) + ".Status", "failed") return False if cam.get(cv2.CAP_PROP_FRAME_COUNT) > 0: - frame_counter+=1 + frame_counter += 1 if frame_counter >= cam.get(cv2.CAP_PROP_FRAME_COUNT): frame_counter = 0 cam = cv2.VideoCapture(cam_id) - pubsub.publish("cvcams." + str(cam_id) + ".vid", (frame,)) + pubsub.publish("CVCams." + str(cam_id) + ".Vid", (frame,)) msg = listen_default(sub, block=False, empty='') cam.release() diff --git a/cv_pubsubs/window_sub/__init__.py b/cv_pubsubs/window_sub/__init__.py index 0cab85a..d7d6227 100644 --- a/cv_pubsubs/window_sub/__init__.py +++ b/cv_pubsubs/window_sub/__init__.py @@ -1 +1 @@ -from .cv_window_sub import sub_win_loop, frame_dict \ No newline at end of file +from .cv_window_sub import SubscriberWindows diff --git a/cv_pubsubs/window_sub/cv_window_sub.py b/cv_pubsubs/window_sub/cv_window_sub.py index ec3b054..725b15c 100644 --- a/cv_pubsubs/window_sub/cv_window_sub.py +++ b/cv_pubsubs/window_sub/cv_window_sub.py @@ -1,64 +1,66 @@ -import cv2 -from ..webcam_pub.camctrl import CamCtrl +import warnings +import cv2 +import pubsub + +from .winctrl import WinCtrl +from ..listen_default import listen_default +from ..webcam_pub.camctrl import CamCtrl if False: from typing import List -frame_dict = {} -def triangle_seen(): - print("a triangle was seen") -def square_seen(): - print("a square was seen") -def nothing_seen(): - print("nothing was seen") +class SubscriberWindows(object): + frame_dict = {} -command_dict = { - "t": triangle_seen, - "s": square_seen, - " ": nothing_seen -} + esc_key_codes = [27] # ESC key on most keyboards -# todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 -def sub_win_loop( - names, # type: List[str] + def __init__(self, + window_names, # type: List[str] input_vid_global_names, # type: List[str] callbacks=(None,), input_cams=(0,) ): - global frame_dict + self.window_names = window_names + self.input_vid_global_names = input_vid_global_names + self.callbacks = callbacks + self.input_cams = input_cams - while True: - for i in range(len(input_vid_global_names)): - if input_vid_global_names[i] in frame_dict and frame_dict[input_vid_global_names[i]] is not None: - if callbacks[i % len(callbacks)] is not None: - frames = callbacks[i % len(callbacks)](frame_dict[input_vid_global_names[i]]) + 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)") + for c in self.input_cams: + CamCtrl.stop_cam(c) + WinCtrl.quit() + elif key_input not in [-1, 0]: + try: + WinCtrl.key_stroke(chr(key_input)) + except ValueError: + warnings.warn( + RuntimeWarning("Unknown key code: [{}]. Please report to cv_pubsubs issue page.".format(key_input)) + ) + + def update_window_frames(self): + 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: + frames = self.callbacks[i % len(self.callbacks)](self.frame_dict[self.input_vid_global_names[i]]) else: - frames = frame_dict[input_vid_global_names[i]] + frames = self.frame_dict[self.input_vid_global_names[i]] for f in range(len(frames)): - cv2.imshow(names[f % len(names)]+" (press q to quit)", frames[f]) - if cv2.getWindowProperty(names[f % len(names)]+" (press q to quit)", 0) != 0: - print("X was pressed") - for name in names: - cv2.destroyWindow(name) - for c in input_cams: - CamCtrl.stop_cam(c) - - - key_criteria = cv2.waitKey(1) & 0xFF - - if key_criteria == ord("q"): - for name in names: - cv2.destroyWindow(name) - for c in input_cams: - CamCtrl.stop_cam(c) - return - - if chr(key_criteria) in command_dict: - command_dict[chr(key_criteria)]() - CamCtrl.key_stroke(chr(key_criteria)) - elif chr(key_criteria) != "ÿ": - print(chr(key_criteria)) - + cv2.imshow(self.window_names[f % len(self.window_names)] + " (press ESC to quit)", frames[f]) + # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 + def loop(self): + sub_cmd = pubsub.subscribe("CVWinCmd") + msg_cmd = '' + while msg_cmd != 'quit': + self.update_window_frames() + self.handle_keys(cv2.waitKey(1)) + msg_cmd = listen_default(sub_cmd, block=False, empty='') + pubsub.publish("CVWinCmd", 'quit') diff --git a/cv_pubsubs/window_sub/winctrl.py b/cv_pubsubs/window_sub/winctrl.py new file mode 100644 index 0000000..d971664 --- /dev/null +++ b/cv_pubsubs/window_sub/winctrl.py @@ -0,0 +1,12 @@ +import pubsub + + +class WinCtrl: + + @staticmethod + def key_stroke(key_entered): + pubsub.publish("CVKeyStroke", key_entered) + + @staticmethod + def quit(): + pubsub.publish("CVWinCmd", "quit") diff --git a/setup.py b/setup.py index 2217471..5cd3bfe 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ from distutils.core import setup +from setuptools import find_packages setup( name= 'cv_pubsubs', - packages = ['cv_pubsubs', 'cv_pubsubs.webcam_pub', 'cv_pubsubs.window_sub'], - version='0.1', + version='1.0.0', + packages = find_packages(), description='Pubsub interface for Python OpenCV', author='Josh Miklos', author_email='simulatorleek@gmail.com', @@ -16,5 +17,7 @@ setup( 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', ] ) \ No newline at end of file diff --git a/tests_interactive/__init__.py b/tests_interactive/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests_interactive/test_sub_win.py b/tests_interactive/test_sub_win.py index e3d4960..089e1ad 100644 --- a/tests_interactive/test_sub_win.py +++ b/tests_interactive/test_sub_win.py @@ -1,22 +1,36 @@ +import threading import unittest as ut -import cv_pubsubs.webcam_pub as w -from cv_pubsubs.window_sub import frame_dict, sub_win_loop -def subscribe_to_key_command(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)) +import pubsub + +import cv_pubsubs.webcam_pub as w +from cv_pubsubs.listen_default import listen_default +from cv_pubsubs.window_sub import SubscriberWindows + + +def print_keys_thread(): + sub_key = pubsub.subscribe("CVKeyStroke") + sub_cmd = pubsub.subscribe("CVWinCmd") + msg_cmd = '' + while msg_cmd != 'quit': + key_chr = listen_default(sub_key, timeout=.1) # type: np.ndarray + if key_chr is not None: + print("key pressed: " + str(key_chr)) + msg_cmd = listen_default(sub_cmd, block=False, empty='') + pubsub.publish("CVWinCmd", 'quit') + + +def start_print_keys_thread(): # type: (...) -> threading.Thread + t = threading.Thread(target=print_keys_thread, args=()) t.start() return t + class TestSubWin(ut.TestCase): def test_sub(self): def cam_handler(frame, cam_id): - frame_dict[str(cam_id) + "Frame"] = (frame, frame) + SubscriberWindows.frame_dict[str(cam_id) + "Frame"] = (frame, frame) t = w.frame_handler_thread(0, cam_handler, request_size=(1280, 720), @@ -24,9 +38,31 @@ class TestSubWin(ut.TestCase): fps_limit=240 ) - sub_win_loop(names=['cammy', 'cammy2'], - input_vid_global_names=[str(0) + "Frame"]) + SubscriberWindows(window_names=['cammy', 'cammy2'], + input_vid_global_names=[str(0) + "Frame"] + ).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) + + t = w.frame_handler_thread(0, cam_handler, + request_size=(1280, 720), + high_speed=True, + fps_limit=240 + ) + + kt = start_print_keys_thread() + + SubscriberWindows(window_names=['cammy', 'cammy2'], + input_vid_global_names=[str(0) + "Frame"] + ).loop() + + w.CamCtrl.stop_cam(0) + + t.join() + kt.join() diff --git a/testt.py b/testt.py deleted file mode 100644 index eb39d42..0000000 --- a/testt.py +++ /dev/null @@ -1,8 +0,0 @@ -def hi(): - print("hi") - -if __name__ == '__main__': - dic = {} - dic['a'] = hi - - dic['a']() \ No newline at end of file From 4d77770d9221d0f922a90c24f1d1b32f052945a2 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 8 Aug 2018 20:05:21 -0700 Subject: [PATCH 11/59] Changed to cvpubsubs. Initialized hatch. --- .coveragerc | 15 ++ .gitignore | 1 + LICENSE-APACHE | 178 ++++++++++++++++++ LICENSE-MIT | 21 +++ MANIFEST.in | 6 + README.md | 26 ++- __init__.py | 2 +- cv_pubsubs/__init__.py | 1 - cvpubsubs/__init__.py | 0 {cv_pubsubs => cvpubsubs}/listen_default.py | 0 .../webcam_pub/__init__.py | 0 .../webcam_pub/camctrl.py | 0 .../webcam_pub/frame_handler.py | 2 +- .../webcam_pub/get_cam_ids.py | 0 .../webcam_pub/pub_cam.py | 2 +- .../window_sub/__init__.py | 0 .../window_sub/cv_window_sub.py | 0 .../window_sub/winctrl.py | 0 pyproject.toml | 17 ++ requirements.txt | 3 +- setup.py | 65 +++++-- tests/__init__.py | 0 {tests_interactive => tests}/test_pub_cam.py | 2 +- {tests_interactive => tests}/test_sub_win.py | 6 +- tox.ini | 18 ++ 25 files changed, 336 insertions(+), 29 deletions(-) create mode 100644 .coveragerc create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 MANIFEST.in delete mode 100644 cv_pubsubs/__init__.py create mode 100644 cvpubsubs/__init__.py rename {cv_pubsubs => cvpubsubs}/listen_default.py (100%) rename {cv_pubsubs => cvpubsubs}/webcam_pub/__init__.py (100%) rename {cv_pubsubs => cvpubsubs}/webcam_pub/camctrl.py (100%) rename {cv_pubsubs => cvpubsubs}/webcam_pub/frame_handler.py (96%) rename {cv_pubsubs => cvpubsubs}/webcam_pub/get_cam_ids.py (100%) rename {cv_pubsubs => cvpubsubs}/webcam_pub/pub_cam.py (98%) rename {cv_pubsubs => cvpubsubs}/window_sub/__init__.py (100%) rename {cv_pubsubs => cvpubsubs}/window_sub/cv_window_sub.py (100%) rename {cv_pubsubs => cvpubsubs}/window_sub/winctrl.py (100%) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py rename {tests_interactive => tests}/test_pub_cam.py (93%) rename {tests_interactive => tests}/test_sub_win.py (93%) create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..fcae781 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,15 @@ +[run] +source = + cvpubsubs + tests +branch = True +omit = + cvpubsubs/cli.py + +[report] +exclude_lines = + no cov + no qa + noqa + pragma: no cover + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index 71d977d..3c71423 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ venv.bak/ .mypy_cache/ .idea/ +.pytest_cache/ diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..8a03bef --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright 2018 SimLeek + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..2acd40f --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +Copyright (c) 2018 SimLeek + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4b1e582 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.md +include README.rst +include README.txt +include LICENSE-APACHE +include LICENSE-MIT +include LICENSE.md diff --git a/README.md b/README.md index f539f9c..0152002 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. \ No newline at end of file +CVPubSubs +========= + +A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. + +Installation +------------ + +CVPubSubs 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 + +License +------- + +CVPubSubs is distributed under the terms of both + +- `MIT License `_ +- `Apache License, Version 2.0 `_ + +at your option. + + diff --git a/__init__.py b/__init__.py index 05bfaf5..b826e76 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ # redirection, so we can use subtree like pip -from .cv_pubsubs import webcam_pub, window_sub +from cvpubsubs import webcam_pub, window_sub diff --git a/cv_pubsubs/__init__.py b/cv_pubsubs/__init__.py deleted file mode 100644 index 02df0f8..0000000 --- a/cv_pubsubs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import window_sub, webcam_pub diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cv_pubsubs/listen_default.py b/cvpubsubs/listen_default.py similarity index 100% rename from cv_pubsubs/listen_default.py rename to cvpubsubs/listen_default.py diff --git a/cv_pubsubs/webcam_pub/__init__.py b/cvpubsubs/webcam_pub/__init__.py similarity index 100% rename from cv_pubsubs/webcam_pub/__init__.py rename to cvpubsubs/webcam_pub/__init__.py diff --git a/cv_pubsubs/webcam_pub/camctrl.py b/cvpubsubs/webcam_pub/camctrl.py similarity index 100% rename from cv_pubsubs/webcam_pub/camctrl.py rename to cvpubsubs/webcam_pub/camctrl.py diff --git a/cv_pubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py similarity index 96% rename from cv_pubsubs/webcam_pub/frame_handler.py rename to cvpubsubs/webcam_pub/frame_handler.py index a0a3cbb..89069fa 100644 --- a/cv_pubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -3,7 +3,7 @@ import threading import numpy as np import pubsub -from cv_pubsubs.listen_default import listen_default +from cvpubsubs.listen_default import listen_default from .pub_cam import pub_cam_thread if False: diff --git a/cv_pubsubs/webcam_pub/get_cam_ids.py b/cvpubsubs/webcam_pub/get_cam_ids.py similarity index 100% rename from cv_pubsubs/webcam_pub/get_cam_ids.py rename to cvpubsubs/webcam_pub/get_cam_ids.py diff --git a/cv_pubsubs/webcam_pub/pub_cam.py b/cvpubsubs/webcam_pub/pub_cam.py similarity index 98% rename from cv_pubsubs/webcam_pub/pub_cam.py rename to cvpubsubs/webcam_pub/pub_cam.py index 91ce302..59dccd5 100644 --- a/cv_pubsubs/webcam_pub/pub_cam.py +++ b/cvpubsubs/webcam_pub/pub_cam.py @@ -5,7 +5,7 @@ import cv2 import numpy as np import pubsub -from cv_pubsubs.listen_default import listen_default +from cvpubsubs.listen_default import listen_default if False: from typing import Union, Tuple diff --git a/cv_pubsubs/window_sub/__init__.py b/cvpubsubs/window_sub/__init__.py similarity index 100% rename from cv_pubsubs/window_sub/__init__.py rename to cvpubsubs/window_sub/__init__.py diff --git a/cv_pubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py similarity index 100% rename from cv_pubsubs/window_sub/cv_window_sub.py rename to cvpubsubs/window_sub/cv_window_sub.py diff --git a/cv_pubsubs/window_sub/winctrl.py b/cvpubsubs/window_sub/winctrl.py similarity index 100% rename from cv_pubsubs/window_sub/winctrl.py rename to cvpubsubs/window_sub/winctrl.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c5cab61 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[metadata] +name = 'CVPubSubs' +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/_/CVPubSubs' + +[requires] +python_version = ['2.7', '3.5', '3.6', 'pypy', 'pypy3'] + +[build-system] +requires = ['setuptools', 'wheel'] + +[tool.hatch.commands] +prerelease = 'hatch build' diff --git a/requirements.txt b/requirements.txt index 3d37beb..08f0fc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -opencv-python pubsub -numpy \ No newline at end of file +opencv-python \ No newline at end of file diff --git a/setup.py b/setup.py index 5cd3bfe..9e82fc6 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,52 @@ -from distutils.core import setup -from setuptools import find_packages +from io import open + +from setuptools import find_packages, setup + +with open('cvpubsubs/__init__.py', 'r') as f: + for line in f: + if line.startswith('__version__'): + version = line.strip().split('=')[1].strip(' \'"') + break + else: + version = '0.0.1' + +with open('README.md', 'r', encoding='utf-8') as f: + readme = f.read() + +REQUIRES = [] setup( - name= 'cv_pubsubs', - version='1.0.0', - packages = find_packages(), - description='Pubsub interface for Python OpenCV', - author='Josh Miklos', - author_email='simulatorleek@gmail.com', - url='https://github.com/SimLeek/cv_pubsubs', - download_url='https://github.com/SimLeek/cv_pubsubs/archive/0.1.tar.gz', - keywords=['OpenCV', 'PubSub'], - license='MIT', + name='CVPubSubs', + version=version, + description='', + long_description=readme, + author='SimLeek', + author_email='josh.miklos@gmail.com', + maintainer='SimLeek', + maintainer_email='josh.miklos@gmail.com', + url='https://github.com/_/CVPubSubs', + license='MIT/Apache-2.0', + + keywords=[ + '', + ], + classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 2', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: English', + 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', - ] -) \ No newline at end of file + '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'], + + packages=find_packages(), +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests_interactive/test_pub_cam.py b/tests/test_pub_cam.py similarity index 93% rename from tests_interactive/test_pub_cam.py rename to tests/test_pub_cam.py index 4da4d45..e2f6e9f 100644 --- a/tests_interactive/test_pub_cam.py +++ b/tests/test_pub_cam.py @@ -1,4 +1,4 @@ -import cv_pubsubs.webcam_pub as w +import cvpubsubs.webcam_pub as w import unittest as ut diff --git a/tests_interactive/test_sub_win.py b/tests/test_sub_win.py similarity index 93% rename from tests_interactive/test_sub_win.py rename to tests/test_sub_win.py index 089e1ad..a54a5ba 100644 --- a/tests_interactive/test_sub_win.py +++ b/tests/test_sub_win.py @@ -3,9 +3,9 @@ import unittest as ut import pubsub -import cv_pubsubs.webcam_pub as w -from cv_pubsubs.listen_default import listen_default -from cv_pubsubs.window_sub import SubscriberWindows +import cvpubsubs.webcam_pub as w +from cvpubsubs.listen_default import listen_default +from cvpubsubs.window_sub import SubscriberWindows def print_keys_thread(): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e25dc9a --- /dev/null +++ b/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = + py27, + py35, + py36, + pypy, + pypy3, + +[testenv] +passenv = * +deps = + coverage + pytest +commands = + python setup.py --quiet clean develop + coverage run --parallel-mode -m pytest + coverage combine --append + coverage report -m From e98e897bac1b20428b9930f4ac25ca607bbee61a Mon Sep 17 00:00:00 2001 From: SimLeek Date: Thu, 9 Aug 2018 21:48:12 -0700 Subject: [PATCH 12/59] Changed frame_handler_thread to VideoHandlerThread class. Added display_callbacks. Added frame_handler documentation. Added display function. Added set_global_frames_dict. Update VideoHandlerThread to use callback list. Added tests. Used tests as examples in readme. Updated pypi readme. --- README.md | 70 +++++++++++++++++++ cvpubsubs/__init__.py | 1 + cvpubsubs/webcam_pub/__init__.py | 2 +- cvpubsubs/webcam_pub/frame_handler.py | 99 +++++++++++++++++++-------- cvpubsubs/window_sub/cv_window_sub.py | 33 ++++++--- setup.py | 1 + tests/test_pub_cam.py | 2 +- tests/test_sub_win.py | 67 +++++++++++------- 8 files changed, 209 insertions(+), 66 deletions(-) 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() From 07eb14163a053dc8d34304f48d9f95e6e6b97d12 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Thu, 9 Aug 2018 21:53:30 -0700 Subject: [PATCH 13/59] updated readme to consistent format. --- README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 451c81f..c2409f7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -CVPubSubs -========= +#CVPubSubs A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. -Installation ------------- +##Installation CVPubSubs is distributed on `PyPI `_ as a universal wheel and is available on Linux/macOS and Windows and supports @@ -12,8 +10,7 @@ Python 2.7/3.5+ and PyPy. $ pip install CVPubSubs -Usage ------------ +##Usage ###Video Editing and Publishing @@ -82,8 +79,7 @@ Usage t1.join() t1.join() -License -------- +##License CVPubSubs is distributed under the terms of both From 25c17526af6f1cac80a4770de8f060dd9fd86b4d Mon Sep 17 00:00:00 2001 From: SimLeek Date: Thu, 9 Aug 2018 21:55:03 -0700 Subject: [PATCH 14/59] fixed readme titles. Fixed pypi to git site link. --- README.md | 20 ++++++++++---------- setup.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c2409f7..ce013de 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -#CVPubSubs +# CVPubSubs A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported. -##Installation +## Installation CVPubSubs is distributed on `PyPI `_ as a universal wheel and is available on Linux/macOS and Windows and supports @@ -10,16 +10,16 @@ Python 2.7/3.5+ and PyPy. $ pip install CVPubSubs -##Usage +## Usage -###Video Editing and Publishing +### Video Editing and Publishing -####Display your webcam +#### Display your webcam import cvpubsubs.webcam_pub as w w.VideoHandlerThread().display() -####Change Display Arguments +#### Change Display Arguments import cvpubsubs.webcam_pub as w video_thread = w.VideoHandlerThread(video_source=0, @@ -31,7 +31,7 @@ Python 2.7/3.5+ and PyPy. video_thread.display() -####Run your own functions on the frames +#### Run your own functions on the frames import cvpubsubs.webcam_pub as w def redden_frame_print_spam(frame, cam_id): @@ -41,7 +41,7 @@ Python 2.7/3.5+ and PyPy. w.VideoHandlerThread(callbacks=[redden_frame_print_spam] + w.display_callbacks).display() -####Display multiple windows from one source +#### Display multiple windows from one source import cvpubsubs.webcam_pub as w from cvpubsubs.window_sub import SubscriberWindows @@ -62,7 +62,7 @@ Python 2.7/3.5+ and PyPy. t.join() -####Display multiple windows from multiple sources +#### Display multiple windows from multiple sources iport cvpubsubs.webcam_pub as w from cvpubsubs.window_sub import SubscriberWindows @@ -79,7 +79,7 @@ Python 2.7/3.5+ and PyPy. t1.join() t1.join() -##License +## License CVPubSubs is distributed under the terms of both diff --git a/setup.py b/setup.py index 5b1ed0c..15f066f 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( author_email='josh.miklos@gmail.com', maintainer='SimLeek', maintainer_email='josh.miklos@gmail.com', - url='https://github.com/_/CVPubSubs', + url='https://github.com/SimLeek/CV_PubSubs', license='MIT/Apache-2.0', keywords=[ From 5de0a4c19d9786967ac480583ae90b523e4ebd2f Mon Sep 17 00:00:00 2001 From: SimLeek Date: Thu, 15 Nov 2018 16:25:20 -0700 Subject: [PATCH 15/59] Updated requirements and version --- cvpubsubs/__init__.py | 2 +- pyproject.toml | 2 +- requirements.txt | 3 ++- setup.py | 2 +- tests/test_sub_win.py | 7 +++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 7fd229a..fc79d63 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.2.0' +__version__ = '0.2.1' diff --git a/pyproject.toml b/pyproject.toml index c5cab61..f0cca88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ 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/_/CVPubSubs' +url = 'https://github.com/simleek/CVPubSubs' [requires] python_version = ['2.7', '3.5', '3.6', 'pypy', 'pypy3'] diff --git a/requirements.txt b/requirements.txt index 08f0fc3..d04d9fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pubsub -opencv-python \ No newline at end of file +numpy +opencv_python \ No newline at end of file diff --git a/setup.py b/setup.py index 15f066f..77354bd 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ with open('cvpubsubs/__init__.py', 'r') as f: with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() -REQUIRES = [] +REQUIRES = ['pubsub', 'numpy', 'opencv_python'] setup( name='CVPubSubs', diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 6582ff9..0645a1b 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -47,8 +47,7 @@ class TestSubWin(ut.TestCase): def test_sub_with_callback(self): def redden_frame_print_spam(frame, cam_id): frame[:, :, 0] = 0 - frame[:, :, 1] = 0 - print("Spam!") + frame[:, :, 2] = 0 w.VideoHandlerThread(callbacks=[redden_frame_print_spam] + w.display_callbacks).display() @@ -71,8 +70,8 @@ class TestSubWin(ut.TestCase): t.join() def test_multi_cams_multi_source(self): - t1 = w.VideoHandlerThread(0) - t2 = w.VideoHandlerThread(1) + t1 = w.VideoHandlerThread(0, request_size=(1920,1080)) + t2 = w.VideoHandlerThread(1, request_size=(1920,1080)) t1.start() t2.start() From 2e8ce6c26a5c986b19d5ad0386cd4d0e8410b714 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Thu, 15 Nov 2018 16:35:57 -0700 Subject: [PATCH 16/59] Updated fix version. --- MANIFEST.in | 1 - cvpubsubs/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4b1e582..7d1c81b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include README.md -include README.rst include README.txt include LICENSE-APACHE include LICENSE-MIT diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index fc79d63..020ed73 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.2.1' +__version__ = '0.2.2' diff --git a/setup.py b/setup.py index 77354bd..66e0580 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( license='MIT/Apache-2.0', keywords=[ - '', + 'opencv', 'camera', ], classifiers=[ From d5dbab7fd87780855d9d94730c119c75cd10b692 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 21 Jan 2019 22:04:30 -0700 Subject: [PATCH 17/59] Updated minor version. Added ability to display numpy arrays as still videos. Added test. --- cvpubsubs/__init__.py | 2 +- cvpubsubs/webcam_pub/__init__.py | 1 + cvpubsubs/webcam_pub/camctrl.py | 1 + cvpubsubs/webcam_pub/frame_handler.py | 17 +++++++---- cvpubsubs/webcam_pub/np_cam.py | 43 +++++++++++++++++++++++++++ cvpubsubs/webcam_pub/pub_cam.py | 23 ++++++++++---- cvpubsubs/window_sub/cv_window_sub.py | 33 +++++++++++++++----- tests/test_sub_win.py | 11 +++++-- 8 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 cvpubsubs/webcam_pub/np_cam.py diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 020ed73..0404d81 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.2.2' +__version__ = '0.3.0' diff --git a/cvpubsubs/webcam_pub/__init__.py b/cvpubsubs/webcam_pub/__init__.py index 7392a0f..c3b2eb5 100644 --- a/cvpubsubs/webcam_pub/__init__.py +++ b/cvpubsubs/webcam_pub/__init__.py @@ -2,3 +2,4 @@ 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 index 04efd32..289ebb0 100644 --- a/cvpubsubs/webcam_pub/camctrl.py +++ b/cvpubsubs/webcam_pub/camctrl.py @@ -10,3 +10,4 @@ class CamCtrl: def stop_cam(cam_id # type: Union[int, str] ): pubsub.publish("CVCamHandlers." + str(cam_id) + ".Cmd", 'quit') + pubsub.publish("CVCams." + str(cam_id) + ".Cmd", 'quit') diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index 826b7ca..e2a3b4f 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -5,7 +5,7 @@ import pubsub from cvpubsubs.listen_default import listen_default from .pub_cam import pub_cam_thread - +from cvpubsubs.webcam_pub.camctrl import CamCtrl if False: from typing import Union, Tuple, Any, Callable, List @@ -31,7 +31,7 @@ class VideoHandlerThread(threading.Thread): 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] + request_size=(-1, -1), # type: Tuple[int, int] high_speed=True, # type: bool fps_limit=240 # type: float ): @@ -49,7 +49,14 @@ class VideoHandlerThread(threading.Thread): :type fps_limit: float """ super(VideoHandlerThread, self).__init__(target=self.loop, args=()) - self.cam_id = video_source + if isinstance(video_source, (int, str)): + self.cam_id = str(video_source) + elif isinstance(video_source, np.ndarray): + self.cam_id = str(hash(str(video_source))) + else: + raise TypeError( + "Only strings or ints representing cameras, or numpy arrays representing pictures supported.") + self.video_source = video_source self.callbacks = callbacks self.request_size = request_size self.high_speed = high_speed @@ -57,7 +64,7 @@ class VideoHandlerThread(threading.Thread): 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) + t = pub_cam_thread(self.video_source, 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 = '' @@ -68,7 +75,7 @@ class VideoHandlerThread(threading.Thread): 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') + CamCtrl.stop_cam(self.cam_id) t.join() def display(self, diff --git a/cvpubsubs/webcam_pub/np_cam.py b/cvpubsubs/webcam_pub/np_cam.py new file mode 100644 index 0000000..cdd8e87 --- /dev/null +++ b/cvpubsubs/webcam_pub/np_cam.py @@ -0,0 +1,43 @@ +import numpy as np +import cv2 + + +class NpCam(object): + def __init__(self, img): + assert isinstance(img, np.ndarray) + self.__img = img + self.__is_opened = True + + self.__width = self.__img.shape[1] + self.__height = self.__img.shape[0] + self.__ratio = float(self.__width) / self.__height + + self.__wait_for_ratio = False + + 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 + if args[0] == cv2.CAP_PROP_FRAME_WIDTH: + self.__width = args[1] + 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)) + + def get(self, *args, **kwargs): + if args[0] == cv2.CAP_PROP_FRAME_COUNT: + return float("inf") + + def read(self): + return (True, self.__img) + + def isOpened(self): + return self.__is_opened + + def release(self): + self.__is_opened = False diff --git a/cvpubsubs/webcam_pub/pub_cam.py b/cvpubsubs/webcam_pub/pub_cam.py index 59dccd5..3e4718e 100644 --- a/cvpubsubs/webcam_pub/pub_cam.py +++ b/cvpubsubs/webcam_pub/pub_cam.py @@ -6,6 +6,8 @@ import numpy as np import pubsub from cvpubsubs.listen_default import listen_default +from cvpubsubs.webcam_pub.camctrl import CamCtrl +from .np_cam import NpCam if False: from typing import Union, Tuple @@ -27,12 +29,21 @@ def pub_cam_loop(cam_id, # type: Union[int, str] :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. """ - sub = pubsub.subscribe("CVCams." + str(cam_id) + ".Cmd") - msg = '' - cam = cv2.VideoCapture(cam_id) + + if isinstance(cam_id, (int, str)): + cam = cv2.VideoCapture(cam_id) + name = str(cam_id) + elif isinstance(cam_id, np.ndarray): + cam = NpCam(cam_id) # type: NpCam + name = str(hash(str(cam_id))) + else: + raise TypeError("Only strings or ints representing cameras, or numpy arrays representing pictures supported.") # cam.set(cv2.CAP_PROP_CONVERT_RGB, 0) frame_counter = 0 + sub = pubsub.subscribe("CVCams." + str(name) + ".Cmd") + msg = '' + if high_speed: cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG) @@ -40,7 +51,7 @@ def pub_cam_loop(cam_id, # type: Union[int, str] cam.set(cv2.CAP_PROP_FRAME_HEIGHT, request_size[1]) if not cam.isOpened(): - pubsub.publish("CVCams." + str(cam_id) + ".Status", "failed") + pubsub.publish("CVCams." + name + ".Status", "failed") return False now = time.time() while msg != 'quit': @@ -49,14 +60,14 @@ def pub_cam_loop(cam_id, # type: Union[int, str] (ret, frame) = cam.read() # type: Tuple[bool, np.ndarray ] if ret is False or not isinstance(frame, np.ndarray): cam.release() - pubsub.publish("CVCams." + str(cam_id) + ".Status", "failed") + pubsub.publish("CVCams." + name + ".Status", "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) - pubsub.publish("CVCams." + str(cam_id) + ".Vid", (frame,)) + pubsub.publish("CVCams." + name + ".Vid", (frame,)) msg = listen_default(sub, block=False, empty='') cam.release() diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 8566eb9..157d842 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -2,6 +2,7 @@ import warnings import cv2 import pubsub +import numpy as np from .winctrl import WinCtrl from ..listen_default import listen_default @@ -23,17 +24,33 @@ class SubscriberWindows(object): callbacks=(None,), # type: List[Callable[[List[np.ndarray]], Any]] ): self.window_names = window_names - self.input_vid_global_names = [str(name) + "frame" for name in video_sources] + self.source_names = [] + for name in video_sources: + if len(str(name))<=1000: + self.source_names.append(str(name)) + self.input_vid_global_names = [str(name) + "frame" for name in video_sources] + elif isinstance(name, np.ndarray): + self.source_names.append(str(hash(str(name)))) + self.input_vid_global_names = [str(hash(str(name))) + "frame" for name in video_sources] + else: + raise ValueError("Input window name too long.") + self.callbacks = callbacks self.input_cams = video_sources @staticmethod def set_global_frame_dict(name, *args): - SubscriberWindows.frame_dict[str(name)+"frame"] = [*args] + if len(str(name)) <= 1000: + SubscriberWindows.frame_dict[str(name) + "frame"] = [*args] + elif isinstance(name, np.ndarray): + SubscriberWindows.frame_dict[str(hash(str(name))) + "frame"] = [*args] + else: + raise ValueError("Input window name too long.") + def __stop_all_cams(self): - for c in self.input_cams: + for c in self.source_names: CamCtrl.stop_cam(c) def handle_keys(self, @@ -42,8 +59,9 @@ class SubscriberWindows(object): if key_input in self.esc_key_codes: for name in self.window_names: cv2.destroyWindow(name + " (press ESC to quit)") - self.__stop_all_cams() WinCtrl.quit() + self.__stop_all_cams() + return 'quit' elif key_input not in [-1, 0]: try: WinCtrl.key_stroke(chr(key_input)) @@ -71,7 +89,8 @@ class SubscriberWindows(object): msg_cmd = '' while msg_cmd != 'quit': self.update_window_frames() - self.handle_keys(cv2.waitKey(1)) - msg_cmd = listen_default(sub_cmd, block=False, empty='') - pubsub.publish("CVWinCmd", 'quit') + msg_cmd = self.handle_keys(cv2.waitKey(1)) + if not msg_cmd: + msg_cmd = listen_default(sub_cmd, block=False, empty='') + WinCtrl.quit() self.__stop_all_cams() diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 0645a1b..efe732d 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -2,6 +2,7 @@ import threading import unittest as ut import pubsub +import numpy as np import cvpubsubs.webcam_pub as w from cvpubsubs.listen_default import listen_default @@ -34,6 +35,10 @@ class TestSubWin(ut.TestCase): def test_sub(self): w.VideoHandlerThread().display() + def test_image(self): + img = np.random.uniform(0, 1, (300, 300, 3)) + w.VideoHandlerThread(video_source=img).display() + def test_sub_with_args(self): video_thread = w.VideoHandlerThread(video_source=0, callbacks=w.display_callbacks, @@ -70,14 +75,14 @@ class TestSubWin(ut.TestCase): t.join() def test_multi_cams_multi_source(self): - t1 = w.VideoHandlerThread(0, request_size=(1920,1080)) - t2 = w.VideoHandlerThread(1, request_size=(1920,1080)) + t1 = w.VideoHandlerThread(0, request_size=(1920, 1080)) + t2 = w.VideoHandlerThread(1, request_size=(1920, 1080)) t1.start() t2.start() SubscriberWindows(window_names=['cammy', 'cammy2'], - video_sources=[0,1] + video_sources=[0, 1] ).loop() t1.join() From 8d7799665bf5a5374095f73b08d69410b8c65d41 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 21 Jan 2019 22:18:51 -0700 Subject: [PATCH 18/59] fixed obvious bug. How the heck did this get here anyway? --- cvpubsubs/listen_default.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cvpubsubs/listen_default.py b/cvpubsubs/listen_default.py index 3ec99e0..31a050d 100644 --- a/cvpubsubs/listen_default.py +++ b/cvpubsubs/listen_default.py @@ -1,3 +1,4 @@ +import queue if False: from typing import Any, Optional, queue From b1ad31cc6b664cf0c71ae55f6afe6b585f33ef73 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 21 Jan 2019 22:19:12 -0700 Subject: [PATCH 19/59] grew fix. --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 0404d81..e1424ed 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.3.0' +__version__ = '0.3.1' From b7baf00a37672c8d2710b382f382fbe6da65555f Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 22 Jan 2019 21:58:49 -0700 Subject: [PATCH 20/59] Switched from pubsubs to my own localpubsub library. Set to specific version. --- cvpubsubs/listen_default.py | 19 -------- cvpubsubs/webcam_pub/camctrl.py | 65 ++++++++++++++++++++++++--- cvpubsubs/webcam_pub/frame_handler.py | 22 ++++----- cvpubsubs/webcam_pub/pub_cam.py | 27 +++++++---- cvpubsubs/window_sub/cv_window_sub.py | 32 ++++++------- cvpubsubs/window_sub/winctrl.py | 17 ++++--- requirements.txt | 4 +- setup.py | 3 +- tests/test_sub_win.py | 16 ++++--- 9 files changed, 131 insertions(+), 74 deletions(-) delete mode 100644 cvpubsubs/listen_default.py diff --git a/cvpubsubs/listen_default.py b/cvpubsubs/listen_default.py deleted file mode 100644 index 31a050d..0000000 --- a/cvpubsubs/listen_default.py +++ /dev/null @@ -1,19 +0,0 @@ -import queue -if False: - from typing import Any, Optional, queue - - -def listen_default(sub, # type: queue - block=True, # type: bool - timeout=None, # type: Optional[float] - empty=None # type: Any - ): # type: (...)->Any - try: - msg = (sub.listen(block=block, timeout=timeout)) - try: - msg = next(msg)['data'] - except StopIteration: - msg = empty - except queue.Empty: - msg = empty - return msg diff --git a/cvpubsubs/webcam_pub/camctrl.py b/cvpubsubs/webcam_pub/camctrl.py index 289ebb0..dee6ea6 100644 --- a/cvpubsubs/webcam_pub/camctrl.py +++ b/cvpubsubs/webcam_pub/camctrl.py @@ -1,13 +1,68 @@ -import pubsub +from threading import Lock +from localpubsub import VariablePub, VariableSub if False: - from typing import Union + from typing import Union, Dict -class CamCtrl: +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] ): - pubsub.publish("CVCamHandlers." + str(cam_id) + ".Cmd", 'quit') - pubsub.publish("CVCams." + str(cam_id) + ".Cmd", 'quit') + 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 index e2a3b4f..87a61c6 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -1,9 +1,7 @@ import threading import numpy as np -import pubsub -from cvpubsubs.listen_default import listen_default from .pub_cam import pub_cam_thread from cvpubsubs.webcam_pub.camctrl import CamCtrl if False: @@ -22,7 +20,7 @@ def global_cv_display_callback(frame, # type: np.ndarray :param cam_id: The video or image source :type cam_id: Union[int, str] """ - SubscriberWindows.frame_dict[str(cam_id) + "frame"] = (frame,) + SubscriberWindows.frame_dict[str(cam_id) + "frame"] = frame display_callbacks = [global_cv_display_callback] @@ -65,16 +63,20 @@ class VideoHandlerThread(threading.Thread): 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) - sub_cam = pubsub.subscribe("CVCams." + str(self.cam_id) + ".Vid") - sub_owner = pubsub.subscribe("CVCamHandlers." + str(self.cam_id) + ".Cmd") - msg_owner = '' + 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 = listen_default(sub_cam, timeout=.1) # type: np.ndarray + frame = sub_cam.get(blocking=True, timeout=1.0) # type: np.ndarray if frame is not None: - frame = frame[0] + frame = frame for c in self.callbacks: - c(frame, self.cam_id) - msg_owner = listen_default(sub_owner, block=False, empty='') + frame = c(frame, self.cam_id) + msg_owner = sub_owner.get() + sub_owner.release() + sub_cam.release() CamCtrl.stop_cam(self.cam_id) t.join() diff --git a/cvpubsubs/webcam_pub/pub_cam.py b/cvpubsubs/webcam_pub/pub_cam.py index 3e4718e..bc85149 100644 --- a/cvpubsubs/webcam_pub/pub_cam.py +++ b/cvpubsubs/webcam_pub/pub_cam.py @@ -1,11 +1,10 @@ import threading import time + import cv2 import numpy as np -import pubsub -from cvpubsubs.listen_default import listen_default from cvpubsubs.webcam_pub.camctrl import CamCtrl from .np_cam import NpCam @@ -31,17 +30,26 @@ def pub_cam_loop(cam_id, # type: Union[int, str] """ if isinstance(cam_id, (int, str)): - cam = cv2.VideoCapture(cam_id) name = str(cam_id) elif isinstance(cam_id, np.ndarray): - cam = NpCam(cam_id) # type: NpCam name = str(hash(str(cam_id))) else: raise TypeError("Only strings or ints representing cameras, or numpy arrays representing pictures supported.") + + if isinstance(cam_id, (int, str)): + cam = cv2.VideoCapture(cam_id) + elif isinstance(cam_id, np.ndarray): + cam = NpCam(cam_id) # type: NpCam + else: + raise TypeError("Only strings or ints representing cameras, or numpy arrays representing pictures supported.") + + CamCtrl.register_cam(name) + # cam.set(cv2.CAP_PROP_CONVERT_RGB, 0) frame_counter = 0 - sub = pubsub.subscribe("CVCams." + str(name) + ".Cmd") + sub = CamCtrl.cam_cmd_sub(name) + sub.return_on_no_data = '' msg = '' if high_speed: @@ -51,7 +59,7 @@ def pub_cam_loop(cam_id, # type: Union[int, str] cam.set(cv2.CAP_PROP_FRAME_HEIGHT, request_size[1]) if not cam.isOpened(): - pubsub.publish("CVCams." + name + ".Status", "failed") + CamCtrl.cv_cams_dict[name].status_pub.publish("failed") return False now = time.time() while msg != 'quit': @@ -60,15 +68,16 @@ def pub_cam_loop(cam_id, # type: Union[int, str] (ret, frame) = cam.read() # type: Tuple[bool, np.ndarray ] if ret is False or not isinstance(frame, np.ndarray): cam.release() - pubsub.publish("CVCams." + name + ".Status", "failed") + CamCtrl.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) - pubsub.publish("CVCams." + name + ".Vid", (frame,)) - msg = listen_default(sub, block=False, empty='') + CamCtrl.cv_cams_dict[name].frame_pub.publish(frame) + msg = sub.get() + sub.release() cam.release() return True diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 157d842..eede534 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -1,12 +1,11 @@ import warnings import cv2 -import pubsub import numpy as np from .winctrl import WinCtrl -from ..listen_default import listen_default from ..webcam_pub.camctrl import CamCtrl +from localpubsub import NoData if False: from typing import List, Union, Callable, Any @@ -26,12 +25,12 @@ class SubscriberWindows(object): self.window_names = window_names self.source_names = [] for name in video_sources: - if len(str(name))<=1000: - self.source_names.append(str(name)) - self.input_vid_global_names = [str(name) + "frame" for name in video_sources] - elif isinstance(name, np.ndarray): + if isinstance(name, np.ndarray): self.source_names.append(str(hash(str(name)))) self.input_vid_global_names = [str(hash(str(name))) + "frame" for name in video_sources] + elif len(str(name))<=1000: + self.source_names.append(str(name)) + self.input_vid_global_names = [str(name) + "frame" for name in video_sources] else: raise ValueError("Input window name too long.") @@ -64,7 +63,7 @@ class SubscriberWindows(object): return 'quit' elif key_input not in [-1, 0]: try: - WinCtrl.key_stroke(chr(key_input)) + 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)) @@ -73,24 +72,27 @@ 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.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: 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]] + if isinstance(frames, np.ndarray) and len(frames.shape)<=3: + frames = [frames] for f in range(len(frames)): 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): - sub_cmd = pubsub.subscribe("CVWinCmd") + sub_cmd = WinCtrl.win_cmd_sub() msg_cmd = '' - while msg_cmd != 'quit': + key = '' + while msg_cmd != 'quit' and key != 'quit': self.update_window_frames() - msg_cmd = self.handle_keys(cv2.waitKey(1)) - if not msg_cmd: - msg_cmd = listen_default(sub_cmd, block=False, empty='') - WinCtrl.quit() + msg_cmd = sub_cmd.get() + key = self.handle_keys(cv2.waitKey(1)) + sub_cmd.release() + WinCtrl.quit(force_all_read=False) self.__stop_all_cams() diff --git a/cvpubsubs/window_sub/winctrl.py b/cvpubsubs/window_sub/winctrl.py index d971664..d5790b7 100644 --- a/cvpubsubs/window_sub/winctrl.py +++ b/cvpubsubs/window_sub/winctrl.py @@ -1,12 +1,17 @@ -import pubsub +import threading +import logging + +from localpubsub import VariablePub, VariableSub -class WinCtrl: +class WinCtrl(object): + key_pub = VariablePub() + win_cmd_pub = VariablePub() @staticmethod - def key_stroke(key_entered): - pubsub.publish("CVKeyStroke", key_entered) + def quit(force_all_read=True): + WinCtrl.win_cmd_pub.publish('quit', force_all_read=force_all_read) @staticmethod - def quit(): - pubsub.publish("CVWinCmd", "quit") + def win_cmd_sub(): # type: ()->VariableSub + return WinCtrl.win_cmd_pub.make_sub() # type: VariableSub diff --git a/requirements.txt b/requirements.txt index d04d9fd..530014e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -pubsub numpy -opencv_python \ No newline at end of file +opencv_python +localpubsubs==0.0.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 66e0580..187773c 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,8 @@ with open('cvpubsubs/__init__.py', 'r') as f: with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() -REQUIRES = ['pubsub', 'numpy', 'opencv_python'] +with open('requirements.txt', 'r', encoding='utf-8') as f: + REQUIRES = f.readlines() setup( name='CVPubSubs', diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index efe732d..4a59bd6 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -1,27 +1,28 @@ import threading import unittest as ut -import pubsub import numpy as np import cvpubsubs.webcam_pub as w -from cvpubsubs.listen_default import listen_default from cvpubsubs.window_sub import SubscriberWindows +from cvpubsubs.window_sub.winctrl import WinCtrl if False: import numpy as np def print_keys_thread(): - sub_key = pubsub.subscribe("CVKeyStroke") - sub_cmd = pubsub.subscribe("CVWinCmd") + sub_key = WinCtrl.key_pub.make_sub() + sub_cmd = WinCtrl.win_cmd_pub.make_sub() + sub_cmd.return_on_no_data = '' msg_cmd = '' while msg_cmd != 'quit': - key_chr = listen_default(sub_key, timeout=.1) # type: np.ndarray + key_chr = sub_key.get(sub_key) # type: np.ndarray + WinCtrl.key_pub.publish(None) # consume data if key_chr is not None: print("key pressed: " + str(key_chr)) - msg_cmd = listen_default(sub_cmd, block=False, empty='') - pubsub.publish("CVWinCmd", 'quit') + msg_cmd = sub_cmd.get() + WinCtrl.quit(force_all_read=False) def start_print_keys_thread(): # type: (...) -> threading.Thread @@ -74,6 +75,7 @@ class TestSubWin(ut.TestCase): t.join() + @ut.skip("I don't have stereo cams... :(") def test_multi_cams_multi_source(self): t1 = w.VideoHandlerThread(0, request_size=(1920, 1080)) t2 = w.VideoHandlerThread(1, request_size=(1920, 1080)) From 34e2c2c82042493fecb8fdfb0e009d96bf277f8c Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 22 Jan 2019 22:01:14 -0700 Subject: [PATCH 21/59] grew fix --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index e1424ed..73e3bb4 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.3.1' +__version__ = '0.3.2' From a3f17fb29851fb42ab10fcfdac6f6a43d14816b1 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 22 Jan 2019 22:11:24 -0700 Subject: [PATCH 22/59] fixed requirements file typo --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 530014e..fd8e2aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ numpy opencv_python -localpubsubs==0.0.1 \ No newline at end of file +localpubsub==0.0.1 \ No newline at end of file From 07fb001e3bb7885ef9ae908d4ad51bc3195a08b7 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 22 Jan 2019 22:11:47 -0700 Subject: [PATCH 23/59] grew fix --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 73e3bb4..80eb7f9 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.3.2' +__version__ = '0.3.3' From 3152ca6a9b11f84eec48d49b1d9da8b898be85b6 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 19 Feb 2019 23:25:13 -0700 Subject: [PATCH 24/59] wheel_testing: added .whl file testing to travis. Added auto-deployment. Version fixed requirements.txt. Removed requirements.txt from setup.py. --- .travis.yml | 20 ++++++++++++++++++++ requirements.txt | 6 +++--- setup.py | 7 +++++-- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..238fe2c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: python +cache: pip + +python: +- '2.7' +- '3.5' +- '3.6' +- '3.7' + +install: +- pip install -r requirements.txt -q +script: +- pip install cibuildwheel==0.10.1 +- cibuildwheel --output-dir wheelhouse + +deploy: + provider: pypi + user: "SimLeek" + password: + secure: crD6W+uBJu+d/T4A8K+ZVAhyVQil5GXk1m/N0c5P7qX1DdMrcL1KXGzD5URtuvqTfUop1ah9npmT57yTP39WCRRqTRsra+WGFDZl+dH89xSnApFhWULhD1Z2JXu/54BKYlPU5xILUkqm+4BNwW4ZULXLTPZAA1dHSuSHqhQUsjfx/P7J0veNRgEdHoL2V9k8tvO8JJiGhGNDgyprv0PutzPOtTihd+aSM1f0sA4/vHF7Hbo7Surr7nfEXQeC9husBqYomG4bBS/F1P709y5hQRZVMdqzjQ8eQyQYmkWhW7Wmfq4uTkGTQf7TMzqB8GkxtU82ktSdRsDk6yhejNtG6TwM4O3XFw0saZMHViaKrSq2/rqdrmdrEQVogSaf7Z7CPa7FA3IGMm2G9mVt2xa94DP4gfElrGgG0R1vJZCV9L4ZMM2bg7OsU9XMU++tsOyKm/o+I/AMOxlgy/2m5KSganPswEGSiFWBE7foxLZT6+amXWHjopE43qWstOLGVsyfsQawBfcXOiqPYsOqEXgnvl8uqGfvE/UfocHuZGeQ/y8w0Q5rLfO3bMGaOFHzqBWeYK92pt8VAmDV3TvXHwPuIkG85qThUa4mw2s3Am/U9wdHWj1Ou6ZiFn6iHR9ASB1AFPR/qOLPWYIboSxlZTnKCs5/z+zuFZDTB9YO5hhmAr4= \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fd8e2aa..cff9e7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -numpy -opencv_python -localpubsub==0.0.1 \ No newline at end of file +opencv_python==3.4.2.17 +localpubsub==0.0.1 +numpy==1.16.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 187773c..e56929c 100644 --- a/setup.py +++ b/setup.py @@ -13,8 +13,11 @@ with open('cvpubsubs/__init__.py', 'r') as f: with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() -with open('requirements.txt', 'r', encoding='utf-8') as f: - REQUIRES = f.readlines() +REQUIRES = [ + "opencv_python==3.4.2.17" + "localpubsub==0.0.1", + "numpy==1.16.1" +] setup( name='CVPubSubs', From fee015edec67b746092c29da5423d76ba99a489a Mon Sep 17 00:00:00 2001 From: SimLeek Date: Tue, 19 Feb 2019 23:35:16 -0700 Subject: [PATCH 25/59] wheel_testing: Made travis pip installs verbose. Updated opencv version. --- .travis.yml | 3 +-- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 238fe2c..50f2b11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,9 @@ python: - '2.7' - '3.5' - '3.6' -- '3.7' install: -- pip install -r requirements.txt -q +- pip install -r requirements.txt script: - pip install cibuildwheel==0.10.1 - cibuildwheel --output-dir wheelhouse diff --git a/requirements.txt b/requirements.txt index cff9e7e..4bb8453 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -opencv_python==3.4.2.17 +opencv_python==4.0.0.21 localpubsub==0.0.1 numpy==1.16.1 \ No newline at end of file diff --git a/setup.py b/setup.py index e56929c..7a7e94f 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() REQUIRES = [ - "opencv_python==3.4.2.17" + "opencv_python==4.0.0.21" "localpubsub==0.0.1", "numpy==1.16.1" ] From 50689d92e02e0a1915f1fa5b3d836cd9964bd2bb Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 00:21:41 -0700 Subject: [PATCH 26/59] wheel_testing: bumped fix version. Fixed pypi uploading. fixed localpubsub requirement. --- .travis.yml | 1 + cvpubsubs/__init__.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 50f2b11..9a33b7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,5 +15,6 @@ script: deploy: provider: pypi user: "SimLeek" + skip_existing: true password: secure: crD6W+uBJu+d/T4A8K+ZVAhyVQil5GXk1m/N0c5P7qX1DdMrcL1KXGzD5URtuvqTfUop1ah9npmT57yTP39WCRRqTRsra+WGFDZl+dH89xSnApFhWULhD1Z2JXu/54BKYlPU5xILUkqm+4BNwW4ZULXLTPZAA1dHSuSHqhQUsjfx/P7J0veNRgEdHoL2V9k8tvO8JJiGhGNDgyprv0PutzPOtTihd+aSM1f0sA4/vHF7Hbo7Surr7nfEXQeC9husBqYomG4bBS/F1P709y5hQRZVMdqzjQ8eQyQYmkWhW7Wmfq4uTkGTQf7TMzqB8GkxtU82ktSdRsDk6yhejNtG6TwM4O3XFw0saZMHViaKrSq2/rqdrmdrEQVogSaf7Z7CPa7FA3IGMm2G9mVt2xa94DP4gfElrGgG0R1vJZCV9L4ZMM2bg7OsU9XMU++tsOyKm/o+I/AMOxlgy/2m5KSganPswEGSiFWBE7foxLZT6+amXWHjopE43qWstOLGVsyfsQawBfcXOiqPYsOqEXgnvl8uqGfvE/UfocHuZGeQ/y8w0Q5rLfO3bMGaOFHzqBWeYK92pt8VAmDV3TvXHwPuIkG85qThUa4mw2s3Am/U9wdHWj1Ou6ZiFn6iHR9ASB1AFPR/qOLPWYIboSxlZTnKCs5/z+zuFZDTB9YO5hhmAr4= \ No newline at end of file diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 80eb7f9..bfeb9e7 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.3.3' +__version__ = '0.3.4' diff --git a/requirements.txt b/requirements.txt index 4bb8453..d892d0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ opencv_python==4.0.0.21 -localpubsub==0.0.1 +localpubsub==0.0.3 numpy==1.16.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 7a7e94f..9196310 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ with open('README.md', 'r', encoding='utf-8') as f: REQUIRES = [ "opencv_python==4.0.0.21" - "localpubsub==0.0.1", + "localpubsub==0.0.3", "numpy==1.16.1" ] From 05adb7945e5028c4f1af56f81a919e446dcd688d Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 00:22:22 -0700 Subject: [PATCH 27/59] wheel_testing: Removed quotes from username just in case. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9a33b7c..47f8588 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ script: deploy: provider: pypi - user: "SimLeek" + user: SimLeek skip_existing: true password: secure: crD6W+uBJu+d/T4A8K+ZVAhyVQil5GXk1m/N0c5P7qX1DdMrcL1KXGzD5URtuvqTfUop1ah9npmT57yTP39WCRRqTRsra+WGFDZl+dH89xSnApFhWULhD1Z2JXu/54BKYlPU5xILUkqm+4BNwW4ZULXLTPZAA1dHSuSHqhQUsjfx/P7J0veNRgEdHoL2V9k8tvO8JJiGhGNDgyprv0PutzPOtTihd+aSM1f0sA4/vHF7Hbo7Surr7nfEXQeC9husBqYomG4bBS/F1P709y5hQRZVMdqzjQ8eQyQYmkWhW7Wmfq4uTkGTQf7TMzqB8GkxtU82ktSdRsDk6yhejNtG6TwM4O3XFw0saZMHViaKrSq2/rqdrmdrEQVogSaf7Z7CPa7FA3IGMm2G9mVt2xa94DP4gfElrGgG0R1vJZCV9L4ZMM2bg7OsU9XMU++tsOyKm/o+I/AMOxlgy/2m5KSganPswEGSiFWBE7foxLZT6+amXWHjopE43qWstOLGVsyfsQawBfcXOiqPYsOqEXgnvl8uqGfvE/UfocHuZGeQ/y8w0Q5rLfO3bMGaOFHzqBWeYK92pt8VAmDV3TvXHwPuIkG85qThUa4mw2s3Am/U9wdHWj1Ou6ZiFn6iHR9ASB1AFPR/qOLPWYIboSxlZTnKCs5/z+zuFZDTB9YO5hhmAr4= \ No newline at end of file From cde4c00fc7230e3e1087e633ec342c2059033f53 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 00:30:23 -0700 Subject: [PATCH 28/59] wheel_testing: Added spaces after requires as a hack to fix cibuildwheel --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9196310..d9eb6ca 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() REQUIRES = [ - "opencv_python==4.0.0.21" - "localpubsub==0.0.3", - "numpy==1.16.1" + "opencv_python==4.0.0.21 " + "localpubsub==0.0.3 ", + "numpy==1.16.1 " ] setup( From 90b9972b307cdf60cb093800e79b79b929fbb55c Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 00:41:09 -0700 Subject: [PATCH 29/59] wheel_testing: Added spaces between requires operators as a hack to fix cibuildwheel --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index d9eb6ca..55ba4f6 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() REQUIRES = [ - "opencv_python==4.0.0.21 " - "localpubsub==0.0.3 ", - "numpy==1.16.1 " + 'opencv_python == 4.0.0.21' + 'localpubsub == 0.0.3', + 'numpy == 1.16.1' ] setup( From f930064e52e9a1020292f8f0f22c90529660a23d Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 00:41:34 -0700 Subject: [PATCH 30/59] wheel_testing: Added comma to fix requires --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 55ba4f6..f28b1fb 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() REQUIRES = [ - 'opencv_python == 4.0.0.21' + 'opencv_python == 4.0.0.21', 'localpubsub == 0.0.3', 'numpy == 1.16.1' ] From c84b3da42998d3a62d68e1f579bfc54b7e32be61 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 00:57:17 -0700 Subject: [PATCH 31/59] wheel_testing: restricting wheel tests to current testing python version. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 47f8588..5fcd0e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,9 @@ python: 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 - pip install cibuildwheel==0.10.1 - cibuildwheel --output-dir wheelhouse From 2cfa87dd192a88bcd15de2bf0f31fa96b4ea8e0c Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 01:01:56 -0700 Subject: [PATCH 32/59] wheel_testing: fixed wheel testing build selector. --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5fcd0e6..94c250f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,9 @@ python: 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 == 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 - pip install cibuildwheel==0.10.1 - cibuildwheel --output-dir wheelhouse From 82b58310b32bb082d43399c901764dd485f5f89a Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 01:13:57 -0700 Subject: [PATCH 33/59] wheel_testing: updated deploy key using com method. --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 94c250f..e351240 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,9 @@ language: python cache: pip - python: - '2.7' - '3.5' - '3.6' - install: - pip install -r requirements.txt script: @@ -14,10 +12,9 @@ script: - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then export CIBW_BUILD='cp36*'; fi - pip install cibuildwheel==0.10.1 - cibuildwheel --output-dir wheelhouse - deploy: provider: pypi user: SimLeek skip_existing: true password: - secure: crD6W+uBJu+d/T4A8K+ZVAhyVQil5GXk1m/N0c5P7qX1DdMrcL1KXGzD5URtuvqTfUop1ah9npmT57yTP39WCRRqTRsra+WGFDZl+dH89xSnApFhWULhD1Z2JXu/54BKYlPU5xILUkqm+4BNwW4ZULXLTPZAA1dHSuSHqhQUsjfx/P7J0veNRgEdHoL2V9k8tvO8JJiGhGNDgyprv0PutzPOtTihd+aSM1f0sA4/vHF7Hbo7Surr7nfEXQeC9husBqYomG4bBS/F1P709y5hQRZVMdqzjQ8eQyQYmkWhW7Wmfq4uTkGTQf7TMzqB8GkxtU82ktSdRsDk6yhejNtG6TwM4O3XFw0saZMHViaKrSq2/rqdrmdrEQVogSaf7Z7CPa7FA3IGMm2G9mVt2xa94DP4gfElrGgG0R1vJZCV9L4ZMM2bg7OsU9XMU++tsOyKm/o+I/AMOxlgy/2m5KSganPswEGSiFWBE7foxLZT6+amXWHjopE43qWstOLGVsyfsQawBfcXOiqPYsOqEXgnvl8uqGfvE/UfocHuZGeQ/y8w0Q5rLfO3bMGaOFHzqBWeYK92pt8VAmDV3TvXHwPuIkG85qThUa4mw2s3Am/U9wdHWj1Ou6ZiFn6iHR9ASB1AFPR/qOLPWYIboSxlZTnKCs5/z+zuFZDTB9YO5hhmAr4= \ No newline at end of file + secure: Iyg4UilFJrpZeOyRCdyVmUT76qZzkmtPGoundyjgKIjmXharVgdpN5G1v/7NRCjifxk9vnI5QGFUcd88LpnyTg5tbLm7gttiyV4cue/UPXc8LJEu1vhc3y6fjT26pr3slEFQNuhYWCEx1HxMilizDURqxOIrQeOnYAe8UBuYrwbs6lyVLE18ojetRpMeDJ9GDwOtYboizv3TtohR/sv/XbKMpqMVWFdQU8hhahi5KgdMYq2RF+em3L9xraUiVID5AZ6DqtCod5iHbULIoabguB2ykMFCBf5XEzs6vEw4dFpvK/aHG1Z6Mmc5sIm6+Cklq73lgqkKCCTRL5Vjq/lsPQ36J4RREO64+O6pQ052M9n+wXpEo8dvy2YDYE3TvlpOmbVJ5BJrExGgYgjsQ9hslp8GroU4aZbroljQuRz8SpzAXhiHnrB29IYNpkQQk93KZAIQdT7xY5iykhtU0eo/uk1vuB64qHWxxJ05PUqCBaaRwWPHOECKccc+fIH6aRIICeRjvyAo/LHDD8b+fC+HFR6nPHKswi7Mhzl9kFI58nbNR6kdQTSPrEiVdIiOTVTY1kBWIZweyV/vJGh+PUZyyWe61r5PFxU9lXMZH8oY/xvGPlhaUrgLvJ1tV24m1EGJ9dbbeuQ9T6dQYqY28IC7gl9JKbmnTBewQfW/F0T2N5I= From 147bf9aaff1ff3d968efa2765c0bce86c7e14532 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 22:08:06 -0700 Subject: [PATCH 34/59] nesting: Added ability to display tensors of any rank as multiple windows. Added to readme. lowered opencv_python requirement due to char* assertion bug. --- README.md | 10 ++++++++++ cvpubsubs/window_sub/cv_window_sub.py | 23 ++++++++++++++--------- requirements.txt | 2 +- setup.py | 2 +- tests/test_sub_win.py | 7 +++++++ 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ce013de..1426acb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,16 @@ Python 2.7/3.5+ and PyPy. w.VideoHandlerThread(callbacks=[redden_frame_print_spam] + w.display_callbacks).display() +#### Display a tensor + + def tensor_from_image(frame, cam_id): + ten = tensor_from_pytorch_or_tensorflow(frame) + return ten + + t = wp.VideoHandlerThread(video_source=cam, callbacks=[tensor_from_image] + wp.display_callbacks) + + t.display() + #### Display multiple windows from one source import cvpubsubs.webcam_pub as w from cvpubsubs.window_sub import SubscriberWindows diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index eede534..5bd9c03 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -28,7 +28,7 @@ class SubscriberWindows(object): if isinstance(name, np.ndarray): self.source_names.append(str(hash(str(name)))) self.input_vid_global_names = [str(hash(str(name))) + "frame" for name in video_sources] - elif len(str(name))<=1000: + elif len(str(name)) <= 1000: self.source_names.append(str(name)) self.input_vid_global_names = [str(name) + "frame" for name in video_sources] else: @@ -37,7 +37,6 @@ class SubscriberWindows(object): self.callbacks = callbacks self.input_cams = video_sources - @staticmethod def set_global_frame_dict(name, *args): if len(str(name)) <= 1000: @@ -47,7 +46,6 @@ class SubscriberWindows(object): else: raise ValueError("Input window name too long.") - def __stop_all_cams(self): for c in self.source_names: CamCtrl.stop_cam(c) @@ -69,20 +67,27 @@ class SubscriberWindows(object): RuntimeWarning("Unknown key code: [{}]. Please report to cv_pubsubs issue page.".format(key_input)) ) + def _display_frames(self, frames, win_num): + for f in range(len(frames)): + if frames[f].dtype.num == 17 or len(frames[f].shape) > 3: # detect nested + self._display_frames(frames[f], win_num) + else: + cv2.imshow(self.window_names[win_num % len(self.window_names)] + " (press ESC to quit)", frames[f]) + win_num += 1 + 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.input_vid_global_names[i]], + NoData): + 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]] - if isinstance(frames, np.ndarray) and len(frames.shape)<=3: + if isinstance(frames, np.ndarray) and len(frames.shape) <= 3: frames = [frames] - for f in range(len(frames)): - cv2.imshow(self.window_names[win_num % len(self.window_names)] + " (press ESC to quit)", frames[f]) - win_num += 1 + self._display_frames(frames, win_num) # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 def loop(self): diff --git a/requirements.txt b/requirements.txt index d892d0b..c2474b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -opencv_python==4.0.0.21 +opencv_python==3.4.5.20 localpubsub==0.0.3 numpy==1.16.1 \ No newline at end of file diff --git a/setup.py b/setup.py index f28b1fb..319bbc2 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ with open('README.md', 'r', encoding='utf-8') as f: readme = f.read() REQUIRES = [ - 'opencv_python == 4.0.0.21', + 'opencv_python == 3.4.5.20', 'localpubsub == 0.0.3', 'numpy == 1.16.1' ] diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 4a59bd6..594740c 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -89,3 +89,10 @@ class TestSubWin(ut.TestCase): t1.join() t1.join() + + def test_nested_frames(self): + def nest_frame(frame, cam_id): + frame = np.asarray([[[[[[frame]]]]]]) + return frame + + w.VideoHandlerThread(callbacks=[nest_frame] + w.display_callbacks).display() \ No newline at end of file From 3ba912d757e0c41f8baa0370fb453abe1a2b9f29 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 22:11:06 -0700 Subject: [PATCH 35/59] nesting: Grew minor due to large feature addition. --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index bfeb9e7..abeeedb 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.3.4' +__version__ = '0.4.0' From 49c797f3385a02bbdb295ad9692b79b6958f864f Mon Sep 17 00:00:00 2001 From: SimLeek Date: Wed, 20 Feb 2019 22:45:12 -0700 Subject: [PATCH 36/59] nesting: Grew fix to fix tensor displaying. Added list handling. Improved tensor display test. Added list editing handling. --- cvpubsubs/__init__.py | 2 +- cvpubsubs/webcam_pub/frame_handler.py | 4 +++- cvpubsubs/window_sub/cv_window_sub.py | 13 +++++++------ tests/test_sub_win.py | 13 ++++++++++--- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index abeeedb..f0ede3d 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.4.0' +__version__ = '0.4.1' diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index 87a61c6..2eb1372 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -73,7 +73,9 @@ class VideoHandlerThread(threading.Thread): if frame is not None: frame = frame for c in self.callbacks: - frame = c(frame, self.cam_id) + frame_c = c(frame, self.cam_id) + if frame_c is not None: + frame = frame_c msg_owner = sub_owner.get() sub_owner.release() sub_cam.release() diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 5bd9c03..da1533e 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -69,25 +69,26 @@ class SubscriberWindows(object): def _display_frames(self, frames, win_num): for f in range(len(frames)): - if frames[f].dtype.num == 17 or len(frames[f].shape) > 3: # detect nested - self._display_frames(frames[f], win_num) + # 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) 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 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: 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]] if isinstance(frames, np.ndarray) and len(frames.shape) <= 3: frames = [frames] - self._display_frames(frames, win_num) + win_num = self._display_frames(frames, win_num) # todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170 def loop(self): diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 594740c..7adf8a4 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -18,7 +18,7 @@ def print_keys_thread(): msg_cmd = '' while msg_cmd != 'quit': key_chr = sub_key.get(sub_key) # type: np.ndarray - WinCtrl.key_pub.publish(None) # consume data + WinCtrl.key_pub.publish(None) # consume data if key_chr is not None: print("key pressed: " + str(key_chr)) msg_cmd = sub_cmd.get() @@ -92,7 +92,14 @@ class TestSubWin(ut.TestCase): def test_nested_frames(self): def nest_frame(frame, cam_id): - frame = np.asarray([[[[[[frame]]]]]]) + frame = np.asarray([[[[[[frame]]]]], [[[[[frame]]], [[[frame]]]]]]) return frame - w.VideoHandlerThread(callbacks=[nest_frame] + w.display_callbacks).display() \ No newline at end of file + v = w.VideoHandlerThread(callbacks=[nest_frame] + w.display_callbacks) + v.start() + + SubscriberWindows(window_names=[str(i) for i in range(3)], + video_sources=[str(0)] + ).loop() + + v.join() From 6d257e99ca066dd77a90dc6572aa3ab8ffe72d1d Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 01:28:48 -0700 Subject: [PATCH 37/59] 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 3bfc954e3666ca7f0cc5a5d31cebac56e88bafcf Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 01:42:27 -0700 Subject: [PATCH 38/59] 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 72b0a13bbcf32538a37ea2603e9448f8943d9490 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 01:43:09 -0700 Subject: [PATCH 39/59] 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' From 4586e9743f6bcd007a5dc157b63d8a2152dbec09 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 21:04:35 -0700 Subject: [PATCH 40/59] callbacks: Added pytorch function display and pytorch conway life example. Still need to test coords. --- cvpubsubs/webcam_pub/callbacks.py | 75 +++++++++++++++++++++++++++ cvpubsubs/window_sub/cv_window_sub.py | 4 +- tests/test_sub_win.py | 36 +++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/cvpubsubs/webcam_pub/callbacks.py b/cvpubsubs/webcam_pub/callbacks.py index 767aa0f..ece4a73 100644 --- a/cvpubsubs/webcam_pub/callbacks.py +++ b/cvpubsubs/webcam_pub/callbacks.py @@ -65,3 +65,78 @@ class function_display_callback(object): # NOSONAR def __call__(self, *args, **kwargs): return self.inner_function(self, *args, **kwargs) + + +class pytorch_function_display_callback(object): # NOSONAR + def __init__(self, display_function, finish_function=None): + """Used for running arbitrary functions on pixels. + + >>> import random + >>> import torch + >>> from cvpubsubs.webcam_pub import VideoHandlerThread + >>> img = np.zeros((300, 300, 3)) + >>> def fun(array, coords, finished): + ... rgb = torch.empty(array.shape).uniform_(0,1).type(torch.DoubleTensor).to(array.device)/200.0 + ... array[coords] = (array[coords] + rgb[coords])%1.0 + >>> VideoHandlerThread(video_source=img, callbacks=pytorch_function_display_callback(fun)).display() + + thanks: https://medium.com/@awildtaber/building-a-rendering-engine-in-tensorflow-262438b2e062 + + :param display_function: + :param finish_function: + """ + + import torch + from torch.autograd import Variable + + 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 _setup(self, frame, cam_id, *args, **kwargs): + + if "device" in kwargs: + self.device = torch.device(kwargs["device"]) + else: + if torch.cuda.is_available(): + self.device = torch.device("cuda") + else: + self.device = torch.device("cpu") + + self.min_bounds = [0 for _ in frame.shape] + self.max_bounds = list(frame.shape) + grid_slices = [slice(self.min_bounds[d], self.max_bounds[d]) for d in range(len(frame.shape))] + self.space_grid = np.mgrid[grid_slices] + x_tens = torch.LongTensor(self.space_grid[0, ...]).to(self.device) + y_tens = torch.LongTensor(self.space_grid[1, ...]).to(self.device) + c_tens = torch.LongTensor(self.space_grid[2, ...]).to(self.device) + self.x = Variable(x_tens, requires_grad=False) + self.y = Variable(y_tens, requires_grad=False) + self.c = Variable(c_tens, requires_grad=False) + + def _display_internal(self, frame, cam_id, *args, **kwargs): + finished = True + if self.first_call: + # return to display initial frame + _setup(self, frame, finished, *args, **kwargs) + self.first_call = False + return + if self.looping: + tor_frame = torch.from_numpy(frame).to(self.device) + finished = display_function(tor_frame, (self.x, self.y, self.c), finished, *args, **kwargs) + frame[...] = tor_frame.cpu().numpy()[...] + 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/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 08b1562..6e0c4cf 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -40,9 +40,9 @@ class SubscriberWindows(object): @staticmethod def set_global_frame_dict(name, *args): if len(str(name)) <= 1000: - SubscriberWindows.frame_dict[str(name) + "frame"] = [*args] + SubscriberWindows.frame_dict[str(name) + "frame"] = list(args) elif isinstance(name, np.ndarray): - SubscriberWindows.frame_dict[str(hash(str(name))) + "frame"] = [*args] + SubscriberWindows.frame_dict[str(hash(str(name))) + "frame"] = list(args) else: raise ValueError("Input window name too long.") diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 9fac5e6..addfedc 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -128,3 +128,39 @@ class TestSubWin(ut.TestCase): array[coords] = 1.0 VideoHandlerThread(video_source=img, callbacks=function_display_callback(conway)).display() + + def test_conway_life_pytorch(self): + import torch + from torch import functional as F + from cvpubsubs.webcam_pub import VideoHandlerThread + from cvpubsubs.webcam_pub.callbacks import pytorch_function_display_callback + + img = np.ones((600, 800, 1)) + img[10:590, 10:790, :] = 0 + + def fun(frame, coords, finished): + array = frame + neighbor_weights = torch.ones(torch.Size([3, 3])) + neighbor_weights[1, 1, ...] = 0 + neighbor_weights = torch.Tensor(neighbor_weights).type_as(array).to(array.device) + neighbor_weights = neighbor_weights.squeeze()[None, None, :, :] + array = array.permute(2, 1, 0)[None, ...] + neighbors = torch.nn.functional.conv2d(array, neighbor_weights, stride=1, padding=1) + live_array = torch.where((neighbors < 2) | (neighbors > 3), + torch.zeros_like(array), + torch.where((2 <= neighbors) & (neighbors <= 3), + torch.ones_like(array), + array + ) + ) + dead_array = torch.where(neighbors == 3, + torch.ones_like(array), + array) + array = torch.where(array == 1.0, + live_array, + dead_array + ) + array = array.squeeze().permute(1, 0)[...,None] + frame[...] = array[...] + + VideoHandlerThread(video_source=img, callbacks=pytorch_function_display_callback(fun)).display() From 97baf91f6474e16f9f05c44bdfdef1263582ce2e Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 21:21:04 -0700 Subject: [PATCH 41/59] callbacks: Modified callbacks example to use coords. --- cvpubsubs/webcam_pub/callbacks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvpubsubs/webcam_pub/callbacks.py b/cvpubsubs/webcam_pub/callbacks.py index ece4a73..3bc18bf 100644 --- a/cvpubsubs/webcam_pub/callbacks.py +++ b/cvpubsubs/webcam_pub/callbacks.py @@ -76,8 +76,8 @@ class pytorch_function_display_callback(object): # NOSONAR >>> from cvpubsubs.webcam_pub import VideoHandlerThread >>> img = np.zeros((300, 300, 3)) >>> def fun(array, coords, finished): - ... rgb = torch.empty(array.shape).uniform_(0,1).type(torch.DoubleTensor).to(array.device)/200.0 - ... array[coords] = (array[coords] + rgb[coords])%1.0 + ... rgb = torch.empty(array.shape).uniform_(0,1).type(torch.DoubleTensor).to(array.device)/150.0 + ... array[coords] = (array[coords+np.ones_like(coords)] + rgb[coords])%1.0 >>> VideoHandlerThread(video_source=img, callbacks=pytorch_function_display_callback(fun)).display() thanks: https://medium.com/@awildtaber/building-a-rendering-engine-in-tensorflow-262438b2e062 From b685649780de94fcbf8e24d77bf9ff07de94775c Mon Sep 17 00:00:00 2001 From: SimLeek Date: Sun, 24 Feb 2019 21:24:29 -0700 Subject: [PATCH 42/59] callbacks: Modified callbacks example to use coords with modification. --- cvpubsubs/webcam_pub/callbacks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cvpubsubs/webcam_pub/callbacks.py b/cvpubsubs/webcam_pub/callbacks.py index 3bc18bf..e972172 100644 --- a/cvpubsubs/webcam_pub/callbacks.py +++ b/cvpubsubs/webcam_pub/callbacks.py @@ -77,7 +77,9 @@ class pytorch_function_display_callback(object): # NOSONAR >>> img = np.zeros((300, 300, 3)) >>> def fun(array, coords, finished): ... rgb = torch.empty(array.shape).uniform_(0,1).type(torch.DoubleTensor).to(array.device)/150.0 - ... array[coords] = (array[coords+np.ones_like(coords)] + rgb[coords])%1.0 + ... trans = np.zeros_like(coords) + ... trans[0,...] = 1 + ... array[coords] = (array[coords+trans] + rgb[coords])%1.0 >>> VideoHandlerThread(video_source=img, callbacks=pytorch_function_display_callback(fun)).display() thanks: https://medium.com/@awildtaber/building-a-rendering-engine-in-tensorflow-262438b2e062 From 1af06cf8cdce5979f04e03b1145247324c762968 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 20:14:09 -0700 Subject: [PATCH 43/59] exceptions: Moved callbacks to its own location. Added VideoHandlerThread callback exception handling. Added exception tests. --- cvpubsubs/__init__.py | 2 +- cvpubsubs/callbacks.py | 67 ++++++++++++ cvpubsubs/webcam_pub/callbacks.py | 144 -------------------------- cvpubsubs/webcam_pub/frame_handler.py | 16 ++- cvpubsubs/window_sub/cv_window_sub.py | 2 + tests/test_sub_win.py | 67 +++++------- 6 files changed, 112 insertions(+), 186 deletions(-) create mode 100644 cvpubsubs/callbacks.py delete mode 100644 cvpubsubs/webcam_pub/callbacks.py diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 2b8877c..ef7eb44 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.5.0' +__version__ = '0.6.0' diff --git a/cvpubsubs/callbacks.py b/cvpubsubs/callbacks.py new file mode 100644 index 0000000..d95f835 --- /dev/null +++ b/cvpubsubs/callbacks.py @@ -0,0 +1,67 @@ +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, cam_id, *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/webcam_pub/callbacks.py b/cvpubsubs/webcam_pub/callbacks.py deleted file mode 100644 index e972172..0000000 --- a/cvpubsubs/webcam_pub/callbacks.py +++ /dev/null @@ -1,144 +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: - :param finish_function: - """ - 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: - 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) - - -class pytorch_function_display_callback(object): # NOSONAR - def __init__(self, display_function, finish_function=None): - """Used for running arbitrary functions on pixels. - - >>> import random - >>> import torch - >>> from cvpubsubs.webcam_pub import VideoHandlerThread - >>> img = np.zeros((300, 300, 3)) - >>> def fun(array, coords, finished): - ... rgb = torch.empty(array.shape).uniform_(0,1).type(torch.DoubleTensor).to(array.device)/150.0 - ... trans = np.zeros_like(coords) - ... trans[0,...] = 1 - ... array[coords] = (array[coords+trans] + rgb[coords])%1.0 - >>> VideoHandlerThread(video_source=img, callbacks=pytorch_function_display_callback(fun)).display() - - thanks: https://medium.com/@awildtaber/building-a-rendering-engine-in-tensorflow-262438b2e062 - - :param display_function: - :param finish_function: - """ - - import torch - from torch.autograd import Variable - - 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 _setup(self, frame, cam_id, *args, **kwargs): - - if "device" in kwargs: - self.device = torch.device(kwargs["device"]) - else: - if torch.cuda.is_available(): - self.device = torch.device("cuda") - else: - self.device = torch.device("cpu") - - self.min_bounds = [0 for _ in frame.shape] - self.max_bounds = list(frame.shape) - grid_slices = [slice(self.min_bounds[d], self.max_bounds[d]) for d in range(len(frame.shape))] - self.space_grid = np.mgrid[grid_slices] - x_tens = torch.LongTensor(self.space_grid[0, ...]).to(self.device) - y_tens = torch.LongTensor(self.space_grid[1, ...]).to(self.device) - c_tens = torch.LongTensor(self.space_grid[2, ...]).to(self.device) - self.x = Variable(x_tens, requires_grad=False) - self.y = Variable(y_tens, requires_grad=False) - self.c = Variable(c_tens, requires_grad=False) - - def _display_internal(self, frame, cam_id, *args, **kwargs): - finished = True - if self.first_call: - # return to display initial frame - _setup(self, frame, finished, *args, **kwargs) - self.first_call = False - return - if self.looping: - tor_frame = torch.from_numpy(frame).to(self.device) - finished = display_function(tor_frame, (self.x, self.y, self.c), finished, *args, **kwargs) - frame[...] = tor_frame.cpu().numpy()[...] - 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/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index efed326..c218a82 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -4,13 +4,15 @@ 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 + if False: from typing import Union, Tuple, Any, Callable, List, Optional FrameCallable = Callable[[np.ndarray, int], Optional[np.ndarray]] -from cvpubsubs.webcam_pub.callbacks import global_cv_display_callback +from cvpubsubs.callbacks import global_cv_display_callback display_callbacks = [global_cv_display_callback] @@ -53,6 +55,7 @@ class VideoHandlerThread(threading.Thread): 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.""" @@ -66,7 +69,14 @@ class VideoHandlerThread(threading.Thread): frame = sub_cam.get(blocking=True, timeout=1.0) # type: np.ndarray if frame is not None: for c in self.callbacks: - frame_c = c(frame, self.cam_id) + try: + frame_c = c(frame, self.cam_id) + except Exception as e: + import traceback + CamCtrl.stop_cam(self.cam_id) + WinCtrl.quit() + self.exception_raised = e + frame_c = self.exception_raised if frame_c is not None: frame = frame_c msg_owner = sub_owner.get() @@ -93,3 +103,5 @@ class VideoHandlerThread(threading.Thread): 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/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 6e0c4cf..561fb6a 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -68,6 +68,8 @@ class SubscriberWindows(object): ) def _display_frames(self, frames, win_num): + 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: diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index addfedc..8f51eae 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -1,8 +1,6 @@ import threading import unittest as ut -import numpy as np - import cvpubsubs.webcam_pub as w from cvpubsubs.window_sub import SubscriberWindows from cvpubsubs.window_sub.winctrl import WinCtrl @@ -61,6 +59,16 @@ class TestSubWin(ut.TestCase): w.VideoHandlerThread(callbacks=redden_frame_print_spam).display() + def test_sub_with_callback_exception(self): + def redden_frame_print_spam(frame, cam_id): + frame[:, :, 0] = 0 + frame[:, :, 2] = 1 / 0 + + with self.assertRaises(ZeroDivisionError) as e: + v = w.VideoHandlerThread(callbacks=redden_frame_print_spam) + v.display() + self.assertEqual(v.exception_raised, e) + def test_multi_cams_one_source(self): def cam_handler(frame, cam_id): SubscriberWindows.set_global_frame_dict(cam_id, frame, frame) @@ -108,9 +116,26 @@ class TestSubWin(ut.TestCase): v.join() + def test_nested_frames_exception(self): + def nest_frame(frame, cam_id): + frame = np.asarray([[[[[[frame + 1 / 0]]]]], [[[[[frame]]], [[[frame]]]]]]) + return frame + + v = w.VideoHandlerThread(callbacks=[nest_frame] + w.display_callbacks) + v.start() + + with self.assertRaises(ZeroDivisionError) as e: + 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.webcam_pub.callbacks import function_display_callback + from cvpubsubs.callbacks import function_display_callback + import numpy as np img = np.zeros((50, 50, 1)) img[0:5, 0:5, :] = 1 @@ -128,39 +153,3 @@ class TestSubWin(ut.TestCase): array[coords] = 1.0 VideoHandlerThread(video_source=img, callbacks=function_display_callback(conway)).display() - - def test_conway_life_pytorch(self): - import torch - from torch import functional as F - from cvpubsubs.webcam_pub import VideoHandlerThread - from cvpubsubs.webcam_pub.callbacks import pytorch_function_display_callback - - img = np.ones((600, 800, 1)) - img[10:590, 10:790, :] = 0 - - def fun(frame, coords, finished): - array = frame - neighbor_weights = torch.ones(torch.Size([3, 3])) - neighbor_weights[1, 1, ...] = 0 - neighbor_weights = torch.Tensor(neighbor_weights).type_as(array).to(array.device) - neighbor_weights = neighbor_weights.squeeze()[None, None, :, :] - array = array.permute(2, 1, 0)[None, ...] - neighbors = torch.nn.functional.conv2d(array, neighbor_weights, stride=1, padding=1) - live_array = torch.where((neighbors < 2) | (neighbors > 3), - torch.zeros_like(array), - torch.where((2 <= neighbors) & (neighbors <= 3), - torch.ones_like(array), - array - ) - ) - dead_array = torch.where(neighbors == 3, - torch.ones_like(array), - array) - array = torch.where(array == 1.0, - live_array, - dead_array - ) - array = array.squeeze().permute(1, 0)[...,None] - frame[...] = array[...] - - VideoHandlerThread(video_source=img, callbacks=pytorch_function_display_callback(fun)).display() From 79b1d6d223246a17287f3064f9991ab3c01ab314 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 22:09:37 -0700 Subject: [PATCH 44/59] keymouse: Created input loop decorators. Added tests. Added mouse events and mouse event handling. Added mouse interaction to conway test. --- cvpubsubs/input.py | 90 +++++++++++++++++++++++++++ cvpubsubs/window_sub/cv_window_sub.py | 8 +++ cvpubsubs/window_sub/mouse_event.py | 13 ++++ cvpubsubs/window_sub/winctrl.py | 1 + tests/test_sub_win.py | 46 ++++++++------ 5 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 cvpubsubs/input.py create mode 100644 cvpubsubs/window_sub/mouse_event.py diff --git a/cvpubsubs/input.py b/cvpubsubs/input.py new file mode 100644 index 0000000..74e5014 --- /dev/null +++ b/cvpubsubs/input.py @@ -0,0 +1,90 @@ +from cvpubsubs.window_sub.winctrl import WinCtrl +import threading + +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): + 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 + + def __call__(self, *args, **kwargs): + msg_cmd = '' + while msg_cmd != 'quit': + mouse_xyzclick = self.sub_mouse.get() # 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() + 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): + 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 + + 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() + 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/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 561fb6a..c03ea54 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -6,6 +6,7 @@ import numpy as np from .winctrl import WinCtrl from cvpubsubs.webcam_pub.camctrl import CamCtrl from localpubsub import NoData +from cvpubsubs.window_sub.mouse_event import MouseEvent if False: from typing import List, Union, Callable, Any @@ -36,6 +37,9 @@ class SubscriberWindows(object): self.callbacks = callbacks self.input_cams = video_sources + for name in self.window_names: + cv2.namedWindow(name + " (press ESC to quit)") + cv2.setMouseCallback(name + " (press ESC to quit)", self.handle_mouse) @staticmethod def set_global_frame_dict(name, *args): @@ -67,6 +71,10 @@ class SubscriberWindows(object): 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): if isinstance(frames, Exception): raise frames diff --git a/cvpubsubs/window_sub/mouse_event.py b/cvpubsubs/window_sub/mouse_event.py new file mode 100644 index 0000000..b4eb31f --- /dev/null +++ b/cvpubsubs/window_sub/mouse_event.py @@ -0,0 +1,13 @@ +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 index d5790b7..4d41f05 100644 --- a/cvpubsubs/window_sub/winctrl.py +++ b/cvpubsubs/window_sub/winctrl.py @@ -6,6 +6,7 @@ from localpubsub import VariablePub, VariableSub class WinCtrl(object): key_pub = VariablePub() + mouse_pub = VariablePub() win_cmd_pub = VariablePub() @staticmethod diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 8f51eae..c514748 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -4,33 +4,29 @@ 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.input import mouse_loop, key_loop if False: import numpy as np - - -def print_keys_thread(): - sub_key = WinCtrl.key_pub.make_sub() - sub_cmd = WinCtrl.win_cmd_pub.make_sub() - sub_cmd.return_on_no_data = '' - msg_cmd = '' - while msg_cmd != 'quit': - key_chr = sub_key.get(sub_key) # type: np.ndarray - WinCtrl.key_pub.publish(None) # consume data - if key_chr is not None: - print("key pressed: " + str(key_chr)) - msg_cmd = sub_cmd.get() - WinCtrl.quit(force_all_read=False) - - -def start_print_keys_thread(): # type: (...) -> threading.Thread - t = threading.Thread(target=print_keys_thread, args=()) - t.start() - return t + from cvpubsubs.window_sub.mouse_event import MouseEvent class TestSubWin(ut.TestCase): + def test_mouse_loop(self): + @mouse_loop + def print_mouse_thread(mouse_event): + print(mouse_event) + + w.VideoHandlerThread().display() + + def test_key_loop(self): + @key_loop + def print_key_thread(key_chr): + print("key pressed: " + str(key_chr)) + + w.VideoHandlerThread().display() + def test_sub(self): w.VideoHandlerThread().display() @@ -136,6 +132,7 @@ class TestSubWin(ut.TestCase): from cvpubsubs.webcam_pub import VideoHandlerThread from cvpubsubs.callbacks import function_display_callback import numpy as np + import cv2 img = np.zeros((50, 50, 1)) img[0:5, 0:5, :] = 1 @@ -152,4 +149,13 @@ class TestSubWin(ut.TestCase): if neighbors == 3: array[coords] = 1.0 + @mouse_loop + 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 + 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 + VideoHandlerThread(video_source=img, callbacks=function_display_callback(conway)).display() From 1b4ee8e70b8baf47a23ede0d31e6fd7c7e38dac7 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 22:10:37 -0700 Subject: [PATCH 45/59] keymouse: grew fix for added functionality. --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index ef7eb44..8411e55 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.6.0' +__version__ = '0.6.1' From ac0294ec644f2c5f8f282501bf91d4720a4bdf21 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 22:13:04 -0700 Subject: [PATCH 46/59] keymouse: Added input examples. --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6366c7..7107d37 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,26 @@ Python 2.7/3.5+ and PyPy. fps_limit = 8 ) - video_thread.display() +#### handle mouse input + import cvpubsubs.webcam_pub as w + from cvpubsubs.input import mouse_loop + @mouse_loop + def print_mouse(mouse_event): + print(mouse_event) + + w.VideoHandlerThread().display() + +#### take in key input + import cvpubsubs.webcam_pub as w + from cvpubsubs.input import key_loop + + @key_loop + def print_key_thread(key_chr): + print("key pressed: " + str(key_chr)) + + w.VideoHandlerThread().display() + #### Run your own functions on the frames import cvpubsubs.webcam_pub as w From da87f3e0af12a2b4fa19a9ee523989e0c13bc7cc Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 22:57:03 -0700 Subject: [PATCH 47/59] keymouse: Fixed input slowdown. --- cvpubsubs/input.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cvpubsubs/input.py b/cvpubsubs/input.py index 74e5014..d341731 100644 --- a/cvpubsubs/input.py +++ b/cvpubsubs/input.py @@ -1,5 +1,6 @@ from cvpubsubs.window_sub.winctrl import WinCtrl import threading +import time if False: from typing import Callable @@ -18,22 +19,24 @@ class mouse_thread(object): # NOSONAR class mouse_loop_thread(object): # NOSONAR - def __init__(self, f, run_when_no_events=False): + 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() # type: MouseEvent + 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) @@ -59,12 +62,13 @@ class key_thread(object): # NOSONAR class key_loop_thread(object): # NOSONAR - def __init__(self, f, run_when_no_events=False): + 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 = '' @@ -75,6 +79,7 @@ class key_loop_thread(object): # NOSONAR 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) From 66d9a074586a44b773cde7c29db4a1f9e6995ff2 Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 22:57:03 -0700 Subject: [PATCH 48/59] keymouse: Fixed input slowdown. --- cvpubsubs/input.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cvpubsubs/input.py b/cvpubsubs/input.py index 74e5014..d341731 100644 --- a/cvpubsubs/input.py +++ b/cvpubsubs/input.py @@ -1,5 +1,6 @@ from cvpubsubs.window_sub.winctrl import WinCtrl import threading +import time if False: from typing import Callable @@ -18,22 +19,24 @@ class mouse_thread(object): # NOSONAR class mouse_loop_thread(object): # NOSONAR - def __init__(self, f, run_when_no_events=False): + 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() # type: MouseEvent + 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) @@ -59,12 +62,13 @@ class key_thread(object): # NOSONAR class key_loop_thread(object): # NOSONAR - def __init__(self, f, run_when_no_events=False): + 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 = '' @@ -75,6 +79,7 @@ class key_loop_thread(object): # NOSONAR 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) From 4ac5b74b9f6b7796abbb6ce7724dc17bb9d4b5fc Mon Sep 17 00:00:00 2001 From: SimLeek Date: Mon, 25 Feb 2019 23:03:00 -0700 Subject: [PATCH 49/59] keymouse: grew fix because fix. --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 8411e55..aece342 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.6.1' +__version__ = '0.6.2' From 0b63999ead1d44f4fc3a2039edafab66ba5e7ffc Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Sun, 10 Mar 2019 17:02:34 -0700 Subject: [PATCH 50/59] py37: added python 3.7 compatibility. Grew fix. --- .travis.yml | 22 +++++++++++++--------- cvpubsubs/__init__.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index e351240..b4f470d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,21 @@ language: python +dist: xenial +sudo: true + cache: pip python: -- '2.7' -- '3.5' -- '3.6' + - '2.7' + - '3.5' + - '3.6' + - '3.7' install: -- pip install -r requirements.txt + - 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 -- pip install cibuildwheel==0.10.1 -- cibuildwheel --output-dir wheelhouse + - 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 + - pip install cibuildwheel==0.10.1 + - cibuildwheel --output-dir wheelhouse deploy: provider: pypi user: SimLeek diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index aece342..a68d2bd 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.6.2' +__version__ = '0.6.3' From 228e95f277c5503a3ef90a83b82c14618190dd38 Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Sun, 10 Mar 2019 17:07:10 -0700 Subject: [PATCH 51/59] py37: added detection for cp37 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b4f470d..2a2448e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ 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 - pip install cibuildwheel==0.10.1 - cibuildwheel --output-dir wheelhouse deploy: From e70ea519d860a35c4a2f6fbc4e2028182f2259eb Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Sun, 10 Mar 2019 17:19:12 -0700 Subject: [PATCH 52/59] py37: Updated requirements. --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c2474b8..029b888 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ opencv_python==3.4.5.20 -localpubsub==0.0.3 +localpubsub==0.0.4 numpy==1.16.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 319bbc2..831d843 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ with open('README.md', 'r', encoding='utf-8') as f: REQUIRES = [ 'opencv_python == 3.4.5.20', - 'localpubsub == 0.0.3', + 'localpubsub == 0.0.4', 'numpy == 1.16.1' ] From 3382d945c5b7983d459b307baa3efcb2e4bc27fe Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Sun, 10 Mar 2019 17:25:02 -0700 Subject: [PATCH 53/59] py37: grew fix --- cvpubsubs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index a68d2bd..02f8497 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1 @@ -__version__ = '0.6.3' +__version__ = '0.6.4' From c23b538c32e1fd2cbe47cfddd62c608a86cac00b Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Wed, 26 Jun 2019 17:31:03 -0700 Subject: [PATCH 54/59] added display function. --- cvpubsubs/__init__.py | 2 ++ cvpubsubs/window_sub/cv_window_sub.py | 14 ++++++++++++++ tests/test_sub_win.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 02f8497..2abf7d7 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1 +1,3 @@ __version__ = '0.6.4' + +from .window_sub.cv_window_sub import display diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index c03ea54..4b30e43 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -5,6 +5,7 @@ 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 @@ -112,3 +113,16 @@ class SubscriberWindows(object): sub_cmd.release() WinCtrl.quit(force_all_read=False) self.__stop_all_cams() + + +def display(*vids, names=[]): + vid_threads = [VideoHandlerThread(v) for v in vids] + for v in vid_threads: + v.start() + if len(names) == 0: + names = ["window {}".format(i) for i in range(len(vids))] + SubscriberWindows(window_names=names, + video_sources=vids + ).loop() + for v in vid_threads: + v.join() diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index c514748..698b7f8 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -4,7 +4,9 @@ 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 if False: import numpy as np @@ -159,3 +161,19 @@ class TestSubWin(ut.TestCase): 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() + + def test_double_win(self): + vid1 = np.ones((100, 100)) + vid2 = np.zeros((100, 100)) + t1 = w.VideoHandlerThread(vid1) + t2 = w.VideoHandlerThread(vid2) + t1.start() + t2.start() + SubscriberWindows(window_names=['cammy', 'cammy2'], + video_sources=[vid1, vid2] + ).loop() + t1.join() + t1.join() + + def test_display(self): + display(np.ones((100, 100)), np.zeros((100, 100))) From 60702f5db089dd3aa1e53cba600eb19b96606fec Mon Sep 17 00:00:00 2001 From: simleek Date: Sun, 29 Sep 2019 22:12:00 -0700 Subject: [PATCH 55/59] Added more api to display function --- cvpubsubs/__init__.py | 2 +- cvpubsubs/serialize.py | 12 +++ cvpubsubs/webcam_pub/frame_handler.py | 29 +++--- cvpubsubs/window_sub/cv_window_sub.py | 144 +++++++++++++++++++------- tests/test_simple_api.py | 61 +++++++++++ 5 files changed, 196 insertions(+), 52 deletions(-) create mode 100644 cvpubsubs/serialize.py create mode 100644 tests/test_simple_api.py diff --git a/cvpubsubs/__init__.py b/cvpubsubs/__init__.py index 2abf7d7..b2741c7 100644 --- a/cvpubsubs/__init__.py +++ b/cvpubsubs/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.6.4' +__version__ = '0.6.5' from .window_sub.cv_window_sub import display diff --git a/cvpubsubs/serialize.py b/cvpubsubs/serialize.py new file mode 100644 index 0000000..0db2aab --- /dev/null +++ b/cvpubsubs/serialize.py @@ -0,0 +1,12 @@ +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/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index c218a82..84f02c7 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -5,7 +5,7 @@ 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 @@ -21,7 +21,7 @@ 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=(global_cv_display_callback,), # type: Union[List[FrameCallable], FrameCallable] + callbacks=(), # type: Union[List[FrameCallable], FrameCallable] request_size=(-1, -1), # type: Tuple[int, int] high_speed=True, # type: bool fps_limit=240 # type: float @@ -40,13 +40,7 @@ class VideoHandlerThread(threading.Thread): :type fps_limit: float """ super(VideoHandlerThread, self).__init__(target=self.loop, args=()) - if isinstance(video_source, (int, str)): - self.cam_id = str(video_source) - elif isinstance(video_source, np.ndarray): - self.cam_id = str(hash(str(video_source))) - else: - raise TypeError( - "Only strings or ints representing cameras, or numpy arrays representing pictures supported.") + self.cam_id = uid_for_source(video_source) self.video_source = video_source if callable(callbacks): self.callbacks = [callbacks] @@ -68,17 +62,22 @@ 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_c = None for c in self.callbacks: try: - frame_c = c(frame, self.cam_id) + 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: - import traceback + self.exception_raised = e + frame = frame_c = self.exception_raised CamCtrl.stop_cam(self.cam_id) WinCtrl.quit() - self.exception_raised = e - frame_c = self.exception_raised - if frame_c is not None: - frame = frame_c + 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() diff --git a/cvpubsubs/window_sub/cv_window_sub.py b/cvpubsubs/window_sub/cv_window_sub.py index 4b30e43..7c32e4b 100644 --- a/cvpubsubs/window_sub/cv_window_sub.py +++ b/cvpubsubs/window_sub/cv_window_sub.py @@ -8,10 +8,11 @@ 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 -if False: - from typing import List, Union, Callable, Any - import numpy as np +from typing import List, Union, Callable, Any, Dict +import numpy as np +from cvpubsubs.callbacks import global_cv_display_callback class SubscriberWindows(object): @@ -24,23 +25,32 @@ class SubscriberWindows(object): video_sources=(0,), # type: List[Union[str,int]] callbacks=(None,), # type: List[Callable[[List[np.ndarray]], Any]] ): - self.window_names = window_names self.source_names = [] - for name in video_sources: - if isinstance(name, np.ndarray): - self.source_names.append(str(hash(str(name)))) - self.input_vid_global_names = [str(hash(str(name))) + "frame" for name in video_sources] - elif len(str(name)) <= 1000: - self.source_names.append(str(name)) - self.input_vid_global_names = [str(name) + "frame" for name in video_sources] - else: - raise ValueError("Input window name too long.") + 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 - self.input_cams = video_sources - for name in self.window_names: - cv2.namedWindow(name + " (press ESC to quit)") - cv2.setMouseCallback(name + " (press ESC to quit)", self.handle_mouse) + 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): @@ -76,13 +86,13 @@ class SubscriberWindows(object): mousey = MouseEvent(event, x, y, flags, param) WinCtrl.mouse_pub.publish(mousey) - def _display_frames(self, frames, win_num): + 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) + 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 @@ -94,12 +104,39 @@ class SubscriberWindows(object): 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: - frames = self.callbacks[i % len(self.callbacks)](self.frame_dict[self.input_vid_global_names[i]]) + self.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]] - if isinstance(frames, np.ndarray) and len(frames.shape) <= 3: - frames = [frames] - win_num = self._display_frames(frames, win_num) + 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): @@ -107,22 +144,57 @@ class SubscriberWindows(object): msg_cmd = '' key = '' while msg_cmd != 'quit' and key != 'quit': - self.update_window_frames() - msg_cmd = sub_cmd.get() - key = self.handle_keys(cv2.waitKey(1)) + msg_cmd, key = self.update() sub_cmd.release() WinCtrl.quit(force_all_read=False) self.__stop_all_cams() -def display(*vids, names=[]): - vid_threads = [VideoHandlerThread(v) for v in vids] +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(names) == 0: - names = ["window {}".format(i) for i in range(len(vids))] - SubscriberWindows(window_names=names, - video_sources=vids - ).loop() - for v in vid_threads: - v.join() + 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/tests/test_simple_api.py b/tests/test_simple_api.py new file mode 100644 index 0000000..b8893d3 --- /dev/null +++ b/tests/test_simple_api.py @@ -0,0 +1,61 @@ +import unittest as ut + +class TestSubWin(ut.TestCase): + + def test_display_numpy(self): + from cvpubsubs import display + import numpy as np + + display(np.random.normal(0.5, .1, (500,500,3))) + + def test_display_numpy_callback(self): + from cvpubsubs import display + import numpy as np + + arr = np.random.normal(0.5, .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 + + display(arr, callbacks= fix_arr_cv, blocking=True) + + def test_display_numpy_loop(self): + from cvpubsubs import display + import numpy as np + + arr = np.random.normal(0.5, .1, (500, 500, 3)) + + displayer, ids = display(arr, blocking = False) + + while True: + arr[:] += np.random.normal(0.01, .005, (500, 500, 3)) + arr %= 1.0 + displayer.update(arr, ids[0]) + displayer.end() + + def test_display_tensorflow(self): + from cvpubsubs 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.wait_for_init() + autoencoder = models.Sequential() + autoencoder.add( + 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) + 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") From 51e0d15c873a9bf3251fdfa2c93fdf23d1211cd0 Mon Sep 17 00:00:00 2001 From: simleek Date: Sun, 29 Sep 2019 22:33:41 -0700 Subject: [PATCH 56/59] Fixed cam ctrl using its own hashing system --- cvpubsubs/webcam_pub/pub_cam.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cvpubsubs/webcam_pub/pub_cam.py b/cvpubsubs/webcam_pub/pub_cam.py index 2808c4c..9f1384d 100644 --- a/cvpubsubs/webcam_pub/pub_cam.py +++ b/cvpubsubs/webcam_pub/pub_cam.py @@ -6,6 +6,7 @@ import numpy as np from cvpubsubs.webcam_pub.camctrl import CamCtrl from .np_cam import NpCam +from cvpubsubs.serialize import uid_for_source if False: from typing import Union, Tuple @@ -28,12 +29,7 @@ def pub_cam_loop(cam_id, # type: Union[int, str] :return: True if loop ended normally, False if it failed somehow. """ - if isinstance(cam_id, (int, str)): - name = str(cam_id) - elif isinstance(cam_id, np.ndarray): - name = str(hash(str(cam_id))) - else: - raise TypeError("Only strings or ints representing cameras, or numpy arrays representing pictures supported.") + name = uid_for_source(cam_id) if isinstance(cam_id, (int, str)): cam = cv2.VideoCapture(cam_id) From adb644789ee438ca92abb4d402be4805b4545d6f Mon Sep 17 00:00:00 2001 From: simleek Date: Sun, 29 Sep 2019 22:51:00 -0700 Subject: [PATCH 57/59] modified tests so the important ones passed --- cvpubsubs/callbacks.py | 2 +- cvpubsubs/webcam_pub/frame_handler.py | 5 --- tests/test_sub_win.py | 51 +++++---------------------- 3 files changed, 9 insertions(+), 49 deletions(-) diff --git a/cvpubsubs/callbacks.py b/cvpubsubs/callbacks.py index d95f835..a8d6753 100644 --- a/cvpubsubs/callbacks.py +++ b/cvpubsubs/callbacks.py @@ -45,7 +45,7 @@ class function_display_callback(object): # NOSONAR if finished: WinCtrl.quit() - def _display_internal(self, frame, cam_id, *args, **kwargs): + def _display_internal(self, frame, *args, **kwargs): finished = True if self.first_call: # return to display initial frame diff --git a/cvpubsubs/webcam_pub/frame_handler.py b/cvpubsubs/webcam_pub/frame_handler.py index 84f02c7..0f9ae0e 100644 --- a/cvpubsubs/webcam_pub/frame_handler.py +++ b/cvpubsubs/webcam_pub/frame_handler.py @@ -94,11 +94,6 @@ class VideoHandlerThread(threading.Thread): :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/tests/test_sub_win.py b/tests/test_sub_win.py index 698b7f8..e0931f9 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -42,7 +42,6 @@ class TestSubWin(ut.TestCase): 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 @@ -51,14 +50,14 @@ class TestSubWin(ut.TestCase): video_thread.display() def test_sub_with_callback(self): - def redden_frame_print_spam(frame, cam_id): + def redden_frame_print_spam(frame): frame[:, :, 0] = 0 frame[:, :, 2] = 0 w.VideoHandlerThread(callbacks=redden_frame_print_spam).display() def test_sub_with_callback_exception(self): - def redden_frame_print_spam(frame, cam_id): + def redden_frame_print_spam(frame): frame[:, :, 0] = 0 frame[:, :, 2] = 1 / 0 @@ -68,54 +67,20 @@ class TestSubWin(ut.TestCase): self.assertEqual(v.exception_raised, e) def test_multi_cams_one_source(self): - def cam_handler(frame, cam_id): - SubscriberWindows.set_global_frame_dict(cam_id, frame, frame) + display(0, window_names=['cammy','cammy2'], blocking=True) - 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() - - @ut.skip("I don't have stereo cams... :(") def test_multi_cams_multi_source(self): - t1 = w.VideoHandlerThread(0, request_size=(1920, 1080)) - t2 = w.VideoHandlerThread(1, request_size=(1920, 1080)) - - t1.start() - t2.start() - - SubscriberWindows(window_names=['cammy', 'cammy2'], - video_sources=[0, 1] - ).loop() - - t1.join() - t1.join() + display(0, np.random.uniform(0.0, 1.0, (500,500)), blocking=True) def test_nested_frames(self): - def nest_frame(frame, cam_id): + def nest_frame(frame): frame = np.asarray([[[[[[frame]]]]], [[[[[frame]]], [[[frame]]]]]]) return frame - v = w.VideoHandlerThread(callbacks=[nest_frame] + w.display_callbacks) - v.start() - - SubscriberWindows(window_names=[str(i) for i in range(3)], - video_sources=[str(0)] - ).loop() - - v.join() + display(0, callbacks=nest_frame, window_names=["1", "2", "3"], blocking=True) def test_nested_frames_exception(self): - def nest_frame(frame, cam_id): + def nest_frame(frame): frame = np.asarray([[[[[[frame + 1 / 0]]]]], [[[[[frame]]], [[[frame]]]]]]) return frame @@ -176,4 +141,4 @@ class TestSubWin(ut.TestCase): t1.join() def test_display(self): - display(np.ones((100, 100)), np.zeros((100, 100))) + display(np.ones((100, 100)), np.zeros((100, 100)), blocking=True) From bfd0de619b3e49324ed762677f798c2804c94f06 Mon Sep 17 00:00:00 2001 From: simleek Date: Mon, 30 Sep 2019 22:25:45 -0700 Subject: [PATCH 58/59] Used mypy, pydocstyle, pycodestyle, sonarlint, removed py 2.7, and renamed. --- .coveragerc | 4 +- .travis.yml | 2 - README.md | 34 +-- __init__.py | 2 +- cvpubsubs/__init__.py | 3 - cvpubsubs/callbacks.py | 67 ----- cvpubsubs/input.py | 95 ------- cvpubsubs/serialize.py | 12 - cvpubsubs/webcam_pub/__init__.py | 5 - cvpubsubs/webcam_pub/camctrl.py | 68 ----- cvpubsubs/webcam_pub/frame_handler.py | 101 -------- cvpubsubs/window_sub/__init__.py | 1 - cvpubsubs/window_sub/cv_window_sub.py | 200 -------------- cvpubsubs/window_sub/winctrl.py | 18 -- displayarray/__init__.py | 9 + displayarray/callbacks.py | 72 +++++ displayarray/input.py | 104 ++++++++ displayarray/uid.py | 18 ++ displayarray/webcam_pub/__init__.py | 15 ++ displayarray/webcam_pub/camctrl.py | 78 ++++++ displayarray/webcam_pub/frame_handler.py | 100 +++++++ .../webcam_pub/get_cam_ids.py | 8 +- .../webcam_pub/np_cam.py | 9 +- .../webcam_pub/pub_cam.py | 67 ++--- displayarray/window_sub/__init__.py | 7 + displayarray/window_sub/cv_window_sub.py | 245 ++++++++++++++++++ .../window_sub/mouse_event.py | 6 +- displayarray/window_sub/winctrl.py | 18 ++ mypy.ini | 11 + pyproject.toml | 6 +- setup.py | 66 ++--- tests/test_pub_cam.py | 16 +- tests/test_simple_api.py | 44 ++-- tests/test_sub_win.py | 77 +++--- tox.ini | 47 +++- 35 files changed, 894 insertions(+), 741 deletions(-) delete mode 100644 cvpubsubs/__init__.py delete mode 100644 cvpubsubs/callbacks.py delete mode 100644 cvpubsubs/input.py delete mode 100644 cvpubsubs/serialize.py delete mode 100644 cvpubsubs/webcam_pub/__init__.py delete mode 100644 cvpubsubs/webcam_pub/camctrl.py delete mode 100644 cvpubsubs/webcam_pub/frame_handler.py delete mode 100644 cvpubsubs/window_sub/__init__.py delete mode 100644 cvpubsubs/window_sub/cv_window_sub.py delete mode 100644 cvpubsubs/window_sub/winctrl.py create mode 100644 displayarray/__init__.py create mode 100644 displayarray/callbacks.py create mode 100644 displayarray/input.py create mode 100644 displayarray/uid.py create mode 100644 displayarray/webcam_pub/__init__.py create mode 100644 displayarray/webcam_pub/camctrl.py create mode 100644 displayarray/webcam_pub/frame_handler.py rename {cvpubsubs => displayarray}/webcam_pub/get_cam_ids.py (56%) rename {cvpubsubs => displayarray}/webcam_pub/np_cam.py (74%) rename {cvpubsubs => displayarray}/webcam_pub/pub_cam.py (53%) create mode 100644 displayarray/window_sub/__init__.py create mode 100644 displayarray/window_sub/cv_window_sub.py rename {cvpubsubs => displayarray}/window_sub/mouse_event.py (71%) create mode 100644 displayarray/window_sub/winctrl.py create mode 100644 mypy.ini 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..0cff629 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 webcam_pub, window_sub 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/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..69d9530 --- /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 .window_sub.cv_window_sub import display diff --git a/displayarray/callbacks.py b/displayarray/callbacks.py new file mode 100644 index 0000000..4fbb34f --- /dev/null +++ b/displayarray/callbacks.py @@ -0,0 +1,72 @@ +from displayarray.window_sub import winctrl +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.window_sub 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): + 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): + """Call the function "function_display_callback" was set up with.""" + return self.inner_function(self, *args, **kwargs) diff --git a/displayarray/input.py b/displayarray/input.py new file mode 100644 index 0000000..b7c5ac3 --- /dev/null +++ b/displayarray/input.py @@ -0,0 +1,104 @@ +from displayarray.window_sub import winctrl +import threading +import time + +from typing import Callable +from displayarray.window_sub.mouse_event import MouseEvent + + +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 = winctrl.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 = 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): + """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) + winctrl.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 = winctrl.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 = 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): + """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) + winctrl.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/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/displayarray/webcam_pub/__init__.py b/displayarray/webcam_pub/__init__.py new file mode 100644 index 0000000..e0c1e9c --- /dev/null +++ b/displayarray/webcam_pub/__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 camctrl +from .frame_handler import VideoHandlerThread +from .get_cam_ids import get_cam_ids +from .np_cam import NpCam +from .pub_cam import pub_cam_thread diff --git a/displayarray/webcam_pub/camctrl.py b/displayarray/webcam_pub/camctrl.py new file mode 100644 index 0000000..f47f527 --- /dev/null +++ b/displayarray/webcam_pub/camctrl.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/webcam_pub/frame_handler.py b/displayarray/webcam_pub/frame_handler.py new file mode 100644 index 0000000..973dd44 --- /dev/null +++ b/displayarray/webcam_pub/frame_handler.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.webcam_pub import camctrl +from displayarray.webcam_pub.pub_cam import pub_cam_thread +from displayarray.window_sub import winctrl + +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] = (-1, -1), + 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 camctrl.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 + 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) + + 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 = 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 + self.__apply_callbacks_to_frame(frame) + msg_owner = sub_owner.get() + sub_owner.release() + sub_cam.release() + camctrl.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.window_sub 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/webcam_pub/get_cam_ids.py similarity index 56% rename from cvpubsubs/webcam_pub/get_cam_ids.py rename to displayarray/webcam_pub/get_cam_ids.py index e355951..ac95b18 100644 --- a/cvpubsubs/webcam_pub/get_cam_ids.py +++ b/displayarray/webcam_pub/get_cam_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/webcam_pub/np_cam.py similarity index 74% rename from cvpubsubs/webcam_pub/np_cam.py rename to displayarray/webcam_pub/np_cam.py index 34517f8..3366aed 100644 --- a/cvpubsubs/webcam_pub/np_cam.py +++ b/displayarray/webcam_pub/np_cam.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/cvpubsubs/webcam_pub/pub_cam.py b/displayarray/webcam_pub/pub_cam.py similarity index 53% rename from cvpubsubs/webcam_pub/pub_cam.py rename to displayarray/webcam_pub/pub_cam.py index 9f1384d..c5717a0 100644 --- a/cvpubsubs/webcam_pub/pub_cam.py +++ b/displayarray/webcam_pub/pub_cam.py @@ -4,48 +4,50 @@ import time import cv2 import numpy as np -from cvpubsubs.webcam_pub.camctrl import CamCtrl +from displayarray.webcam_pub import camctrl from .np_cam import NpCam -from cvpubsubs.serialize import uid_for_source +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) + camctrl.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 = camctrl.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") + camctrl.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") + camctrl.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) + camctrl.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/window_sub/__init__.py b/displayarray/window_sub/__init__.py new file mode 100644 index 0000000..b0b156a --- /dev/null +++ b/displayarray/window_sub/__init__.py @@ -0,0 +1,7 @@ +""" +Displays arrays. + +SubscriberWindows displays one array per window, updating it as it's changed. +""" + +from .cv_window_sub import SubscriberWindows diff --git a/displayarray/window_sub/cv_window_sub.py b/displayarray/window_sub/cv_window_sub.py new file mode 100644 index 0000000..6b11179 --- /dev/null +++ b/displayarray/window_sub/cv_window_sub.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.webcam_pub import camctrl +from displayarray.webcam_pub.frame_handler import FrameCallable +from displayarray.webcam_pub.frame_handler import VideoHandlerThread +from displayarray.window_sub.mouse_event import MouseEvent +from displayarray.window_sub import winctrl + + +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: + camctrl.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)") + 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): + """Capture mouse input for mouse control subscriber threads.""" + 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): + """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 = 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): + """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.""" + winctrl.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 = 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() + + +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/cvpubsubs/window_sub/mouse_event.py b/displayarray/window_sub/mouse_event.py similarity index 71% rename from cvpubsubs/window_sub/mouse_event.py rename to displayarray/window_sub/mouse_event.py index b4eb31f..9713bd6 100644 --- a/cvpubsubs/window_sub/mouse_event.py +++ b/displayarray/window_sub/mouse_event.py @@ -1,4 +1,6 @@ class MouseEvent(object): + """Holds all the OpenCV mouse event information.""" + def __init__(self, event, x, y, flags, param): self.event = event self.x = x @@ -10,4 +12,6 @@ class MouseEvent(object): return self.__str__() def __str__(self): - return "event:{}\nx,y:{},{}\nflags:{}\nparam:{}\n".format(self.event, self.x, self.y, self.flags, self.param) + return "event:{}\nx,y:{},{}\nflags:{}\nparam:{}\n".format( + self.event, self.x, self.y, self.flags, self.param + ) diff --git a/displayarray/window_sub/winctrl.py b/displayarray/window_sub/winctrl.py new file mode 100644 index 0000000..2a0919c --- /dev/null +++ b/displayarray/window_sub/winctrl.py @@ -0,0 +1,18 @@ +import threading +import logging + +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/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..464b134 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.webcam_pub 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.camctrl.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..1caddbd 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -1,20 +1,15 @@ -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.webcam_pub as w +from displayarray.window_sub import SubscriberWindows +from displayarray import display +from displayarray.input import mouse_loop, key_loop -if False: - import numpy as np - from cvpubsubs.window_sub.mouse_event import MouseEvent +import numpy as np +from displayarray.window_sub.mouse_event import MouseEvent class TestSubWin(ut.TestCase): - def test_mouse_loop(self): @mouse_loop def print_mouse_thread(mouse_event): @@ -41,11 +36,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 +60,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 +77,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.webcam_pub 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 +115,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 +143,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 From 85603d990e84f02544294478cc0f80f59cb015e9 Mon Sep 17 00:00:00 2001 From: simleek Date: Mon, 30 Sep 2019 22:45:07 -0700 Subject: [PATCH 59/59] renamed some files to make it easier to understand --- __init__.py | 2 +- displayarray/__init__.py | 2 +- displayarray/callbacks.py | 8 ++-- .../__init__.py | 10 ++--- .../frame_publishing.py} | 14 +++---- .../frame_update_thread.py} | 22 +++++------ .../get_frame_ids.py} | 0 .../np_to_opencv.py} | 0 .../subscriber_dictionary.py} | 0 displayarray/input.py | 38 ++++++++++++++----- .../__init__.py | 2 +- .../subscriber_windows.py} | 26 ++++++------- .../window_commands.py} | 3 -- displayarray/window_sub/mouse_event.py | 17 --------- tests/test_pub_cam.py | 4 +- tests/test_sub_win.py | 9 ++--- 16 files changed, 77 insertions(+), 80 deletions(-) rename displayarray/{webcam_pub => frame_publising}/__init__.py (68%) rename displayarray/{webcam_pub/pub_cam.py => frame_publising/frame_publishing.py} (86%) rename displayarray/{webcam_pub/frame_handler.py => frame_publising/frame_update_thread.py} (81%) rename displayarray/{webcam_pub/get_cam_ids.py => frame_publising/get_frame_ids.py} (100%) rename displayarray/{webcam_pub/np_cam.py => frame_publising/np_to_opencv.py} (100%) rename displayarray/{webcam_pub/camctrl.py => frame_publising/subscriber_dictionary.py} (100%) rename displayarray/{window_sub => subscriber_window}/__init__.py (67%) rename displayarray/{window_sub/cv_window_sub.py => subscriber_window/subscriber_windows.py} (92%) rename displayarray/{window_sub/winctrl.py => subscriber_window/window_commands.py} (92%) delete mode 100644 displayarray/window_sub/mouse_event.py diff --git a/__init__.py b/__init__.py index 0cff629..77c0b32 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ # redirection, so we can use subtree like pip -from displayarray import webcam_pub, window_sub +from displayarray import frame_publising, subscriber_window diff --git a/displayarray/__init__.py b/displayarray/__init__.py index 69d9530..3ec18ba 100644 --- a/displayarray/__init__.py +++ b/displayarray/__init__.py @@ -6,4 +6,4 @@ display is a function that displays these in their own windows. __version__ = "0.6.6" -from .window_sub.cv_window_sub import display +from .subscriber_window.subscriber_windows import display diff --git a/displayarray/callbacks.py b/displayarray/callbacks.py index 4fbb34f..1612a27 100644 --- a/displayarray/callbacks.py +++ b/displayarray/callbacks.py @@ -1,4 +1,4 @@ -from displayarray.window_sub import winctrl +from displayarray.subscriber_window import window_commands import numpy as np from typing import Union @@ -13,7 +13,7 @@ def global_cv_display_callback(frame: np.ndarray, cam_id: Union[int, str]): :param cam_id: The video or image source :type cam_id: Union[int, str] """ - from displayarray.window_sub import SubscriberWindows + from displayarray.subscriber_window import SubscriberWindows SubscriberWindows.FRAME_DICT[str(cam_id) + "frame"] = frame @@ -41,11 +41,11 @@ class function_display_callback(object): # NOSONAR def _run_finisher(self, frame, finished, *args, **kwargs): if not callable(finish_function): - winctrl.quit() + window_commands.quit() else: finished = finish_function(frame, Ellipsis, finished, *args, **kwargs) if finished: - winctrl.quit() + window_commands.quit() def _display_internal(self, frame, *args, **kwargs): finished = True diff --git a/displayarray/webcam_pub/__init__.py b/displayarray/frame_publising/__init__.py similarity index 68% rename from displayarray/webcam_pub/__init__.py rename to displayarray/frame_publising/__init__.py index e0c1e9c..d1af476 100644 --- a/displayarray/webcam_pub/__init__.py +++ b/displayarray/frame_publising/__init__.py @@ -8,8 +8,8 @@ pub_cam_thread continually publishes updates to arrays, videos, and cameras np_cam simulates numpy arrays as OpenCV cameras """ -from . import camctrl -from .frame_handler import VideoHandlerThread -from .get_cam_ids import get_cam_ids -from .np_cam import NpCam -from .pub_cam import pub_cam_thread +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/displayarray/webcam_pub/pub_cam.py b/displayarray/frame_publising/frame_publishing.py similarity index 86% rename from displayarray/webcam_pub/pub_cam.py rename to displayarray/frame_publising/frame_publishing.py index c5717a0..c1bfe44 100644 --- a/displayarray/webcam_pub/pub_cam.py +++ b/displayarray/frame_publising/frame_publishing.py @@ -4,8 +4,8 @@ import time import cv2 import numpy as np -from displayarray.webcam_pub import camctrl -from .np_cam import NpCam +from displayarray.frame_publising import subscriber_dictionary +from .np_to_opencv import NpCam from displayarray.uid import uid_for_source from typing import Union, Tuple @@ -40,12 +40,12 @@ def pub_cam_loop( "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 = subscriber_dictionary.cam_cmd_sub(name) sub.return_on_no_data = "" msg = "" @@ -56,7 +56,7 @@ def pub_cam_loop( 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": @@ -65,14 +65,14 @@ def pub_cam_loop( (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() diff --git a/displayarray/webcam_pub/frame_handler.py b/displayarray/frame_publising/frame_update_thread.py similarity index 81% rename from displayarray/webcam_pub/frame_handler.py rename to displayarray/frame_publising/frame_update_thread.py index 973dd44..8385f01 100644 --- a/displayarray/webcam_pub/frame_handler.py +++ b/displayarray/frame_publising/frame_update_thread.py @@ -5,9 +5,9 @@ import numpy as np from displayarray.callbacks import global_cv_display_callback from displayarray.uid import uid_for_source -from displayarray.webcam_pub import camctrl -from displayarray.webcam_pub.pub_cam import pub_cam_thread -from displayarray.window_sub import winctrl +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]] @@ -19,7 +19,7 @@ class VideoHandlerThread(threading.Thread): self, video_source: Union[int, str, np.ndarray] = 0, callbacks: Optional[Union[List[FrameCallable], FrameCallable]] = None, - request_size: Tuple[int, int] = (-1, -1), + request_size: Tuple[int, int] = (99999, 99999), high_speed: bool = True, fps_limit: float = 240, ): @@ -38,7 +38,7 @@ class VideoHandlerThread(threading.Thread): self.exception_raised = None def __wait_for_cam_id(self): - while str(self.cam_id) not in camctrl.CV_CAMS_DICT: + while str(self.cam_id) not in subscriber_dictionary.CV_CAMS_DICT: continue def __apply_callbacks_to_frame(self, frame): @@ -54,8 +54,8 @@ class VideoHandlerThread(threading.Thread): except Exception as e: self.exception_raised = e frame = frame_c = self.exception_raised - camctrl.stop_cam(self.cam_id) - winctrl.quit() + 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) @@ -69,8 +69,8 @@ class VideoHandlerThread(threading.Thread): ) self.__wait_for_cam_id() - sub_cam = camctrl.cam_frame_sub(str(self.cam_id)) - sub_owner = camctrl.handler_cmd_sub(str(self.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 @@ -78,7 +78,7 @@ class VideoHandlerThread(threading.Thread): msg_owner = sub_owner.get() sub_owner.release() sub_cam.release() - camctrl.stop_cam(self.cam_id) + subscriber_dictionary.stop_cam(self.cam_id) t.join() def display(self, callbacks: List[Callable[[np.ndarray], Any]] = None): @@ -89,7 +89,7 @@ class VideoHandlerThread(threading.Thread): :param callbacks: List of callbacks to be run on frames before displaying to the screen. """ - from displayarray.window_sub import SubscriberWindows + from displayarray.subscriber_window import SubscriberWindows if callbacks is None: callbacks = [] diff --git a/displayarray/webcam_pub/get_cam_ids.py b/displayarray/frame_publising/get_frame_ids.py similarity index 100% rename from displayarray/webcam_pub/get_cam_ids.py rename to displayarray/frame_publising/get_frame_ids.py diff --git a/displayarray/webcam_pub/np_cam.py b/displayarray/frame_publising/np_to_opencv.py similarity index 100% rename from displayarray/webcam_pub/np_cam.py rename to displayarray/frame_publising/np_to_opencv.py diff --git a/displayarray/webcam_pub/camctrl.py b/displayarray/frame_publising/subscriber_dictionary.py similarity index 100% rename from displayarray/webcam_pub/camctrl.py rename to displayarray/frame_publising/subscriber_dictionary.py diff --git a/displayarray/input.py b/displayarray/input.py index b7c5ac3..ff19b1f 100644 --- a/displayarray/input.py +++ b/displayarray/input.py @@ -1,9 +1,27 @@ -from displayarray.window_sub import winctrl +from displayarray.subscriber_window import window_commands import threading import time from typing import Callable -from displayarray.window_sub.mouse_event import MouseEvent + + +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 @@ -11,7 +29,7 @@ class _mouse_thread(object): # NOSONAR def __init__(self, f): self.f = f - self.sub_mouse = winctrl.mouse_pub.make_sub() + self.sub_mouse = window_commands.mouse_pub.make_sub() def __call__(self, *args, **kwargs): """Call the function this was set up with.""" @@ -23,8 +41,8 @@ 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_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 @@ -40,7 +58,7 @@ class _mouse_loop_thread(object): # NOSONAR self.f(None, *args, **kwargs) msg_cmd = self.sub_cmd.get() time.sleep(1.0 / self.fps) - winctrl.quit(force_all_read=False) + window_commands.quit(force_all_read=False) class mouse_loop(object): # NOSONAR @@ -60,7 +78,7 @@ class _key_thread(object): # NOSONAR def __init__(self, f): self.f = f - self.sub_key = winctrl.key_pub.make_sub() + self.sub_key = window_commands.key_pub.make_sub() def __call__(self, *args, **kwargs): """Call the function this was set up with.""" @@ -72,8 +90,8 @@ 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_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 @@ -89,7 +107,7 @@ class _key_loop_thread(object): # NOSONAR self.f(None, *args, **kwargs) msg_cmd = self.sub_cmd.get() time.sleep(1.0 / self.fps) - winctrl.quit(force_all_read=False) + window_commands.quit(force_all_read=False) class key_loop(object): # NOSONAR diff --git a/displayarray/window_sub/__init__.py b/displayarray/subscriber_window/__init__.py similarity index 67% rename from displayarray/window_sub/__init__.py rename to displayarray/subscriber_window/__init__.py index b0b156a..6df33c0 100644 --- a/displayarray/window_sub/__init__.py +++ b/displayarray/subscriber_window/__init__.py @@ -4,4 +4,4 @@ Displays arrays. SubscriberWindows displays one array per window, updating it as it's changed. """ -from .cv_window_sub import SubscriberWindows +from .subscriber_windows import SubscriberWindows diff --git a/displayarray/window_sub/cv_window_sub.py b/displayarray/subscriber_window/subscriber_windows.py similarity index 92% rename from displayarray/window_sub/cv_window_sub.py rename to displayarray/subscriber_window/subscriber_windows.py index 6b11179..472835c 100644 --- a/displayarray/window_sub/cv_window_sub.py +++ b/displayarray/subscriber_window/subscriber_windows.py @@ -8,11 +8,11 @@ from localpubsub import NoData from displayarray.callbacks import global_cv_display_callback from displayarray.uid import uid_for_source -from displayarray.webcam_pub import camctrl -from displayarray.webcam_pub.frame_handler import FrameCallable -from displayarray.webcam_pub.frame_handler import VideoHandlerThread -from displayarray.window_sub.mouse_event import MouseEvent -from displayarray.window_sub import winctrl +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): @@ -61,7 +61,7 @@ class SubscriberWindows(object): def __stop_all_cams(self): for c in self.source_names: - camctrl.stop_cam(c) + subscriber_dictionary.stop_cam(c) def handle_keys( self, key_input # type: int @@ -70,12 +70,12 @@ class SubscriberWindows(object): if key_input in self.ESC_KEY_CODES: for name in self.window_names: cv2.destroyWindow(name + " (press ESC to quit)") - winctrl.quit() + window_commands.quit() self.__stop_all_cams() return "quit" elif key_input not in [-1, 0]: try: - winctrl.key_pub.publish(chr(key_input)) + window_commands.key_pub.publish(chr(key_input)) except ValueError: warnings.warn( RuntimeWarning( @@ -88,7 +88,7 @@ class SubscriberWindows(object): def handle_mouse(self, event, x, y, flags, param): """Capture mouse input for mouse control subscriber threads.""" mousey = MouseEvent(event, x, y, flags, param) - winctrl.mouse_pub.publish(mousey) + window_commands.mouse_pub.publish(mousey) def _display_frames(self, frames, win_num, ids=None): if isinstance(frames, Exception): @@ -137,7 +137,7 @@ class SubscriberWindows(object): if id not in self.input_cams: self.add_source(id) self.add_window(id) - sub_cmd = winctrl.win_cmd_sub() + sub_cmd = window_commands.win_cmd_sub() self.update_window_frames() msg_cmd = sub_cmd.get() key = self.handle_keys(cv2.waitKey(1)) @@ -152,7 +152,7 @@ class SubscriberWindows(object): def end(self): """Close all threads. Should be used with non-blocking mode.""" - winctrl.quit(force_all_read=False) + window_commands.quit(force_all_read=False) self.__stop_all_cams() if self.close_threads is not None: for t in self.close_threads: @@ -163,13 +163,13 @@ class SubscriberWindows(object): def loop(self): """Continually update window frame. OpenCV only allows this in the main thread.""" - sub_cmd = winctrl.win_cmd_sub() + 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() - winctrl.quit(force_all_read=False) + window_commands.quit(force_all_read=False) self.__stop_all_cams() diff --git a/displayarray/window_sub/winctrl.py b/displayarray/subscriber_window/window_commands.py similarity index 92% rename from displayarray/window_sub/winctrl.py rename to displayarray/subscriber_window/window_commands.py index 2a0919c..f2015d1 100644 --- a/displayarray/window_sub/winctrl.py +++ b/displayarray/subscriber_window/window_commands.py @@ -1,6 +1,3 @@ -import threading -import logging - from localpubsub import VariablePub, VariableSub key_pub = VariablePub() diff --git a/displayarray/window_sub/mouse_event.py b/displayarray/window_sub/mouse_event.py deleted file mode 100644 index 9713bd6..0000000 --- a/displayarray/window_sub/mouse_event.py +++ /dev/null @@ -1,17 +0,0 @@ -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 - ) diff --git a/tests/test_pub_cam.py b/tests/test_pub_cam.py index 464b134..fe0435f 100644 --- a/tests/test_pub_cam.py +++ b/tests/test_pub_cam.py @@ -1,4 +1,4 @@ -import displayarray.webcam_pub as w +import displayarray.frame_publising as w import unittest as ut @@ -8,7 +8,7 @@ class TestFrameHandler(ut.TestCase): 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 diff --git a/tests/test_sub_win.py b/tests/test_sub_win.py index 1caddbd..c06538d 100644 --- a/tests/test_sub_win.py +++ b/tests/test_sub_win.py @@ -1,12 +1,11 @@ import unittest as ut -import displayarray.webcam_pub as w -from displayarray.window_sub import SubscriberWindows +import displayarray.frame_publising as w +from displayarray.subscriber_window import SubscriberWindows from displayarray import display -from displayarray.input import mouse_loop, key_loop +from displayarray.input import mouse_loop, key_loop, MouseEvent import numpy as np -from displayarray.window_sub.mouse_event import MouseEvent class TestSubWin(ut.TestCase): @@ -89,7 +88,7 @@ class TestSubWin(ut.TestCase): v.join() def test_conway_life(self): - from displayarray.webcam_pub import VideoHandlerThread + from displayarray.frame_publising import VideoHandlerThread from displayarray.callbacks import function_display_callback import numpy as np import cv2