+2
-2
@@ -1,10 +1,10 @@
|
||||
[run]
|
||||
source =
|
||||
cvpubsubs
|
||||
displayarray
|
||||
tests
|
||||
branch = True
|
||||
omit =
|
||||
cvpubsubs/cli.py
|
||||
displayarray/cli.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <https://pypi.org>`_ as a universal
|
||||
displayarray is distributed on `PyPI <https://pypi.org>`_ 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 <https://choosealicense.com/licenses/mit>`_
|
||||
- `Apache License, Version 2.0 <https://choosealicense.com/licenses/apache-2.0>`_
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
# redirection, so we can use subtree like pip
|
||||
from cvpubsubs import webcam_pub, window_sub
|
||||
from displayarray import frame_publising, subscriber_window
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
__version__ = '0.6.5'
|
||||
|
||||
from .window_sub.cv_window_sub import display
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
from .cv_window_sub import SubscriberWindows
|
||||
@@ -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
|
||||
@@ -1,13 +0,0 @@
|
||||
class MouseEvent(object):
|
||||
def __init__(self, event, x, y, flags, param):
|
||||
self.event = event
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.flags = flags
|
||||
self.param = param
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return "event:{}\nx,y:{},{}\nflags:{}\nparam:{}\n".format(self.event, self.x, self.y, self.flags, self.param)
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Display any array, webcam, or video file.
|
||||
|
||||
display is a function that displays these in their own windows.
|
||||
"""
|
||||
|
||||
__version__ = "0.6.6"
|
||||
|
||||
from .subscriber_window.subscriber_windows import display
|
||||
@@ -0,0 +1,72 @@
|
||||
from displayarray.subscriber_window import window_commands
|
||||
import numpy as np
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
def global_cv_display_callback(frame: np.ndarray, cam_id: Union[int, str]):
|
||||
"""
|
||||
Send frames to the global frame dictionary.
|
||||
|
||||
:param frame: The video or image frame
|
||||
:type frame: np.ndarray
|
||||
:param cam_id: The video or image source
|
||||
:type cam_id: Union[int, str]
|
||||
"""
|
||||
from displayarray.subscriber_window import SubscriberWindows
|
||||
|
||||
SubscriberWindows.FRAME_DICT[str(cam_id) + "frame"] = frame
|
||||
|
||||
|
||||
class function_display_callback(object): # NOSONAR
|
||||
"""
|
||||
Used for running arbitrary functions on pixels.
|
||||
|
||||
>>> import random
|
||||
>>> from displayarray.webcam_pub import VideoHandlerThread
|
||||
>>> img = np.zeros((300, 300, 3))
|
||||
>>> def fun(array, coords, finished):
|
||||
... r,g,b = random.random()/20.0, random.random()/20.0, random.random()/20.0
|
||||
... array[coords[0:2]] = (array[coords[0:2]] + [r,g,b])%1.0
|
||||
>>> VideoHandlerThread(video_source=img, callbacks=function_display_callback(fun)).display()
|
||||
|
||||
:param display_function: a function to run on the input image.
|
||||
:param finish_function: a function to run on the input image when the other function finishes.
|
||||
"""
|
||||
|
||||
def __init__(self, display_function, finish_function=None):
|
||||
|
||||
self.looping = True
|
||||
self.first_call = True
|
||||
|
||||
def _run_finisher(self, frame, finished, *args, **kwargs):
|
||||
if not callable(finish_function):
|
||||
window_commands.quit()
|
||||
else:
|
||||
finished = finish_function(frame, Ellipsis, finished, *args, **kwargs)
|
||||
if finished:
|
||||
window_commands.quit()
|
||||
|
||||
def _display_internal(self, frame, *args, **kwargs):
|
||||
finished = True
|
||||
if self.first_call:
|
||||
# return to display initial frame
|
||||
self.first_call = False
|
||||
return
|
||||
if self.looping:
|
||||
it = np.nditer(frame, flags=["multi_index"])
|
||||
while not it.finished:
|
||||
x, y, c = it.multi_index
|
||||
finished = display_function(
|
||||
frame, (x, y, c), finished, *args, **kwargs
|
||||
)
|
||||
it.iternext()
|
||||
if finished:
|
||||
self.looping = False
|
||||
_run_finisher(self, frame, finished, *args, **kwargs)
|
||||
|
||||
self.inner_function = _display_internal
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Call the function "function_display_callback" was set up with."""
|
||||
return self.inner_function(self, *args, **kwargs)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Handles publishing arrays, videos, and cameras.
|
||||
|
||||
CamCtrl handles sending and receiving commands to specific camera (or array/video) publishers
|
||||
VideoHandlerThread updates the frames for the global displayer, since OpenCV can only update on the main thread
|
||||
get_cam_ids gets the ids for all cameras that OpenCV can detect
|
||||
pub_cam_thread continually publishes updates to arrays, videos, and cameras
|
||||
np_cam simulates numpy arrays as OpenCV cameras
|
||||
"""
|
||||
|
||||
from . import subscriber_dictionary
|
||||
from .frame_update_thread import VideoHandlerThread
|
||||
from .get_frame_ids import get_cam_ids
|
||||
from .np_to_opencv import NpCam
|
||||
from .frame_publishing import pub_cam_thread
|
||||
+37
-32
@@ -4,48 +4,50 @@ import time
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from cvpubsubs.webcam_pub.camctrl import CamCtrl
|
||||
from .np_cam import NpCam
|
||||
from cvpubsubs.serialize import uid_for_source
|
||||
from displayarray.frame_publising import subscriber_dictionary
|
||||
from .np_to_opencv import NpCam
|
||||
from displayarray.uid import uid_for_source
|
||||
|
||||
if False:
|
||||
from typing import Union, Tuple
|
||||
from typing import Union, Tuple
|
||||
|
||||
|
||||
def pub_cam_loop(cam_id, # type: Union[int, str]
|
||||
request_size=(1280, 720), # type: Tuple[int, int]
|
||||
high_speed=False, # type: bool
|
||||
fps_limit=240 # type: float
|
||||
): # type: (...)->bool
|
||||
"""Publishes whichever camera you select to CVCams.<cam_id>.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.<cam_id>.Vid.
|
||||
|
||||
You can send a quit command 'quit' to CVCams.<cam_id>.Cmd
|
||||
Status information, such as failure to open, will be posted to CVCams.<cam_id>.Status
|
||||
|
||||
|
||||
:param high_speed: Selects mjpeg transferring, which most cameras seem to support, so speed isn't limited
|
||||
:param fps_limit: Limits the frames per second.
|
||||
:param cam_id: An integer representing which webcam to use, or a string representing a video file.
|
||||
:param request_size: A tuple with width, then height, to request the video size.
|
||||
:return: True if loop ended normally, False if it failed somehow.
|
||||
"""
|
||||
|
||||
name = uid_for_source(cam_id)
|
||||
|
||||
if isinstance(cam_id, (int, str)):
|
||||
cam = cv2.VideoCapture(cam_id)
|
||||
cam: Union[NpCam, cv2.VideoCapture] = cv2.VideoCapture(cam_id)
|
||||
elif isinstance(cam_id, np.ndarray):
|
||||
cam = NpCam(cam_id) # type: NpCam
|
||||
cam = NpCam(cam_id)
|
||||
else:
|
||||
raise TypeError("Only strings or ints representing cameras, or numpy arrays representing pictures supported.")
|
||||
raise TypeError(
|
||||
"Only strings or ints representing cameras, or numpy arrays representing pictures supported."
|
||||
)
|
||||
|
||||
CamCtrl.register_cam(name)
|
||||
subscriber_dictionary.register_cam(name)
|
||||
|
||||
# cam.set(cv2.CAP_PROP_CONVERT_RGB, 0)
|
||||
frame_counter = 0
|
||||
|
||||
sub = CamCtrl.cam_cmd_sub(name)
|
||||
sub.return_on_no_data = ''
|
||||
msg = ''
|
||||
sub = subscriber_dictionary.cam_cmd_sub(name)
|
||||
sub.return_on_no_data = ""
|
||||
msg = ""
|
||||
|
||||
if high_speed:
|
||||
cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG)
|
||||
@@ -54,23 +56,23 @@ def pub_cam_loop(cam_id, # type: Union[int, str]
|
||||
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, request_size[1])
|
||||
|
||||
if not cam.isOpened():
|
||||
CamCtrl.cv_cams_dict[name].status_pub.publish("failed")
|
||||
subscriber_dictionary.CV_CAMS_DICT[name].status_pub.publish("failed")
|
||||
return False
|
||||
now = time.time()
|
||||
while msg != 'quit':
|
||||
time.sleep(1. / (fps_limit - (time.time() - now)))
|
||||
while msg != "quit":
|
||||
time.sleep(1.0 / (fps_limit - (time.time() - now)))
|
||||
now = time.time()
|
||||
(ret, frame) = cam.read() # type: Tuple[bool, np.ndarray ]
|
||||
if ret is False or not isinstance(frame, np.ndarray):
|
||||
cam.release()
|
||||
CamCtrl.cv_cams_dict[name].status_pub.publish("failed")
|
||||
subscriber_dictionary.CV_CAMS_DICT[name].status_pub.publish("failed")
|
||||
return False
|
||||
if cam.get(cv2.CAP_PROP_FRAME_COUNT) > 0:
|
||||
frame_counter += 1
|
||||
if frame_counter >= cam.get(cv2.CAP_PROP_FRAME_COUNT):
|
||||
frame_counter = 0
|
||||
cam = cv2.VideoCapture(cam_id)
|
||||
CamCtrl.cv_cams_dict[name].frame_pub.publish(frame)
|
||||
subscriber_dictionary.CV_CAMS_DICT[name].frame_pub.publish(frame)
|
||||
msg = sub.get()
|
||||
sub.release()
|
||||
|
||||
@@ -78,12 +80,15 @@ def pub_cam_loop(cam_id, # type: Union[int, str]
|
||||
return True
|
||||
|
||||
|
||||
def pub_cam_thread(cam_id, # type: Union[int, str]
|
||||
request_ize=(1280, 720), # type: Tuple[int, int]
|
||||
high_speed=False, # type: bool
|
||||
fps_limit=240 # type: float
|
||||
):
|
||||
# type: (...) -> threading.Thread
|
||||
t = threading.Thread(target=pub_cam_loop, args=(cam_id, request_ize, high_speed, fps_limit))
|
||||
def pub_cam_thread(
|
||||
cam_id: Union[int, str],
|
||||
request_ize: Tuple[int, int] = (1280, 720),
|
||||
high_speed: bool = False,
|
||||
fps_limit: float = 240,
|
||||
) -> threading.Thread:
|
||||
"""Run pub_cam_loop in a new thread."""
|
||||
t = threading.Thread(
|
||||
target=pub_cam_loop, args=(cam_id, request_ize, high_speed, fps_limit)
|
||||
)
|
||||
t.start()
|
||||
return t
|
||||
@@ -0,0 +1,100 @@
|
||||
import threading
|
||||
from typing import Union, Tuple, Any, Callable, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from displayarray.callbacks import global_cv_display_callback
|
||||
from displayarray.uid import uid_for_source
|
||||
from displayarray.frame_publising import subscriber_dictionary
|
||||
from displayarray.frame_publising.frame_publishing import pub_cam_thread
|
||||
from displayarray.subscriber_window import window_commands
|
||||
|
||||
FrameCallable = Callable[[np.ndarray], Optional[np.ndarray]]
|
||||
|
||||
|
||||
class VideoHandlerThread(threading.Thread):
|
||||
"""Thread for publishing frames from a video source."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
video_source: Union[int, str, np.ndarray] = 0,
|
||||
callbacks: Optional[Union[List[FrameCallable], FrameCallable]] = None,
|
||||
request_size: Tuple[int, int] = (99999, 99999),
|
||||
high_speed: bool = True,
|
||||
fps_limit: float = 240,
|
||||
):
|
||||
super(VideoHandlerThread, self).__init__(target=self.loop, args=())
|
||||
self.cam_id = uid_for_source(video_source)
|
||||
self.video_source = video_source
|
||||
if callbacks is None:
|
||||
callbacks = []
|
||||
if callable(callbacks):
|
||||
self.callbacks = [callbacks]
|
||||
else:
|
||||
self.callbacks = callbacks
|
||||
self.request_size = request_size
|
||||
self.high_speed = high_speed
|
||||
self.fps_limit = fps_limit
|
||||
self.exception_raised = None
|
||||
|
||||
def __wait_for_cam_id(self):
|
||||
while str(self.cam_id) not in subscriber_dictionary.CV_CAMS_DICT:
|
||||
continue
|
||||
|
||||
def __apply_callbacks_to_frame(self, frame):
|
||||
if frame is not None:
|
||||
frame_c = None
|
||||
for c in self.callbacks:
|
||||
try:
|
||||
frame_c = c(frame)
|
||||
except TypeError as te:
|
||||
raise TypeError(
|
||||
"Callback functions for cvpubsub need to accept two arguments: array and uid"
|
||||
)
|
||||
except Exception as e:
|
||||
self.exception_raised = e
|
||||
frame = frame_c = self.exception_raised
|
||||
subscriber_dictionary.stop_cam(self.cam_id)
|
||||
window_commands.quit()
|
||||
raise e
|
||||
if frame_c is not None:
|
||||
global_cv_display_callback(frame_c, self.cam_id)
|
||||
else:
|
||||
global_cv_display_callback(frame, self.cam_id)
|
||||
|
||||
def loop(self):
|
||||
"""Continually get frames from the video publisher, run callbacks on them, and listen to commands."""
|
||||
t = pub_cam_thread(
|
||||
self.video_source, self.request_size, self.high_speed, self.fps_limit
|
||||
)
|
||||
self.__wait_for_cam_id()
|
||||
|
||||
sub_cam = subscriber_dictionary.cam_frame_sub(str(self.cam_id))
|
||||
sub_owner = subscriber_dictionary.handler_cmd_sub(str(self.cam_id))
|
||||
msg_owner = sub_owner.return_on_no_data = ""
|
||||
while msg_owner != "quit":
|
||||
frame = sub_cam.get(blocking=True, timeout=1.0) # type: np.ndarray
|
||||
self.__apply_callbacks_to_frame(frame)
|
||||
msg_owner = sub_owner.get()
|
||||
sub_owner.release()
|
||||
sub_cam.release()
|
||||
subscriber_dictionary.stop_cam(self.cam_id)
|
||||
t.join()
|
||||
|
||||
def display(self, callbacks: List[Callable[[np.ndarray], Any]] = None):
|
||||
"""
|
||||
Start default display operation.
|
||||
|
||||
For multiple video sources, please use something outside of this class.
|
||||
|
||||
:param callbacks: List of callbacks to be run on frames before displaying to the screen.
|
||||
"""
|
||||
from displayarray.subscriber_window import SubscriberWindows
|
||||
|
||||
if callbacks is None:
|
||||
callbacks = []
|
||||
self.start()
|
||||
SubscriberWindows(video_sources=[self.cam_id], callbacks=callbacks).loop()
|
||||
self.join()
|
||||
if self.exception_raised is not None:
|
||||
raise self.exception_raised
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,122 @@
|
||||
from displayarray.subscriber_window import window_commands
|
||||
import threading
|
||||
import time
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class MouseEvent(object):
|
||||
"""Holds all the OpenCV mouse event information."""
|
||||
|
||||
def __init__(self, event, x, y, flags, param):
|
||||
self.event = event
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.flags = flags
|
||||
self.param = param
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return "event:{}\nx,y:{},{}\nflags:{}\nparam:{}\n".format(
|
||||
self.event, self.x, self.y, self.flags, self.param
|
||||
)
|
||||
|
||||
|
||||
class _mouse_thread(object): # NOSONAR
|
||||
"""Run a function on mouse information that is received by the window."""
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
self.sub_mouse = window_commands.mouse_pub.make_sub()
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Call the function this was set up with."""
|
||||
self.f(self.sub_mouse, *args, **kwargs)
|
||||
|
||||
|
||||
class _mouse_loop_thread(object): # NOSONAR
|
||||
"""Run a function on mouse information that is received by the window, in the main loop."""
|
||||
|
||||
def __init__(self, f, run_when_no_events=False, fps=60):
|
||||
self.f = f
|
||||
self.sub_mouse = window_commands.mouse_pub.make_sub()
|
||||
self.sub_cmd = window_commands.win_cmd_pub.make_sub()
|
||||
self.sub_cmd.return_on_no_data = ""
|
||||
self.run_when_no_events = run_when_no_events
|
||||
self.fps = fps
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Run the function this was set up with in a loop."""
|
||||
msg_cmd = ""
|
||||
while msg_cmd != "quit":
|
||||
mouse_xyzclick = self.sub_mouse.get(blocking=True) # type: MouseEvent
|
||||
if mouse_xyzclick is not self.sub_mouse.return_on_no_data:
|
||||
self.f(mouse_xyzclick, *args, **kwargs)
|
||||
elif self.run_when_no_events:
|
||||
self.f(None, *args, **kwargs)
|
||||
msg_cmd = self.sub_cmd.get()
|
||||
time.sleep(1.0 / self.fps)
|
||||
window_commands.quit(force_all_read=False)
|
||||
|
||||
|
||||
class mouse_loop(object): # NOSONAR
|
||||
"""Run a function on mouse information that is received by the window, continuously in a new thread."""
|
||||
|
||||
def __init__(self, f, run_when_no_events=False):
|
||||
self.t = threading.Thread(target=_mouse_loop_thread(f, run_when_no_events))
|
||||
self.t.start()
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Return the thread that was started with the function passed in."""
|
||||
return self.t
|
||||
|
||||
|
||||
class _key_thread(object): # NOSONAR
|
||||
"""Run a function on mouse information that is received by the window."""
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
self.sub_key = window_commands.key_pub.make_sub()
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Call the function this was set up with."""
|
||||
self.f(self.sub_key, *args, **kwargs)
|
||||
|
||||
|
||||
class _key_loop_thread(object): # NOSONAR
|
||||
"""Run a function on mouse information that is received by the window, in the main loop."""
|
||||
|
||||
def __init__(self, f, run_when_no_events=False, fps=60):
|
||||
self.f = f
|
||||
self.sub_key = window_commands.key_pub.make_sub()
|
||||
self.sub_cmd = window_commands.win_cmd_pub.make_sub()
|
||||
self.sub_cmd.return_on_no_data = ""
|
||||
self.run_when_no_events = run_when_no_events
|
||||
self.fps = fps
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Run the function this was set up with in a loop."""
|
||||
msg_cmd = ""
|
||||
while msg_cmd != "quit":
|
||||
key_chr = self.sub_key.get() # type: chr
|
||||
if key_chr is not self.sub_key.return_on_no_data:
|
||||
self.f(key_chr, *args, **kwargs)
|
||||
elif self.run_when_no_events:
|
||||
self.f(None, *args, **kwargs)
|
||||
msg_cmd = self.sub_cmd.get()
|
||||
time.sleep(1.0 / self.fps)
|
||||
window_commands.quit(force_all_read=False)
|
||||
|
||||
|
||||
class key_loop(object): # NOSONAR
|
||||
"""Run a function on mouse information that is received by the window, continuously in a new thread."""
|
||||
|
||||
def __init__(self, f: Callable[[str], None], run_when_no_events=False):
|
||||
self.t = threading.Thread(target=_key_loop_thread(f, run_when_no_events))
|
||||
self.t.start()
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Return the thread that was started with the function passed in."""
|
||||
return self.t
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Displays arrays.
|
||||
|
||||
SubscriberWindows displays one array per window, updating it as it's changed.
|
||||
"""
|
||||
|
||||
from .subscriber_windows import SubscriberWindows
|
||||
@@ -0,0 +1,245 @@
|
||||
import warnings
|
||||
from threading import Thread
|
||||
from typing import List, Union, Callable, Any, Dict, Iterable, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from localpubsub import NoData
|
||||
|
||||
from displayarray.callbacks import global_cv_display_callback
|
||||
from displayarray.uid import uid_for_source
|
||||
from displayarray.frame_publising import subscriber_dictionary
|
||||
from displayarray.frame_publising.frame_update_thread import FrameCallable
|
||||
from displayarray.frame_publising.frame_update_thread import VideoHandlerThread
|
||||
from displayarray.input import MouseEvent
|
||||
from displayarray.subscriber_window import window_commands
|
||||
|
||||
|
||||
class SubscriberWindows(object):
|
||||
"""Windows that subscribe to updates to cameras, videos, and arrays."""
|
||||
|
||||
FRAME_DICT: Dict[str, np.ndarray] = {}
|
||||
ESC_KEY_CODES = [27] # ESC key on most keyboards
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
window_names: Iterable[str] = ("displayarray",),
|
||||
video_sources: Iterable[Union[str, int]] = (0,),
|
||||
callbacks: Optional[List[Callable[[np.ndarray], Any]]] = None,
|
||||
):
|
||||
self.source_names: List[Union[str, int]] = []
|
||||
self.close_threads: Optional[List[Thread]] = None
|
||||
self.frames: List[np.ndarray] = []
|
||||
self.input_vid_global_names: List[str] = []
|
||||
self.window_names: List[str] = []
|
||||
self.input_cams: List[str] = []
|
||||
|
||||
if callbacks is None:
|
||||
callbacks = []
|
||||
for name in video_sources:
|
||||
self.add_source(name)
|
||||
self.callbacks = callbacks
|
||||
for name in window_names:
|
||||
self.add_window(name)
|
||||
|
||||
def add_source(self, name):
|
||||
"""Add another source for this class to display."""
|
||||
uid = uid_for_source(name)
|
||||
self.source_names.append(uid)
|
||||
self.input_vid_global_names.append(uid + "frame")
|
||||
self.input_cams.append(name)
|
||||
|
||||
def add_window(self, name):
|
||||
"""Add another window for this class to display sources with. The name will be the title."""
|
||||
self.window_names.append(name)
|
||||
cv2.namedWindow(name + " (press ESC to quit)")
|
||||
cv2.setMouseCallback(name + " (press ESC to quit)", self.handle_mouse)
|
||||
|
||||
def add_callback(self, callback):
|
||||
"""Add a callback for this class to apply to videos."""
|
||||
self.callbacks.append(callback)
|
||||
|
||||
def __stop_all_cams(self):
|
||||
for c in self.source_names:
|
||||
subscriber_dictionary.stop_cam(c)
|
||||
|
||||
def handle_keys(
|
||||
self, key_input # type: int
|
||||
):
|
||||
"""Capture key input for the escape function and passing to key control subscriber threads."""
|
||||
if key_input in self.ESC_KEY_CODES:
|
||||
for name in self.window_names:
|
||||
cv2.destroyWindow(name + " (press ESC to quit)")
|
||||
window_commands.quit()
|
||||
self.__stop_all_cams()
|
||||
return "quit"
|
||||
elif key_input not in [-1, 0]:
|
||||
try:
|
||||
window_commands.key_pub.publish(chr(key_input))
|
||||
except ValueError:
|
||||
warnings.warn(
|
||||
RuntimeWarning(
|
||||
"Unknown key code: [{}]. Please report to cv_pubsubs issue page.".format(
|
||||
key_input
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def handle_mouse(self, event, x, y, flags, param):
|
||||
"""Capture mouse input for mouse control subscriber threads."""
|
||||
mousey = MouseEvent(event, x, y, flags, param)
|
||||
window_commands.mouse_pub.publish(mousey)
|
||||
|
||||
def _display_frames(self, frames, win_num, ids=None):
|
||||
if isinstance(frames, Exception):
|
||||
raise frames
|
||||
for f in range(len(frames)):
|
||||
# detect nested:
|
||||
if (
|
||||
isinstance(frames[f], (list, tuple))
|
||||
or frames[f].dtype.num == 17
|
||||
or len(frames[f].shape) > 3
|
||||
):
|
||||
win_num = self._display_frames(frames[f], win_num, ids)
|
||||
else:
|
||||
cv2.imshow(
|
||||
self.window_names[win_num % len(self.window_names)]
|
||||
+ " (press ESC to quit)",
|
||||
frames[f],
|
||||
)
|
||||
win_num += 1
|
||||
return win_num
|
||||
|
||||
def update_window_frames(self):
|
||||
"""Update the windows with the newest data for all frames."""
|
||||
win_num = 0
|
||||
for i in range(len(self.input_vid_global_names)):
|
||||
if self.input_vid_global_names[i] in self.FRAME_DICT and not isinstance(
|
||||
self.FRAME_DICT[self.input_vid_global_names[i]], NoData
|
||||
):
|
||||
if (
|
||||
len(self.callbacks) > 0
|
||||
and self.callbacks[i % len(self.callbacks)] is not None
|
||||
):
|
||||
self.frames = self.callbacks[i % len(self.callbacks)](
|
||||
self.FRAME_DICT[self.input_vid_global_names[i]]
|
||||
)
|
||||
else:
|
||||
self.frames = self.FRAME_DICT[self.input_vid_global_names[i]]
|
||||
if isinstance(self.frames, np.ndarray) and len(self.frames.shape) <= 3:
|
||||
self.frames = [self.frames]
|
||||
win_num = self._display_frames(self.frames, win_num)
|
||||
|
||||
def update(self, arr=None, id=None):
|
||||
"""Update window frames once. Optionally add a new input and input id."""
|
||||
if arr is not None and id is not None:
|
||||
global_cv_display_callback(arr, id)
|
||||
if id not in self.input_cams:
|
||||
self.add_source(id)
|
||||
self.add_window(id)
|
||||
sub_cmd = window_commands.win_cmd_sub()
|
||||
self.update_window_frames()
|
||||
msg_cmd = sub_cmd.get()
|
||||
key = self.handle_keys(cv2.waitKey(1))
|
||||
return msg_cmd, key
|
||||
|
||||
def wait_for_init(self):
|
||||
"""Update window frames in a loop until they're actually updated. Useful for waiting for cameras to init."""
|
||||
msg_cmd = ""
|
||||
key = ""
|
||||
while msg_cmd != "quit" and key != "quit" and len(self.frames) == 0:
|
||||
msg_cmd, key = self.update()
|
||||
|
||||
def end(self):
|
||||
"""Close all threads. Should be used with non-blocking mode."""
|
||||
window_commands.quit(force_all_read=False)
|
||||
self.__stop_all_cams()
|
||||
if self.close_threads is not None:
|
||||
for t in self.close_threads:
|
||||
t.join()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.end()
|
||||
|
||||
def loop(self):
|
||||
"""Continually update window frame. OpenCV only allows this in the main thread."""
|
||||
sub_cmd = window_commands.win_cmd_sub()
|
||||
msg_cmd = ""
|
||||
key = ""
|
||||
while msg_cmd != "quit" and key != "quit":
|
||||
msg_cmd, key = self.update()
|
||||
sub_cmd.release()
|
||||
window_commands.quit(force_all_read=False)
|
||||
self.__stop_all_cams()
|
||||
|
||||
|
||||
def _get_video_callback_dict_threads(
|
||||
*vids, callbacks: Optional[Dict[Any, FrameCallable]] = None
|
||||
):
|
||||
assert callbacks is not None
|
||||
vid_threads = []
|
||||
for v in vids:
|
||||
v_name = uid_for_source(v)
|
||||
v_callbacks: List[Callable[[np.ndarray], Any]] = []
|
||||
if v_name in callbacks:
|
||||
v_callbacks.append(callbacks[v_name])
|
||||
if v in callbacks:
|
||||
v_callbacks.append(callbacks[v])
|
||||
vid_threads.append(VideoHandlerThread(v, callbacks=v_callbacks))
|
||||
return vid_threads
|
||||
|
||||
|
||||
def _get_video_threads(
|
||||
*vids,
|
||||
callbacks: Optional[
|
||||
Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable]
|
||||
] = None
|
||||
):
|
||||
vid_threads: List[Thread] = []
|
||||
if isinstance(callbacks, Dict):
|
||||
vid_threads = _get_video_callback_dict_threads(*vids, callbacks=callbacks)
|
||||
elif isinstance(callbacks, List):
|
||||
for v in vids:
|
||||
vid_threads.append(VideoHandlerThread(v, callbacks=callbacks))
|
||||
elif callable(callbacks):
|
||||
for v in vids:
|
||||
vid_threads.append(VideoHandlerThread(v, callbacks=[callbacks]))
|
||||
else:
|
||||
for v in vids:
|
||||
if v is not None:
|
||||
vid_threads.append(VideoHandlerThread(v))
|
||||
return vid_threads
|
||||
|
||||
|
||||
def display(
|
||||
*vids,
|
||||
callbacks: Optional[
|
||||
Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable]
|
||||
] = None,
|
||||
window_names=None,
|
||||
blocking=False
|
||||
):
|
||||
"""
|
||||
Display all the arrays, cameras, and videos passed in.
|
||||
|
||||
callbacks can be a dictionary linking functions to videos, or a list of function or functions operating on the video
|
||||
data before displaying.
|
||||
Window names end up becoming the title of the windows
|
||||
"""
|
||||
vid_threads = _get_video_threads(*vids, callbacks=callbacks)
|
||||
for v in vid_threads:
|
||||
v.start()
|
||||
if window_names is None:
|
||||
window_names = ["window {}".format(i) for i in range(len(vids))]
|
||||
if blocking:
|
||||
SubscriberWindows(window_names=window_names, video_sources=vids).loop()
|
||||
for v in vid_threads:
|
||||
v.join()
|
||||
else:
|
||||
s = SubscriberWindows(window_names=window_names, video_sources=vids)
|
||||
s.close_threads = vid_threads
|
||||
v_names = []
|
||||
for v in vids:
|
||||
v_name = uid_for_source(v)
|
||||
v_names.append(v_name)
|
||||
return s, v_names
|
||||
@@ -0,0 +1,15 @@
|
||||
from localpubsub import VariablePub, VariableSub
|
||||
|
||||
key_pub = VariablePub()
|
||||
mouse_pub = VariablePub()
|
||||
win_cmd_pub = VariablePub()
|
||||
|
||||
|
||||
def quit(force_all_read=True):
|
||||
"""Quit the main loop displaying all the windows."""
|
||||
win_cmd_pub.publish("quit", force_all_read=force_all_read)
|
||||
|
||||
|
||||
def win_cmd_sub() -> VariableSub:
|
||||
"""Get a subscriber to the main window loop."""
|
||||
return win_cmd_pub.make_sub()
|
||||
@@ -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
|
||||
@@ -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
|
||||
+3
-3
@@ -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']
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user