From decf7d7849bdbdf64bf7ac87f1bd93c01e426c4d Mon Sep 17 00:00:00 2001 From: simleek Date: Sun, 17 Nov 2019 14:26:43 -0700 Subject: [PATCH] Added beter controls and an example. Improved documentation. --- displayarray/__init__.py | 3 +- displayarray/callbacks.py | 16 +- displayarray/effects/__init__.py | 2 + displayarray/effects/crop.py | 39 +-- displayarray/effects/lens.py | 249 +++++++++------ displayarray/frame/frame_updater.py | 13 +- displayarray/frame/np_to_opencv.py | 11 +- displayarray/input.py | 16 +- displayarray/window/subscriber_windows.py | 1 + docs/.doctrees/crop.doctree | Bin 6412 -> 8628 bytes docs/.doctrees/display.doctree | Bin 0 -> 32070 bytes docs/.doctrees/displayarray.doctree | Bin 13299 -> 32909 bytes docs/.doctrees/displayarray_bash.doctree | Bin 11146 -> 13011 bytes docs/.doctrees/effects.doctree | Bin 0 -> 45932 bytes docs/.doctrees/environment.pickle | Bin 76938 -> 87710 bytes docs/.doctrees/frame.doctree | Bin 29600 -> 33388 bytes docs/.doctrees/index.doctree | Bin 6953 -> 6778 bytes docs/.doctrees/input.doctree | Bin 9128 -> 9298 bytes docs/.doctrees/lens.doctree | Bin 8805 -> 34384 bytes docs/.doctrees/select_channels.doctree | Bin 4169 -> 7370 bytes docs/.doctrees/window.doctree | Bin 23352 -> 24359 bytes .../_modules/displayarray/__main__/index.html | 185 +++++++++++ .../displayarray/effects/crop/index.html | 39 ++- .../displayarray/effects/lens/index.html | 206 ++++++++++--- .../effects/select_channels/index.html | 41 ++- .../frame/frame_publishing/index.html | 20 +- .../frame/frame_updater/index.html | 59 ++-- .../frame/get_frame_ids/index.html | 15 +- .../frame/np_to_opencv/index.html | 27 +- docs/_modules/displayarray/input/index.html | 48 ++- .../window/subscriber_windows/index.html | 79 ++--- docs/_modules/index.html | 14 +- .../display.rst.txt} | 11 +- docs/_sources/displayarray.rst.txt | 11 +- docs/_sources/effects.rst.txt | 30 ++ docs/_sources/frame.rst.txt | 8 + docs/_sources/index.rst.txt | 11 +- docs/crop/index.html | 14 +- docs/display/index.html | 212 +++++++++++++ docs/displayarray/index.html | 90 +++++- docs/displayarray_bash/index.html | 10 +- docs/docsrc/conf.py | 22 +- docs/docsrc/crop.rst | 7 - docs/docsrc/display.rst | 11 + docs/docsrc/effects.rst | 30 ++ docs/docsrc/frame.rst | 8 + docs/docsrc/index.rst | 11 +- docs/docsrc/lens.rst | 13 - docs/docsrc/select_channels.rst | 7 - docs/docsrc/window.rst | 7 - docs/effects/index.html | 288 ++++++++++++++++++ docs/frame/index.html | 62 ++-- docs/genindex/index.html | 97 ++++-- docs/index.html | 38 +-- docs/input/index.html | 29 +- docs/lens/index.html | 107 ++++++- docs/objects.inv | Bin 719 -> 774 bytes docs/py-modindex/index.html | 15 +- docs/search/index.html | 11 +- docs/searchindex.js | 2 +- docs/select_channels/index.html | 19 +- docs/window/index.html | 8 +- examples/documentation/fractal.py | 24 +- examples/effects/lens_crop.py | 7 +- examples/effects/manual_control.py | 36 +++ pyproject.toml | 2 +- tests/effects/test_crop.py | 4 +- 67 files changed, 1829 insertions(+), 516 deletions(-) create mode 100644 docs/.doctrees/display.doctree create mode 100644 docs/.doctrees/effects.doctree create mode 100644 docs/_modules/displayarray/__main__/index.html rename docs/{docsrc/displayarray.rst => _sources/display.rst.txt} (59%) create mode 100644 docs/_sources/effects.rst.txt create mode 100644 docs/display/index.html delete mode 100644 docs/docsrc/crop.rst create mode 100644 docs/docsrc/display.rst create mode 100644 docs/docsrc/effects.rst delete mode 100644 docs/docsrc/lens.rst delete mode 100644 docs/docsrc/select_channels.rst delete mode 100644 docs/docsrc/window.rst create mode 100644 docs/effects/index.html create mode 100644 examples/effects/manual_control.py diff --git a/displayarray/__init__.py b/displayarray/__init__.py index 0d09492..90a7bca 100644 --- a/displayarray/__init__.py +++ b/displayarray/__init__.py @@ -4,8 +4,9 @@ Display any array, webcam, or video file. display is a function that displays these in their own windows. """ -__version__ = "0.7.1" +__version__ = "0.7.2" from .window.subscriber_windows import display, breakpoint_display from .frame.frame_updater import read_updates from .frame.frame_publishing import publish_updates_zero_mq, publish_updates_ros +from . import effects diff --git a/displayarray/callbacks.py b/displayarray/callbacks.py index 0e2586c..e276829 100644 --- a/displayarray/callbacks.py +++ b/displayarray/callbacks.py @@ -24,13 +24,15 @@ class function_display_callback(object): # NOSONAR """ Used for running arbitrary functions on pixels. - >>> import random - >>> from displayarray.frame import FrameUpdater - >>> 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 - >>> FrameUpdater(video_source=img, callbacks=function_display_callback(fun)).display() + .. code-block:: python + + >>> import random + >>> from displayarray.frame import FrameUpdater + >>> 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 + >>> FrameUpdater(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. diff --git a/displayarray/effects/__init__.py b/displayarray/effects/__init__.py index 2885141..b066ad7 100644 --- a/displayarray/effects/__init__.py +++ b/displayarray/effects/__init__.py @@ -1 +1,3 @@ """Effects to run on numpy arrays to make data clearer.""" + +from . import crop, lens, select_channels diff --git a/displayarray/effects/crop.py b/displayarray/effects/crop.py index ae18e32..347a731 100644 --- a/displayarray/effects/crop.py +++ b/displayarray/effects/crop.py @@ -1,28 +1,21 @@ """Crop any n-dimensional array.""" import numpy as np -from displayarray.input import mouse_loop +from ..input import mouse_loop class Crop(object): - """ - A callback class that will return the input array cropped to the output size. N-dimensional. - - >>> crop_it = Crop((2,2,2)) - >>> arr = np.ones((4,4,4)) - >>> crop_it(arr) - array([[[1., 1.], - [1., 1.]], - - [[1., 1.], - [1., 1.]]]) - - """ + """A callback class that will return the input array cropped to the output size. N-dimensional.""" def __init__(self, output_size=(64, 64, 3), center=None): - """Create the cropper.""" + """ + Create the cropping callback class. + + :param output_size: Specified the size the input should be cropped to. Can be redefined later. + :param center: Specifies the center on the input array to take the crop out of. + """ self._output_size = None - self._center = None + self._center = np.asarray([o // 2 for o in output_size]) self.odd_center = None self.mouse_control = None self.input_size = None @@ -32,27 +25,27 @@ class Crop(object): @property def output_size(self): - """Get the output size after cropping.""" + """Get the output size.""" return self._output_size @output_size.setter def output_size(self, set): - """Set what the output size will be after cropping.""" + """Set the output size.""" self._output_size = set if self._output_size is not None: self._output_size = np.asarray(set) @property def center(self): - """Get center crop position on the input.""" + """Get the center.""" return self._center @center.setter def center(self, set): - """Set center crop position on the input.""" - self._center = set - if self._center is not None: - self._center = np.asarray(set) + """Set the center. Guarded so that colors need not be set.""" + if set is not None: + for x in range(len(set)): + self._center[x] = set[x] def __call__(self, arr): """Crop the input array to the specified output size. output is centered on self.center point on input.""" diff --git a/displayarray/effects/lens.py b/displayarray/effects/lens.py index 5dc8278..754522b 100644 --- a/displayarray/effects/lens.py +++ b/displayarray/effects/lens.py @@ -1,145 +1,156 @@ """Create lens effects. Currently only 2D+color arrays are supported.""" import numpy as np -from displayarray.input import mouse_loop +from ..input import mouse_loop import cv2 +try: + import torch +except ImportError: + torch = None # type: ignore + + +class ControllableLens(object): + """A lens callback that can be controlled by the program or the user.""" -class _ControllableLens(object): def __init__(self, use_bleed=False, zoom=1, center=None): - self.center = center - self.zoom = zoom + """Create the lens callback.""" + self._center = center + self._zoom = zoom self.use_bleed = use_bleed self.bleed = None self.mouse_control = None - def check_setup_bleed(self, arr): + def _check_setup_bleed(self, arr): if not isinstance(self.bleed, np.ndarray) and self.use_bleed: self.bleed = np.zeros_like(arr) def run_bleed(self, arr, x, y): + """Spread color outwards, like food coloring in water.""" arr[y, ...] = (arr[(y + 1) % len(y), ...] + arr[(y - 1) % len(y), ...]) / 2 arr[:, x, ...] = ( arr[:, (x + 1) % len(x), ...] + arr[:, (x - 1) % len(x), ...] ) / 2 -class Barrel(_ControllableLens): - """ - Create a barrel distortion. +class Barrel(ControllableLens): + """A barrel lens distortion callback.""" - >>> distort_it = Barrel(zoom=1, barrel_power=1.5) - >>> x = np.linspace(0, 1, 4) - >>> y = np.linspace(0, 1, 4) - >>> c = np.linspace(0, 1, 2) - >>> arrx, arry, arrc = np.meshgrid(x,y,c) - >>> arrx - array([[[0. , 0. ], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [1. , 1. ]], - - [[0. , 0. ], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [1. , 1. ]], - - [[0. , 0. ], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [1. , 1. ]], - - [[0. , 0. ], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [1. , 1. ]]]) - - >>> distort_it(arrx) - array([[[0.33333333, 0.33333333], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [0.66666667, 0.66666667]], - - [[0.33333333, 0.33333333], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [0.66666667, 0.66666667]], - - [[0.33333333, 0.33333333], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [0.66666667, 0.66666667]], - - [[0.33333333, 0.33333333], - [0.33333333, 0.33333333], - [0.66666667, 0.66666667], - [0.66666667, 0.66666667]]]) - - :param zoom: How far to zoom into the array - :param barrel_power: How much to distort. - 1 = no distortion. >1 increases size of center. 0 0: - self.zoom *= 1.1 - else: - self.zoom /= 1.1 + if self.input_size is not None: + if crop_size is not None: + self.center = [ + me.y * self.input_size[0] / crop_size[0], + me.x * self.input_size[1] / crop_size[1], + ] else: - if me.flags > 0: - self.barrel_power *= 1.1 + self.center = [me.y, me.x] + if me.event == cv2.EVENT_MOUSEWHEEL: + if me.flags & cv2.EVENT_FLAG_CTRLKEY: + if me.flags > 0: + self.zoom *= 1.1 + else: + self.zoom /= 1.1 else: - self.barrel_power /= 1.1 + if me.flags > 0: + self.barrel_power *= 1.1 + else: + self.barrel_power /= 1.1 + print(self.barrel_power) self.mouse_control = m_loop return self def __call__(self, arr): - """Run the distortion on an array.""" + """Run the lens distortion algorithm on the input.""" zoom_out = 1.0 / self.zoom - self.check_setup_bleed(arr) + self._check_setup_bleed(arr) + + self.input_size = arr.shape y = np.arange(arr.shape[0]) x = np.arange(arr.shape[1]) - if self.center is None: - self.center = [len(y) / 2.0, len(x) / 2.0] + if self._center is None: + self._center = [len(y) / 2.0, len(x) / 2.0] y2_ = (y - (len(y) / 2.0)) * zoom_out / arr.shape[0] x2_ = (x - (len(x) / 2.0)) * zoom_out / arr.shape[1] p2 = np.array(np.meshgrid(x2_, y2_)) - cy = self.center[0] / arr.shape[0] - cx = self.center[1] / arr.shape[1] - - barrel_power = self.barrel_power + cy = self._center[0] / arr.shape[0] + cx = self._center[1] / arr.shape[1] theta = np.arctan2(p2[1], p2[0]) radius = np.linalg.norm(p2, axis=0, ord=2) - radius = pow(radius, barrel_power) + radius = pow(radius, self.barrel_power) x_new = 0.5 * (radius * np.cos(theta) + cx * 2) x_new = np.clip(x_new * len(x), 0, len(x) - 1) @@ -163,13 +174,54 @@ class Barrel(_ControllableLens): return arr -class Mustache(_ControllableLens): - """Create a mustache distortion.""" +class BarrelPyTorch(Barrel): + """A barrel distortion callback class accelerated by PyTorch.""" + + def __call__(self, arr): + """Run a pytorch accelerated lens distortion algorithm on the input.""" + zoom_out = 1.0 / self.zoom + self.input_size = arr.shape + y = torch.arange(arr.shape[1]).type(torch.FloatTensor).cuda() + x = torch.arange(arr.shape[0]).type(torch.FloatTensor).cuda() + if self._center is None: + self._center = [y.shape[0] / 2.0, x.shape[0] / 2.0] + + y2_ = (y - (y.shape[0] / 2.0)) * zoom_out / arr.shape[1] + x2_ = (x - (x.shape[0] / 2.0)) * zoom_out / arr.shape[0] + p2 = torch.stack(torch.meshgrid(x2_, y2_)) + + cy = self._center[1] / arr.shape[1] + cx = self._center[0] / arr.shape[0] + + theta = torch.atan2(p2[1], p2[0]) + + radius = torch.norm(p2, dim=0) + + radius = torch.pow(radius, self.barrel_power) + + x_new = 0.5 * (radius * torch.cos(theta) + cx * 2) + x_new = torch.clamp(x_new * x.shape[0], 0, x.shape[0] - 1) + + y_new = 0.5 * (radius * torch.sin(theta) + cy * 2) + y_new = torch.clamp(y_new * y.shape[0], 0, y.shape[0] - 1) + + p = torch.stack(torch.meshgrid([x, y])).type(torch.IntTensor) + + p_new = torch.stack((x_new, y_new)) + p_new = p_new.type(torch.IntTensor) + + arr[p[0], p[1], :] = arr[p_new[0], p_new[1], :] + + return arr + + +class Mustache(ControllableLens): + """A mustache distortion callback.""" def __init__( self, use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None ): - """Create the distorter.""" + """Create the mustache distortion callback.""" super().__init__(use_bleed, zoom, center) self.center = center self.zoom = zoom @@ -181,12 +233,11 @@ class Mustache(_ControllableLens): def enable_mouse_control(self): """ - Enable mouse control. + Enable the default mouse loop to control the mustache distortion. - Move the mouse to center the image. - Scroll to increase/decrease barrel. - Ctrl+scroll to increase/decrease zoom. - Shift+Scroll to increase/decrease pincushion. + ctrl+scroll wheel: zoom in and out + shift+scroll wheel: adjust pincushion power + scroll wheel: adjust barrel power """ @mouse_loop @@ -212,9 +263,9 @@ class Mustache(_ControllableLens): self.mouse_control = m_loop def __call__(self, arr): - """Run the distortion on an array.""" + """Run the mustache distortion algorithm on the input numpy array.""" zoom_out = 1.0 / self.zoom - self.check_setup_bleed(arr) + self._check_setup_bleed(arr) y = np.arange(arr.shape[0]) x = np.arange(arr.shape[1]) diff --git a/displayarray/frame/frame_updater.py b/displayarray/frame/frame_updater.py index b149ac8..de14e01 100644 --- a/displayarray/frame/frame_updater.py +++ b/displayarray/frame/frame_updater.py @@ -48,7 +48,7 @@ class FrameUpdater(threading.Thread): continue def __apply_callbacks_to_frame(self, frame): - if frame is not None: + if frame is not None and not isinstance(frame, NoData): try: for c in self.callbacks: frame_c = c(frame) @@ -133,10 +133,13 @@ async def read_updates( Read back all updates from the requested videos. Example usage: - >>> from examples.videos import test_video - >>> f = 0 - >>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)): - ... print(f"Frame:{f}. Array:{r}") + + .. code-block:: python + + >>> from examples.videos import test_video + >>> f = 0 + >>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)): + ... print(f"Frame:{f}. Array:{r}") """ from displayarray.window import SubscriberWindows diff --git a/displayarray/frame/np_to_opencv.py b/displayarray/frame/np_to_opencv.py index a9a482c..a4912a3 100644 --- a/displayarray/frame/np_to_opencv.py +++ b/displayarray/frame/np_to_opencv.py @@ -12,9 +12,14 @@ class NpCam(object): assert isinstance(img, np.ndarray) self.__img = img self.__is_opened = True - - self.__width = self.__img.shape[1] - self.__height = self.__img.shape[0] + if len(img.shape) > 0: + self.__height = self.__img.shape[0] + if len(self.__img.shape) > 1: + self.__width = self.__img.shape[1] + else: + self.__width = self.__height + else: + self.__width = self.__height = 1 self.__ratio = float(self.__width) / self.__height self.__wait_for_ratio = False diff --git a/displayarray/input.py b/displayarray/input.py index ea814cb..3695c0a 100644 --- a/displayarray/input.py +++ b/displayarray/input.py @@ -68,9 +68,11 @@ class mouse_loop(object): # NOSONAR """ Run a function on mouse information that is received by the window, continuously in a new thread. - >>> @mouse_loop - ... def fun(mouse_event): - ... print("x:{}, y:{}".format(mouse_event.x, mouse_event.y)) + .. code-block:: python + + >>> @mouse_loop + ... def fun(mouse_event): + ... print("x:{}, y:{}".format(mouse_event.x, mouse_event.y)) """ def __init__(self, f): @@ -126,9 +128,11 @@ class key_loop(object): # NOSONAR """ Run a function on mouse information that is received by the window, continuously in a new thread. - >>> @key_loop - ... def fun(key): - ... print("key pressed:{}".format(key)) + .. code-block:: python + + >>> @key_loop + ... def fun(key): + ... print("key pressed:{}".format(key)) """ def __init__(self, f: Callable[[str], None]): diff --git a/displayarray/window/subscriber_windows.py b/displayarray/window/subscriber_windows.py index b294803..d054b2d 100644 --- a/displayarray/window/subscriber_windows.py +++ b/displayarray/window/subscriber_windows.py @@ -173,6 +173,7 @@ class SubscriberWindows(object): if frame is not None: self.frames[-1] = frame self.__check_too_many_channels() + self.FRAME_DICT[self.input_vid_global_names[i]] = NoData() self._display_frames(self.frames) def update(self, arr: np.ndarray = None, id: str = None): diff --git a/docs/.doctrees/crop.doctree b/docs/.doctrees/crop.doctree index 5201c06be570b775db901807f9d760942279c278..08fcb9029f20b95e474113ed8657c60c81f581f2 100644 GIT binary patch delta 1451 zcmb7E&rcIU6lS-yKz}Z25iAAT60t>Xf?^1%SfwB+Nu%Ln3>7PGGEB4-x{VfN#DiLr zJ(#@Ji;3ZCI1sHD55~lJ(f`4di3fs&L{FZ4Guz$%fS7oh-Ffrg_rCY$d!wFwm$a@m zKYty1W4&S#6qprGls7DXoBj(MmP?SbPX@XckO60GA8VDA#D<7bIO9yfUC9UE-AT7vM-h&B zM&h0+SqUmc4(ezz18$$)#HACArJs%?bQEHew>43r4fECj$kKu^4_~CCaLv9ctQ(yI zmn#5o9qYn93_I;qs+HEdT&_8wPhinCE4+XYuEAzNf9Pu&fOhxO=4bk*2j06SyvDC? zzbA{P3<;$*(x2977Jj*hySC6)AW<@oWS8+@?<~S5JpIB1Ydi4jU`9aW#|j!N*vC@rHl`>_;RKySH&x6PHF}->+3v^+S-Pds8@Qw2*pk6z zodJw-a(=Q$x~ZNh1y}9yP|s39$rY6O{Nk#7kTwUIHl48=FD}qEqwV!asM3_gRaz79 zUsA0$h|B9Yc4vr7cMmJ6NY_lBs^z7T2K_o3Ip9px+047(sVpLlIfrA_JX93fh2T)g zq=yYzAKv^n(x~cS8+C9hqV6x*ki}1vHKM^ePxyk*m^$-##vPuj~E`DuVr zjo@gsdUsd?T{MT;Ar3baO@|?xdIGCbX!PHz89|VPRkH)AIk2}6g&SmNUAUde=PRn4 zk7nWB7QUFEGUsBPH{%D9-5^Xy@xEH*{r oB_2`Y-X-o?Dv&eaY;&K28Y2L1 CuGVt^ diff --git a/docs/.doctrees/display.doctree b/docs/.doctrees/display.doctree new file mode 100644 index 0000000000000000000000000000000000000000..023d9ae0f86326e1aefa8a06519081904f58108b GIT binary patch literal 32070 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 F^Z#3PR$>4E literal 0 HcmV?d00001 diff --git a/docs/.doctrees/displayarray.doctree b/docs/.doctrees/displayarray.doctree index 75b407da32ec6454f0d73d2f5c10bed1e547ad77..bef47b0dd99d05f8c14c2b22a023213da6c59f63 100644 GIT binary patch literal 32909 zcmeHQdyE}ddG})<-nG3>oS285G=oXVU3+~Uk`Ri)4t8upEo%R$R5eOf2tf!Wqy|b`sj5Kz zedjqdJNLDB<8>ul^4*y;=kcBIeCK}cdm3wY$~#s;;QCxtras zY(fGw&&C3;>2&7X3pSg!lO*yM+6g*BfN%9`F@R)~)xeHpB;t9zX*a~vwpzPoUKZ@? zg~+K>o;I6I>_z(2acA7U?sjU?-N)Yg(2;ZRjGZVx=ZCS|IQQss)wQF>qfzJs1@XCb z&&~mjNWo83&3q_FF*DX;|)}2dr z_7{ihY-&u;RVrT7ODdIeYxxKv0Pr;zt*~xoTfOb{=Jo={ehLF?FGhCDO?8{io)lGD zc5~U{v@&NcISW<0F=vI5bw6hY|P84cmyg9LBc};roA}d^KT1%j*a4FV^ z8o3^WzMli2ZyO4IF3@~>YpQ?Ks(*@U>{1DAQqq7LVf}6ZSU^qh8XN$c&F4jS_W%Hy z0D=#x_J)~i?i<*CIUJ=~s|0+DnWYATQ!FOjtuhmTFUyzQjFSMdr8UnuF~aBK_Cj0* z|8t^>8uMOL(11gP?;d1h5CGNGBvd1E5K;nPDzOKqn6#rbKRs zAo^b!Y>u$0OO4=2+_Fj3Z)-*1CDBEvL#%HFwR}+2vhkF>v?%zn;szS~H!xqsuU-Po@3}$PLQbqZVP~1;r+cY^?ytMQLHzJ{!4E$< zNXFkXjMM#1u49s@4V%GoYT`)U;~?A7a+)>BhRP)vBdarPViR7I8$3n^$g+FbwzI-0 zI^W6m=WBT|+0Kievs8t_cL;S|=-6qKf;#tCfcY!=Qt3==65u7vEfN!|t_}=Er(O#i zw%6>it#MN8uLs14JVRnfi%!yEhbuy5iVvrCq{Bj99$u}>z;r`pUajLY zHo+UJ=IuoS{YwD-i#9*umvlHEbqn!`m3r#t3-eq_T^GfOLFe z8*!zp;Y2nb>}E}vN3qo+tG5PgKzRE834 zqfjD-n~*kg#0lA@DhC;s>N&Q;7Vw#H)0JCOqU?0dPHePQ4j2NH^9ycVDIh^tS0L*Kze8Y{QiDXwdhoez2w|EgohoJ)!<=jfZ8W{?P7k3qGZq+ z+`R5_ZiHCe2(chwtab}DTn!sU-o!Y`2-OHzNM_OK%R~PQ$UiiSZb}tk`#yt)1%X(Z&?vpBk zy?S+_(g7$8kZT7HTv(zfWf5Q1g?6tlq-eM3lO{~?kiGtufvHkyez1i7WnGG0KY0Cw z#>lQ5&Nw(c{~~G-vLZEn|CJjaBj6U-rWIR{C1ESJ$PlueOAalBg-xs7#Cljt z5R$*W8idfKwHkfR+S8N8{|;efum5e0m;r^T-=-2=0r#Jmg?mkF8UcvX!V%~`^?Vyu zGZLnLm7ZYrh~%=`57haiB1k%N_ zDnJT4c5%&GK%H=Rj{i#NqMj>!$NYfjH77N;e$1s~xyzrNC>}b1MaNq}W%K zv3Vr5bOCqSLKp_LCh;aUFReEPc4MJtA3K&>k9bGa#wxK`yEp39j`Xmoxsy%QGl;8H zWjz`C@hBy+Nu@fVOKmWbYBlQ25@_-U8Eyy}3OdsY>y4r4;`zP?96?U`8$+;xLy%># z$H6Ll>2Qjzl4*8yz`SHM85ZoniMa7P8%<}6Kq)qw&J<-G*=U+AUUJ|%&Ebq@qZb?e zdjLs+x%AF{0OrjlgWFZ+(w(SFYr4L8>u)X*V9H#&8_&{QG6R=0mr5yz;5~1Lj%m>z z4GUW_;0@%;rd5x^1~+^n=iO~5POx^sH(Hs!L&F!;ld@q%53qZ$5MClHF(pj@6FsMd!%$v17+XR~(TZmqjluuh9yl#DZR(RCtoe3ccta z@k9#%b5=zAryOiep#=b^lvaOArfgQq!0R(R*iSk4%@#Wy`W!*!z~0>ige9 z%JY%;)UTASIP}+ewwCt>4W|`m(_DUkrt3GE<+?CAgWoYXmcO_Ms?jStY*CN%JLRi#nnU zaIao9>bZUMyvAZb4kiV${|zx<&*_3JS+gB3 zaeIz%+uHz7kAg%rm*~c{8x!akTgSvpwyQ5{hzIN{?Gy7&KmC8F3V5s9tYUW#GG2TL z(k|;=xl(G3R;6cquzBm?y3&sUET)9EI@p#6?pMvwW47j9Bh`GL=Kfpon63HtQEJ|v zB63}CFl}7n5c$ONanZi9Z%5%r{ zwqH2QvF`+WB=)5*c~CDS2b+e)9ty7sMlJTNScIY$R7`BkpjBrvz4`9fvNew*3OIxk z^Uc88?f%j>qUVJlVO&7ME&4OC8<@@pY!p|g?KOTwf;)mxe}&*3}C!x@XbuYEy{M=6$|^ZfK5w5*P-lB(tYw$6`2dU-5 z`c$T;tN%vy3tf5x{TW6x^}owWn(K)V$ijmNl_K1&Rj9lJ?RFI^0=`j%%IzutfR-gn zZG$v9&_|l2igyzX-81R>=f}DD5Bh9cn@isK8W2?toF}|7x+BDxo$GuhQxB@l8 z64!b};YW=m|7mJ#um5q~WIsG9ZMRA#gtq%pS$Gh}RMfY%GNwO9yImQRfG=a0mKNxaDO zrhNZ1St?Xb=99B6ZXO?@#oepvT(w&CDcka{QCgl5vq`n4Ub8LSK1vIj6$m*OGilkj z4i4Q`cSfs5nT)(b3SK`W?^F@k#YWzVqO32qkw<*PtV(NLEdwegqtWoLcl}7&~kOrG!``kN}UcMj$aNBrzFULcpZg3NrUgl?#McIn7&h z8bm|mls!4JwZ#SHup%`FT|a~C7J-0VsgZ21r!@`+6whjh!N+vL>j`;AIoXjNc%hK?6{q@$hTA~U!}fap z!)-gNVKG$T$Kq-(_KIB$aibFW7VTEE7M88cP88z260V3KE@t-St+~9$w*OUv80!@p z@O}ta7Tkxagj}zXg?lMo%33PIkdYjD8>`QH1==mJ;0PM5S8(`7aV_l?Mn*BSySvb@ z#2Jrd3~K!>eb1hsVslqEh|MXu`Z2hjxqghq+Fq0-GPJ4neO-`U7YH*c z%kAm;i3Y#lyi)oqUu4mx{2$X}j*)!_X@-%Bx8WICZhRFB`(8oYJy_U5#lmpAi&`ed zt!=p0jNiPmXj3mRlS`445NMd-{m;un$WGX@F1+J2oq~% zRrjLZuB=MHHwvqIP0?atigD@1)z|f9TSYZ>*VUfdASPA}Wj~fConYmn(xocacf@8= zy2R@xSa5x0V8TuFf>XK_yAF|U+(0u~{0XOm*;85gc@N7c>)FoLz;oz(ubHawM4p^8 z1Lh@vfWfK%IH8x*&HDKRE*61O><@^GvR+ev0NW?!e9@#RTJ~-A13B*~FxVC{(C|?I zLDXd=nRpvolKH<%5XoiV6L^-s0yDI|daLDJ+EKt>Pq0 z>>RE+;z8OP)72yj4))4N&OzlGyryj$9DZKl@N;X$A)PESI5c#ZH#3xRQbau6OZ7HR zz7C+fN_;WKqe%Q$(D0-rd}_(Nc+Y0qG!aAF%`h2#&*s|vF9NE5GJLyaSDHN6zGw4= zf$2&L8s%^KhAy}cdp1W#mN7}jG*YmxEnf6w;sW)D8t{JHNQqW5M@)D9A5aB3bL3g1 z873~?hGya{Gp>r8|Eq@AwA{lOIjoEk!ov`Ll%dOMtol19Ih&uRKSLQM&v8t0qhwMR z?xpG|7FC3iwX&$Y&~8^2CEy!q1-!^VKR8U874Z8CL0Xp;@MDiT0Z!OG;kcd)JMrxqRhpD>=()n04~kXKN~nh8$e>AwfD~J%VFy{6=*gH&u6!OT%Ri z%i)gLcuE&!ossRqJU$0<f(p3%J=hIbm= z_>WTs91>$S-7O&RJF0neYPfQH*ueR-ef9xzFRrAd1Q}rU0rPSLOye?zmGv{gK3D`w zu>tn}qO2nuV264i6U&jSS)2U@KvS?b`@Viat}?k$AT5pzH}>C*Pexjcw~VyDv(QD9 z2U#xD62MAWpU{n_2DOLjB^k1%_!*fivTd=+7T5b?8P{IMd9uXCS8U0pxG>c)2qR;b zjarSSc>hnR1AG0?>VEW_=+tvM0Cv4{J+58+q3jMgAg?OdtE-<2C{8%1DdNUVfGejXJE$OpQP^BeA%oq#s%B*%7ktTf{o8%I&ToB; zTTa!hd)3YgVd7f?6aU6bLZKhGUU=f;R&Ieof9>Bv4G`;}7t?@hF6JRc=JPR>c&<+$ zsZ-C2PSNnD4}BF>>3>aR{mTJr{ffw{NL{Tmt$DX(v4#6H}G}li%dl+@Dk^;rr*~y};8>OAGR}Vn7MIQ6x{d$pC-^>6A4J4($-GRD{ z`X=7iq`vv2y5auacM)*XdA=Xd(s^zMzJ~_4M|#A?ktyjg*UP+JzG^Y#x2d_keym#^ zP~Y-jMJ0rbyd?`~T;xw450ZnA_3(j91;DzuJpGvK)d$P`+Vb>6{ha2i;(Ig}t_xTU z&Yu)GfB#x>PA+wWb3>Q~0-T>uE+VP!rFy&6KLMau`>VK!+$U*bAb(Ynp)xp38GqFu z6oQoYSDA_twNu{lq$hAXNzP)Li}|~waAT9Hrc@lyGk3eTqXs0GuA|wgcD9NM&u@l8 zbkgn;Q3qdj4^Y|7Si5awlP$s*=%B1SREh}B%C^N$La|h^`}k%GFv}y(;`}59yg`_t ziJ!LvTsUVm?3sBswdgeI0=6*f;DujMm*AQ=ylfZEz;;3GqIotgU)xcDx1C8?t{LGooIV z2UwP+Z_9qj6=pY4A7UWN1&RV5WpHO;Hy7JDGYgE@5Kd|f;tI1{A_OOiYL%Kp;jqvQ zMYx^E;hnHqM>l~HuU0{L!n$_}YBHO#+euh4No)(kM6sPU1edI6!UJM?0z9Nngp;~d z;Vzy6>^5T>ev{OSopvp(AQ~ca-GqQqAl zupQ`3j42?l90S$Ry$9$_g@V}f$iy=(r&U3paLl+_<^4r4uolP_O%rW1%T1D2eB|)q zrKKg@{Du1(n&mKB#Dea{!^jyQ?yx;nf@-7^6$DHImJ=S!(Pz@08mYJwt%{40lc%R&=ViM@a+P)pp->22(G5LBb0=avS)B7#U>WiR8G47LeV|cEk-E^Xc7fOy3wKZ{hFa zwSr)pw#4A=5W1Kt7+*21Fk-}iDky!S-9or7igwLI-N@;F6+{wNahEoRTFw96#1RmF zESv8pr?CLvY@95kE!RN+v#V?mq5{|#Dh>jk1r@|_Mz~B0GQGmKI?am|k(zB2oB&e~ zRD|d;9_Tu$hzMwW%x|VRXK0B>9sq?P{`3{_s!S@6+C#~xt*&1v1R906M*1WriszF5 zMUq2*N0x*cc5H$FY_o2;^zyL{(1VLt477o=cvFR(GT4tC4T6>6`%jmDSV&* z&>>Zy|Ik?ue!Rn{{nO%yHX!=^hcm@N)s|P-Kx-f%2eVRwa8XisjzV`Mi@BZs(IBEX|g&f!BJ99+Y4eC z&SM}xD8_)|$A~eY$N^#u=!RG^2BbBJF`x^-#2C<3Ibsax@&qvkbiP=O0Uf~+V?bNq z#Td|rSuqB*??#LPE#QhVpydcL5M*Pt+0OJF=47=KG5a{TB5om4h|j8gf_~E}A|Ha1 zV$;ZaEhB79?Urj!K{O$9P%s)(= z7rpA79yhw)dr_@_LXbC)HK9!nQlrRcSj^R>;8oz(CC6^Ma2uB8c?34`A z2)MG%!90sOJ?2r*m#&ldi%U*!5IaXzLpSdiw`KE~L@j5YRS23~B`3}3GI^Su5j3>w zC;O`=PnK0^p{6x4jAlp?0zSCIFm1N4n8(b%kBNaHYd6#6C)Ikwr9GT^iMgpIl?AEAQ);JZ zWbI>`Jhw(px->%`#M=#2n!%m53q&#Wu$3ehrKe7wC?Gspj+KKMC_LGwR)I;4d2(E> l?PlS+wTw(Y%##&`RhXr+JSTrJP?*fwV8rCkJlVD(007QFYg_;T diff --git a/docs/.doctrees/displayarray_bash.doctree b/docs/.doctrees/displayarray_bash.doctree index 90826f14fb38a49ec1f0db78d387fa53bbe715b4..85c97d7e9f4f74c15d05a9209fecec10e257cac6 100644 GIT binary patch delta 3462 zcmb7HeQZ-z6z9G6b?w%DbX&Kvj`fY|XgfO)6#}>{Cvq=uaEY%^tE%9 z%;8re=KdkM4@rm`mBeTy#%)ARzz;Ac`Ui;#i5iTFnt;SVjK;|n6ZM>X``T^>1AnaV zynD~b@BGer_ntks>zpDTupj)ERCR=q>tD33olWB;0BKs#S3nZfKieDP&@1U zsFbf5QL6xB&`(&<(;$6-QuriD?}ObbD;RM^BC3+ga>~wF%j(y(+r2_KUpD`eNz5G4 zOYefCIr^6S4q<`*>|P};!26$&YoDn8&QyP63ZObX1Jc_NbGGceU9}v3e^UVPX($YGZ1h_OWN^ZcVH4Dp@LA-f7)zB}jr^g$@&h@Ya zI~zb}wQw7=0f1UF{lydLyHK=_VT3L;?iNz;?x)8+4fH^h z!>RFrBN)Jq@E+ixPSFpVLg3-YrnQuKWco{;-Hkq;WZT+adbj6J!7RCIwcI?3ZY1W0 zIm9e?aEJ5M-@Hzkr8}F~x@LLM69V1b)-SwMPIJd~r@5}>1^~IcrI%iAsen9Zft+dC zDqJXmbbGY@xUPn@Qqt!zTpObOf@7?7TL&F>b~sCoeTI_OHS}h0CBT)c0HRx@hnl;A zQHXGCafB;nKE5dPahAuLr=NN^2_Y)9t#^e)R`ot~6m~QMpj9ZYl(6<^ubY&MZ8DHj z)80#uwpTC@L6E{2RBp6=Kzz82xp^roLn8gvvy#!WjhR&SdnAvoM@nuGe14-G! z%&zN;IMLYytbEzoOZWOJkWX8XZ}JU_=c*tt>6OzQCqn*^<2*&b@~r`n&aN%=WLL%G zrD~5?uI}+tg@g|)B%I{V=jZNOv0QNZY%KrfHr5y(2<}C3CF9^Evl_bAQ=zJiQY4bg z7)y_=P4qUuUmV~8@LS)&Ma+2}6I=0nz6CJhsbw9Cv|A2QNp2KIs8_z-HNqi3ga%

$o-*QJ?`LNga)1ID{;uMF42#Z`qgcD3`Lxjc>grtw2?FrM% zJ(I2%Il7~$9CoyV@(aw4p)(Mj#L(g0QGdL2^SR+-c#~Zgcq<;kJ&$!O;sqB~1r@wB z$*rUCR)=rgau|4u&>7*@Hgy9DrejGx7?OiYT~22bg_Ht4BY5p0lXRjW^`iofi0yDh z^O<4eG+Q&(F7(t#>#p9CJW5;hWrmn!r&<`_Y&!|c2Cz8se@k~OA zfWN(FObx)sjqtZ>l-0x!Mv!0#+P2z&ZM258hu^IV>8(Tnk@h5W z&{_wHgM&6G zwxtSIW_SO+xlCL!3^|@jr(=3T#*)MEKEAb(22&W|OQZ2s6)iohjV0|t&c`lz;JDwa zokx&e+XX1{{kVsx(oC%%Mli1V-z4DB>Uw}Utcr`-qDg8pV_K;%_7wDZ9*0}v#6;Pu zmIr5~M$aVmgfeL!M3TO-*um4!$8gNO9ylzD$LwI|f~3RX56MYbqN*h7T=sME>YvlGJPSLKCNcDnC-5yM@)Uqn@M1>CLn_#aeN2g+Krc*sS#W@#73R16an1ef&?nF5#H zZ(Sm;bc%XqNnJO{RxmP|BdWFx)VAPj^Wn(agEfpJP9Z2 z2VkPUto44l=2SO+FYm^0r>mh2S>p}e!fcT>Q_4EtP}cekWAR6K%c9(DUhcSa5Zat&t!E3?*Hw|3 z<)&^#^)w9&zZ6lMrKnU>S=461`Fg?mnhMQjk2X7q&ZOa{4RF@&7p>^I5+e`a?KL(M ze9(V)$@wS-0*RP%-|;91V5+5sba6cU1NeEw{)-(f;s;P0s^JIF-z`HV2#>n@bwO3& z6WHSyYp{Eib!*x2)20E^;C}KyDAWZ4FNYJc=#l$HMj_bhCNVCJiJa#V6G>TA$3C6# zmdN~>yX1qvT7A&!eo6O^Ds~Bb{h|$f-(XW*Y?zA`xmb1`m_FFLyVya0njiG+UOUNa z`Fjl!^dOw^>@+=vk%1DbVN==bz7;|EU6Yjq?`yi`Z*f)m8~jn0^oK*yaB?9YR`4F= jg)xP;;FZYh)F6MYfsvkF0zTqBF0vcm>#5DI_jt&^8Ku1# diff --git a/docs/.doctrees/effects.doctree b/docs/.doctrees/effects.doctree new file mode 100644 index 0000000000000000000000000000000000000000..8dec84d6dd4ed5eb39bf23b4da43f582f9666bea GIT binary patch literal 45932 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 Ef5rZwjsO4v literal 0 HcmV?d00001 diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle index ff830a050f8e254e228b8f552affc01616277771..55e2066b8f799bb8f07b12a0d16b9e40e86b785f 100644 GIT binary patch literal 87710 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%0A-5*V7VK1$1~g6AYQ{nvRQ-vb=dsM+IM$eX7i; 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 delta 17797 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 Jglr3`{9pUue0~4` diff --git a/docs/.doctrees/frame.doctree b/docs/.doctrees/frame.doctree index 0304d294beb5c0f55a14fd277c35dd352db68de5..d1f789d540436c99efc30b6e2b9456340984cf37 100644 GIT binary patch literal 33388 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 literal 29600 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 Ai2wiq diff --git a/docs/.doctrees/index.doctree b/docs/.doctrees/index.doctree index 4945bdb4e09f44bdb375cbf44acc26c28ce9f799..932995ec0d2a6b6bbafb44989538a38ac6558b98 100644 GIT binary patch delta 815 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 K&-HY43jYH3VPRtc diff --git a/docs/.doctrees/lens.doctree b/docs/.doctrees/lens.doctree index 755bca8ff69dc5f5d6d5eb41ad849f20a9b3a066..13537aba59f25f04d2784353a595f0b1da10dfe4 100644 GIT binary patch literal 34384 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 D543M_ diff --git a/docs/.doctrees/window.doctree b/docs/.doctrees/window.doctree index 3722321a1fe12fb5eef552a34643b2d00769db9a..376aba8cdf9dabae537e2e81fc03d362c40a7ead 100644 GIT binary patch delta 2191 zcmbVNZA?>F81~%WwkT2xEfXq8K|cCn6|{ndDy0$uD}pj2P8s2&czeZ`k5*-iQR7#2 z%uS!F$<$?TKa5%2)Nbmw%xGe=?3bD?*-wc^W3sOwOP1)iZ0Fpz_fp*c?9b_WpZ9&A z_q@-0&*{zU{A@Ab!(83&?H+`*yTq``mYfAfvLj&6} zRZ&drCo3z|3A4I#9yy+>k;JanmNpc0M*7DG#sdBRu}y*CU})F)VkyC5jS1SGm_^uX z-5=XSf7O$TED09!k`|tqbiBm7YJGj&vCu^F=luscrizRhExWtd(={^K7xtUOg5SJL zDsLVL1;c@0BorARo-pqc0>gf@bf7?Rn-ms`g#BaYk+IOWvA!LPQ>XcVL~|6T_?7Z9 zf)nOJGMQ9qNE*TIk`^4aG|K3ED6{pFB^b3EQB?E_Swwec+_QzyTxKIyYDGy4UfAof zy!=Q;k80K|!6a@0ak#l|6+CVm0*-auP8C-A@^R1M#rG9q5=#vRzf?NtC)Hpt(pk3q zCwc?Hkx01C90}Wl8+P16+w->EU0{WY3O!h z$A)FblWGHK-&`e+P1QEynt^>vvE~_=cIi^mH3Qp5v4LqAUrDQi_zJ18R7&LxVnw{e zr6b66F2m8fYL31lqs{`oj)p7J>3Akx!D$Mnq|1@J5G#00IWOTwy^HAKbk}63`4pLO z-EE=~58REoP%}h7v&6Hu6_;yAad%Z?8j=aU%Zc2^UhisK#ivAvLNYHFkQMxZTr1eu(C$^wx%fZEYOa%JQq9|FilTX&TGISu3Z^vgkk7AAQ;ItXJdPb^Ezt4eqSA$Zv?Ck;R-D@5)k~;u{*K4R=gK z<+XB0*pES)Ioh4v!IUWtofD`@MeC>3ifH{BNwkkBm=et|*Jkugw6{7Q&%Vh}lX#t- zvnp$lS&5q-*RT&JJ1t9=asl^1!)`jy5~SO2Zqg#SaSeGG|9oSN<1)2zU)TF8%@gVu zAK}+-XB&Hj=-WdVcBc}{1GbYEHgi#0p0b^^u$Dx&L?&kk`*!%l6C?g{_U9-*!O-S~ zc19MJh@uinR3eB<$x$gW8WD14?u!EW#ZSm3)ErQ;1J@`vkz^R{( zf4)_N;D?F`w;<&`x~o&5yOP|c5^QD#mKmrtq(PIx0X5qkn-NUyObrYkJ>Wka8jr+y zg(t?3m7j>c7~9UXvyYyrAJ$C8l4iV|*9u!<)R+tXiabgvN{TsP-R?upc&A4VqHe)> z30abo5}=+`dDVN_K@E2y2FDSbba%O%I_ht=g=HX7;1XXlK6657Fxrul6~Mf$UqSegh8-n)5rX1o zJ-RQtEEtc$O-nWUJGm3fLY<}ImNP#yilMxGG46>{PfUo?@OGCWU(3j3<9s>ag0au6 z(L=E9?Im!nemd2NE%2d%5la{&Hi;7I8B#*kMlX}bMsW-&C>5h&RA&cQ!y)biDWBE( z1_g9B2B-r#BZIl8kK~sqs^np2n(9^S_`)zl7zfs}1m87R!0#3l)yEKSGxp&m*16pW zURx!+P_I<=t|dpD38i1yE0?#*4jtTF?sNpRZHz>MCMLA1a4kZ<~kJcO3NHv zYjj8t{O8Ws(XU{#=y0pbM{&pe$iPx-EBYs?OG)U~+8LIcd{iC_D5D(ZMB8+5M;(Gu zrvYV>%?)Q3)Y*EG5ni=f;b8llDXpy7hb=H+Z=`IjI6m&#J1Gx_z~iIK(E{oxU*@h$ zOIRV;cfCeMSh3YYB!xt)NgSqIwT>-JvSOQEZCir0t_C>gG@%fD<fIR1K<32?rv zQZtWr4<7LFT7JSL*xE}Wb@xg5x4j+(p^mFnJ$%dpX}0T}mI|_Bvt8J8gj*7$E7_%F`)n4zs}1^S=*9%gs1f<>c+Rl5Rqx}3-tb*bZTxh5#; zv^-w)^f_AYm&;_rAy{#njbs|)A&3|9b`#4jB8dxOS`e4JM7oRV^5h^tw0TO)h|`Sd yFe4hzh=w!R2gNvL;nNEIZ@yM diff --git a/docs/_modules/displayarray/__main__/index.html b/docs/_modules/displayarray/__main__/index.html new file mode 100644 index 0000000..0dd93d9 --- /dev/null +++ b/docs/_modules/displayarray/__main__/index.html @@ -0,0 +1,185 @@ + + + + + + + displayarray.__main__ — DisplayArray documentation + + + + + + + + + + + + + + + + + + +

+
+
+ + +
+ +

Source code for displayarray.__main__

+"""
+DisplayArray.
+
+Display NumPy arrays.
+
+Usage:
+  displayarray (-w <webcam-number> | -v <video-filename> | -t <topic-name>[,dtype])... [-m <msg-backend>]
+  displayarray -h
+  displayarray --version
+
+
+Options:
+  -h, --help                                           Show this help text.
+  --version                                            Show version number.
+  -w <webcam-number>, --webcam=<webcam-number>         Display video from a webcam.
+  -v <video-filename>, --video=<video-filename>        Display frames from a video file.
+  -t <topic-name>, --topic=<topic-name>                Display frames from a topic using the chosen message broker.
+  -m <msg-backend>, --message-backend <msg-backend>    Choose message broker backend. [Default: ROS]
+                                                       Currently supported: ROS, ZeroMQ
+  --ros                                                Use ROS as the backend message broker.
+  --zeromq                                             Use ZeroMQ as the backend message broker.
+"""
+
+from docopt import docopt
+import asyncio
+
+
+
[docs]def main(argv=None): + """Process command line arguments.""" + arguments = docopt(__doc__, argv=argv) + if arguments["--version"]: + from displayarray import __version__ + + print(f"DisplayArray V{__version__}") + return + from displayarray import display + + vids = [int(w) for w in arguments["--webcam"]] + arguments["--video"] + v_disps = None + if vids: + v_disps = display(*vids, blocking=False) + from displayarray.frame.frame_updater import read_updates_ros, read_updates_zero_mq + + topics = arguments["--topic"] + topics_split = [t.split(",") for t in topics] + d = display() + + async def msg_recv(): + nonlocal d + while d: + if arguments["--message-backend"] == "ROS": + async for v_name, frame in read_updates_ros( + [t for t, d in topics_split], [d for t, d in topics_split] + ): + d.update(arr=frame, id=v_name) + if arguments["--message-backend"] == "ZeroMQ": + async for v_name, frame in read_updates_zero_mq( + *[bytes(t, encoding="ascii") for t in topics] + ): + d.update(arr=frame, id=v_name) + + async def update_vids(): + while v_disps: + if v_disps: + v_disps.update() + await asyncio.sleep(0) + + async def runner(): + await asyncio.wait([msg_recv(), update_vids()]) + + loop = asyncio.get_event_loop() + loop.run_until_complete(runner()) + loop.close()
+ + +if __name__ == "__main__": + main() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/displayarray/effects/crop/index.html b/docs/_modules/displayarray/effects/crop/index.html index e569173..3a71165 100644 --- a/docs/_modules/displayarray/effects/crop/index.html +++ b/docs/_modules/displayarray/effects/crop/index.html @@ -31,16 +31,24 @@

Source code for displayarray.effects.crop

-import numpy as np
+"""Crop any n-dimensional array."""
+
+import numpy as np
 from ..input import mouse_loop
 
 
-
[docs]class Crop(object): +
[docs]class Crop(object): """A callback class that will return the input array cropped to the output size. N-dimensional.""" def __init__(self, output_size=(64, 64, 3), center=None): + """ + Create the cropping callback class. + + :param output_size: Specified the size the input should be cropped to. Can be redefined later. + :param center: Specifies the center on the input array to take the crop out of. + """ self._output_size = None - self._center = None + self._center = np.asarray([o // 2 for o in output_size]) self.odd_center = None self.mouse_control = None self.input_size = None @@ -50,23 +58,27 @@ @property def output_size(self): + """Get the output size.""" return self._output_size @output_size.setter def output_size(self, set): + """Set the output size.""" self._output_size = set if self._output_size is not None: self._output_size = np.asarray(set) @property def center(self): + """Get the center.""" return self._center @center.setter def center(self, set): - self._center = set - if self._center is not None: - self._center = np.asarray(set) + """Set the center. Guarded so that colors need not be set.""" + if set is not None: + for x in range(len(set)): + self._center[x] = set[x] def __call__(self, arr): """Crop the input array to the specified output size. output is centered on self.center point on input.""" @@ -119,8 +131,8 @@ out_array[put_slices] = arr[get_slices] return out_array.astype(arr.dtype) -
[docs] def enable_mouse_control(self): - """Move the mouse to move where the crop is from on the original image""" +
[docs] def enable_mouse_control(self): + """Move the mouse to move where the crop is from on the original image.""" @mouse_loop def m_loop(me): @@ -154,13 +166,10 @@

Navigation

Python API

Bash API

    diff --git a/docs/_modules/displayarray/effects/lens/index.html b/docs/_modules/displayarray/effects/lens/index.html index 315887a..8465230 100644 --- a/docs/_modules/displayarray/effects/lens/index.html +++ b/docs/_modules/displayarray/effects/lens/index.html @@ -31,87 +31,159 @@

    Source code for displayarray.effects.lens

    -import numpy as np
    +"""Create lens effects. Currently only 2D+color arrays are supported."""
    +
    +import numpy as np
     from ..input import mouse_loop
     import cv2
     
    +try:
    +    import torch
    +except ImportError:
    +    torch = None  # type: ignore
    +
    +
    +
    [docs]class ControllableLens(object): + """A lens callback that can be controlled by the program or the user.""" -class ControllableLens(object): def __init__(self, use_bleed=False, zoom=1, center=None): - self.center = center - self.zoom = zoom + """Create the lens callback.""" + self._center = center + self._zoom = zoom self.use_bleed = use_bleed self.bleed = None self.mouse_control = None - def check_setup_bleed(self, arr): + def _check_setup_bleed(self, arr): if not isinstance(self.bleed, np.ndarray) and self.use_bleed: self.bleed = np.zeros_like(arr) - def run_bleed(self, arr, x, y): +
    [docs] def run_bleed(self, arr, x, y): + """Spread color outwards, like food coloring in water.""" arr[y, ...] = (arr[(y + 1) % len(y), ...] + arr[(y - 1) % len(y), ...]) / 2 arr[:, x, ...] = ( arr[:, (x + 1) % len(x), ...] + arr[:, (x - 1) % len(x), ...] - ) / 2 + ) / 2
    -
    [docs]class Barrel(ControllableLens): +
    [docs]class Barrel(ControllableLens): + """A barrel lens distortion callback.""" + def __init__( self, use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None ): + """Create the barrel lens distortion callback.""" super().__init__(use_bleed, zoom, center) - self.center = center - self.zoom = zoom + self._center = center + self._zoom = zoom self.use_bleed = use_bleed self.bleed = None - self.barrel_power = barrel_power + self._barrel_power = barrel_power self.mouse_control = None + self.input_size = None -
    [docs] def enable_mouse_control(self): + @property + def center(self): + """Guarded get center. Limits to within input.""" + if self.input_size is not None: + self._center[:] = [ + min(max(0, s), self.input_size[i]) for i, s in enumerate(self._center) + ] + return self._center + + @center.setter + def center(self, setpoint): + """Guarded set center. Limits to within input.""" + if self.input_size is not None: + setpoint = [ + min(max(0, s), self.input_size[i]) for i, s in enumerate(setpoint) + ] + self._center[:] = setpoint + + @property + def zoom(self): + """Guarded zoom. Avoids divide by zero conditions.""" + if self._zoom == 0: + return 1e-15 + else: + return self._zoom + + @property + def barrel_power(self): + """Guarded barrel power. Avoids divide by zero conditions.""" + if self._barrel_power == 0: + return 1e-15 + else: + return self._barrel_power + + @barrel_power.setter + def barrel_power(self, setpoint): + """Set the barrel power.""" + self._barrel_power = setpoint + + @zoom.setter # type: ignore + def zoom(self, setpoint): + """Set the zoom power.""" + self._zoom = setpoint + +
    [docs] def enable_mouse_control(self, crop_size=None): """ - Move the mouse to center the image, scroll to increase/decrease barrel, ctrl+scroll to increase/decrease zoom + Enable the default mouse controls. + + Move the mouse to center the image + scroll to increase/decrease barrel + ctrl+scroll to increase/decrease zoom """ @mouse_loop def m_loop(me): - self.center[:] = [me.y, me.x] - if me.event == cv2.EVENT_MOUSEWHEEL: - if me.flags & cv2.EVENT_FLAG_CTRLKEY: - if me.flags > 0: - self.zoom *= 1.1 - else: - self.zoom /= 1.1 + if self.input_size is not None: + if crop_size is not None: + self.center = [ + me.y * self.input_size[0] / crop_size[0], + me.x * self.input_size[1] / crop_size[1], + ] else: - if me.flags > 0: - self.barrel_power *= 1.1 + self.center = [me.y, me.x] + if me.event == cv2.EVENT_MOUSEWHEEL: + if me.flags & cv2.EVENT_FLAG_CTRLKEY: + if me.flags > 0: + self.zoom *= 1.1 + else: + self.zoom /= 1.1 else: - self.barrel_power /= 1.1 + if me.flags > 0: + self.barrel_power *= 1.1 + else: + self.barrel_power /= 1.1 + print(self.barrel_power) self.mouse_control = m_loop return self
    def __call__(self, arr): + """Run the lens distortion algorithm on the input.""" zoom_out = 1.0 / self.zoom - self.check_setup_bleed(arr) + self._check_setup_bleed(arr) + + self.input_size = arr.shape y = np.arange(arr.shape[0]) x = np.arange(arr.shape[1]) - if self.center is None: - self.center = [len(y) / 2.0, len(x) / 2.0] + if self._center is None: + self._center = [len(y) / 2.0, len(x) / 2.0] y2_ = (y - (len(y) / 2.0)) * zoom_out / arr.shape[0] x2_ = (x - (len(x) / 2.0)) * zoom_out / arr.shape[1] p2 = np.array(np.meshgrid(x2_, y2_)) - cy = self.center[0] / arr.shape[0] - cx = self.center[1] / arr.shape[1] - - barrel_power = self.barrel_power + cy = self._center[0] / arr.shape[0] + cx = self._center[1] / arr.shape[1] theta = np.arctan2(p2[1], p2[0]) radius = np.linalg.norm(p2, axis=0, ord=2) - radius = pow(radius, barrel_power) + radius = pow(radius, self.barrel_power) x_new = 0.5 * (radius * np.cos(theta) + cx * 2) x_new = np.clip(x_new * len(x), 0, len(x) - 1) @@ -135,10 +207,54 @@ return arr
    -
    [docs]class Mustache(ControllableLens): +
    [docs]class BarrelPyTorch(Barrel): + """A barrel distortion callback class accelerated by PyTorch.""" + + def __call__(self, arr): + """Run a pytorch accelerated lens distortion algorithm on the input.""" + zoom_out = 1.0 / self.zoom + self.input_size = arr.shape + y = torch.arange(arr.shape[1]).type(torch.FloatTensor).cuda() + x = torch.arange(arr.shape[0]).type(torch.FloatTensor).cuda() + if self._center is None: + self._center = [y.shape[0] / 2.0, x.shape[0] / 2.0] + + y2_ = (y - (y.shape[0] / 2.0)) * zoom_out / arr.shape[1] + x2_ = (x - (x.shape[0] / 2.0)) * zoom_out / arr.shape[0] + p2 = torch.stack(torch.meshgrid(x2_, y2_)) + + cy = self._center[1] / arr.shape[1] + cx = self._center[0] / arr.shape[0] + + theta = torch.atan2(p2[1], p2[0]) + + radius = torch.norm(p2, dim=0) + + radius = torch.pow(radius, self.barrel_power) + + x_new = 0.5 * (radius * torch.cos(theta) + cx * 2) + x_new = torch.clamp(x_new * x.shape[0], 0, x.shape[0] - 1) + + y_new = 0.5 * (radius * torch.sin(theta) + cy * 2) + y_new = torch.clamp(y_new * y.shape[0], 0, y.shape[0] - 1) + + p = torch.stack(torch.meshgrid([x, y])).type(torch.IntTensor) + + p_new = torch.stack((x_new, y_new)) + p_new = p_new.type(torch.IntTensor) + + arr[p[0], p[1], :] = arr[p_new[0], p_new[1], :] + + return arr
    + + +
    [docs]class Mustache(ControllableLens): + """A mustache distortion callback.""" + def __init__( self, use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None ): + """Create the mustache distortion callback.""" super().__init__(use_bleed, zoom, center) self.center = center self.zoom = zoom @@ -148,7 +264,15 @@ self.pincushion_power = pincushion_power self.mouse_control = None - def enable_mouse_control(self): +
    [docs] def enable_mouse_control(self): + """ + Enable the default mouse loop to control the mustache distortion. + + ctrl+scroll wheel: zoom in and out + shift+scroll wheel: adjust pincushion power + scroll wheel: adjust barrel power + """ + @mouse_loop def m_loop(me): self.center[:] = [me.y, me.x] @@ -169,11 +293,12 @@ else: self.barrel_power /= 1.1 - self.mouse_control = m_loop + self.mouse_control = m_loop
    def __call__(self, arr): + """Run the mustache distortion algorithm on the input numpy array.""" zoom_out = 1.0 / self.zoom - self.check_setup_bleed(arr) + self._check_setup_bleed(arr) y = np.arange(arr.shape[0]) x = np.arange(arr.shape[1]) @@ -234,13 +359,10 @@

    Navigation

    Python API

    Bash API

      diff --git a/docs/_modules/displayarray/effects/select_channels/index.html b/docs/_modules/displayarray/effects/select_channels/index.html index 2c46bb2..ed1ce40 100644 --- a/docs/_modules/displayarray/effects/select_channels/index.html +++ b/docs/_modules/displayarray/effects/select_channels/index.html @@ -31,13 +31,24 @@

      Source code for displayarray.effects.select_channels

      -import numpy as np
      +"""Reduce many color images to the three colors that your eyeballs can see."""
      +
      +import numpy as np
       from ..input import mouse_loop
       import cv2
       
      +from typing import Iterable
       
      -
      [docs]class SelectChannels(object): - def __init__(self, selected_channels=None): + +
      [docs]class SelectChannels(object): + """ + Select channels to display from an array with too many colors. + + :param selected_channels: the list of channels to display. + """ + + def __init__(self, selected_channels: Iterable[int] = None): + """Select which channels from the input array to display in the output.""" if selected_channels is None: selected_channels = [0, 0, 0] self.selected_channels = selected_channels @@ -46,6 +57,7 @@ self.num_input_channels = None def __call__(self, arr): + """Run the channel selector.""" self.num_input_channels = arr.shape[-1] out_arr = [ arr[..., min(max(0, x), arr.shape[-1] - 1)] for x in self.selected_channels @@ -53,7 +65,15 @@ out_arr = np.stack(out_arr, axis=-1) return out_arr - def enable_mouse_control(self): +
      [docs] def enable_mouse_control(self): + """ + Enable mouse control. + + Alt+Scroll to increase/decrease channel 2. + Shift+Scroll to increase/decrease channel 1. + Ctrl+scroll to increase/decrease channel 0. + """ + @mouse_loop def m_loop(me): if me.event == cv2.EVENT_MOUSEWHEEL: @@ -91,7 +111,7 @@ if self.mouse_print_channels: print(f"Channel 2 now maps to {self.selected_channels[2]}.") - self.mouse_control = m_loop
      + self.mouse_control = m_loop
      @@ -112,13 +132,10 @@

      Navigation

      Python API

      Bash API

        diff --git a/docs/_modules/displayarray/frame/frame_publishing/index.html b/docs/_modules/displayarray/frame/frame_publishing/index.html index 8c273e4..5945ed5 100644 --- a/docs/_modules/displayarray/frame/frame_publishing/index.html +++ b/docs/_modules/displayarray/frame/frame_publishing/index.html @@ -31,7 +31,8 @@

        Source code for displayarray.frame.frame_publishing

        -import sys
        +"""Publish frames so any function within this program can find them."""
        +
         import threading
         import time
         import asyncio
        @@ -41,7 +42,7 @@
         
         from displayarray.frame import subscriber_dictionary
         from .np_to_opencv import NpCam
        -from displayarray.uid import uid_for_source
        +from displayarray._uid import uid_for_source
         
         from typing import Union, Tuple, Optional, Dict, Any, List, Callable
         
        @@ -146,6 +147,7 @@
             copy=True,
             track=False
         ):
        +    """Publish frames to ZeroMQ when they're updated."""
             import zmq
             from displayarray import read_updates
         
        @@ -193,6 +195,7 @@
             rate_hz=None,
             dtype=None
         ):
        +    """Publish frames to ROS when they're updated."""
             import rospy
             from rospy.numpy_msg import numpy_msg
             import std_msgs.msg
        @@ -220,7 +223,7 @@
                     )  # allow users to use their own custom messages in numpy arrays
                 return msg_type
         
        -    publishers = {}
        +    publishers: Dict[str, rospy.Publisher] = {}
             rospy.init_node(node_name, anonymous=True)
             try:
                 for v in read_updates(vids, callbacks, fps_limit, size, end_callback, blocking):
        @@ -268,13 +271,10 @@
         

        Navigation

        Python API

        Bash API

          diff --git a/docs/_modules/displayarray/frame/frame_updater/index.html b/docs/_modules/displayarray/frame/frame_updater/index.html index 3673043..34940d7 100644 --- a/docs/_modules/displayarray/frame/frame_updater/index.html +++ b/docs/_modules/displayarray/frame/frame_updater/index.html @@ -31,14 +31,16 @@

          Source code for displayarray.frame.frame_updater

          -import threading
          +"""Get and handle updated frames."""
          +
          +import threading
           import asyncio
           from typing import Union, Tuple, Any, Callable, List, Optional, Dict
           
           import numpy as np
           
           from displayarray.callbacks import global_cv_display_callback
          -from displayarray.uid import uid_for_source
          +from displayarray._uid import uid_for_source
           from displayarray.frame import subscriber_dictionary
           from displayarray.frame.frame_publishing import pub_cam_thread
           from displayarray.window import window_commands
          @@ -59,6 +61,7 @@
                   high_speed: bool = True,
                   fps_limit: float = 240,
               ):
          +        """Create the frame updater thread."""
                   super(FrameUpdater, self).__init__(target=self.loop, args=())
                   self.cam_id = uid_for_source(video_source)
                   self.video_source = video_source
          @@ -78,7 +81,7 @@
                       continue
           
               def __apply_callbacks_to_frame(self, frame):
          -        if frame is not None:
          +        if frame is not None and not isinstance(frame, NoData):
                       try:
                           for c in self.callbacks:
                               frame_c = c(frame)
          @@ -148,20 +151,28 @@
           
          [docs]async def read_updates( *vids, callbacks: Optional[ - Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable] + Union[ + Dict[Any, Union[FrameCallable, List[FrameCallable]]], + List[FrameCallable], + FrameCallable, + ] ] = None, fps_limit=float("inf"), size=(-1, -1), end_callback: Callable[[], bool] = lambda: False, blocking=True, ): - """Reads back all updates from the requested videos. + """ + Read back all updates from the requested videos. Example usage: - >>> from examples.videos import test_video - >>> f = 0 - >>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)): - ... print(f"Frame:{f}. Array:{r}") + + .. code-block:: python + + >>> from examples.videos import test_video + >>> f = 0 + >>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)): + ... print(f"Frame:{f}. Array:{r}") """ from displayarray.window import SubscriberWindows @@ -201,13 +212,14 @@ async def read_updates_zero_mq( *topic_names, - address="tcp://127.0.0.1:5600", - flags=0, - copy=True, - track=False, - blocking=False, - end_callback: Callable[[Any], bool] = lambda: False, + address: str = "tcp://127.0.0.1:5600", + flags: int = 0, + copy: bool = True, + track: bool = False, + blocking: bool = False, + end_callback: Callable[[Any], bool] = lambda x: False, ): + """Read updated frames from ZeroMQ.""" import zmq ctx = zmq.Context() @@ -243,8 +255,9 @@ dtypes=None, listener_node_name=None, poll_rate_hz=None, - end_callback: Callable[[Any], bool] = lambda: False, + end_callback: Callable[[Any], bool] = lambda x: False, ): + """Read updated frames from ROS.""" import rospy from rospy.numpy_msg import numpy_msg from rospy.client import _WFM @@ -322,7 +335,8 @@ pass finally: if s is not None: - s.unregister() + for _, sub in s.items(): + sub.unregister() if rospy.core.is_shutdown(): raise rospy.exceptions.ROSInterruptException("rospy shutdown")
          @@ -345,13 +359,10 @@

          Navigation

          Python API

          Bash API

            diff --git a/docs/_modules/displayarray/frame/get_frame_ids/index.html b/docs/_modules/displayarray/frame/get_frame_ids/index.html index d1257b9..88c03ca 100644 --- a/docs/_modules/displayarray/frame/get_frame_ids/index.html +++ b/docs/_modules/displayarray/frame/get_frame_ids/index.html @@ -31,7 +31,9 @@

            Source code for displayarray.frame.get_frame_ids

            -import cv2
            +"""Get camera IDs."""
            +
            +import cv2
             
             from typing import List
             
            @@ -67,13 +69,10 @@
             

            Navigation

            Python API

            Bash API

              diff --git a/docs/_modules/displayarray/frame/np_to_opencv/index.html b/docs/_modules/displayarray/frame/np_to_opencv/index.html index fbef36c..3ad2aea 100644 --- a/docs/_modules/displayarray/frame/np_to_opencv/index.html +++ b/docs/_modules/displayarray/frame/np_to_opencv/index.html @@ -31,7 +31,9 @@

              Source code for displayarray.frame.np_to_opencv

              -import numpy as np
              +"""Allow OpenCV to handle numpy arrays as input."""
              +
              +import numpy as np
               import cv2
               
               
              @@ -39,12 +41,18 @@
                   """Add OpenCV camera controls to a numpy array."""
               
                   def __init__(self, img):
              +        """Create a fake camera for OpenCV based on the initial array."""
                       assert isinstance(img, np.ndarray)
                       self.__img = img
                       self.__is_opened = True
              -
              -        self.__width = self.__img.shape[1]
              -        self.__height = self.__img.shape[0]
              +        if len(img.shape) > 0:
              +            self.__height = self.__img.shape[0]
              +            if len(self.__img.shape) > 1:
              +                self.__width = self.__img.shape[1]
              +            else:
              +                self.__width = self.__height
              +        else:
              +            self.__width = self.__height = 1
                       self.__ratio = float(self.__width) / self.__height
               
                       self.__wait_for_ratio = False
              @@ -105,13 +113,10 @@
               

              Navigation

              Python API

              Bash API

                diff --git a/docs/_modules/displayarray/input/index.html b/docs/_modules/displayarray/input/index.html index d5d6e6a..c6529da 100644 --- a/docs/_modules/displayarray/input/index.html +++ b/docs/_modules/displayarray/input/index.html @@ -31,7 +31,9 @@

                Source code for displayarray.input

                -from displayarray.window import window_commands
                +"""Decorators for creating input loops that OpenCV handles."""
                +
                +from displayarray.window import window_commands
                 import threading
                 import time
                 
                @@ -42,6 +44,7 @@
                     """Holds all the OpenCV mouse event information."""
                 
                     def __init__(self, event, x, y, flags, param):
                +        """Create an OpenCV mouse event."""
                         self.event = event
                         self.x = x
                         self.y = y
                @@ -95,10 +98,21 @@
                 
                 
                 
                [docs]class mouse_loop(object): # NOSONAR - """Run a function on mouse information that is received by the window, continuously in a new thread.""" + """ + 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)) + .. code-block:: python + + >>> @mouse_loop + ... def fun(mouse_event): + ... print("x:{}, y:{}".format(mouse_event.x, mouse_event.y)) + """ + + def __init__(self, f): + """Start a new mouse thread for the decorated function.""" + self.t = threading.Thread( + target=_mouse_loop_thread(f, run_when_no_events=False) + ) self.t.start() def __call__(self, *args, **kwargs): @@ -144,10 +158,19 @@
                [docs]class key_loop(object): # NOSONAR - """Run a function on mouse information that is received by the window, continuously in a new thread.""" + """ + 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)) + .. code-block:: python + + >>> @key_loop + ... def fun(key): + ... print("key pressed:{}".format(key)) + """ + + def __init__(self, f: Callable[[str], None]): + """Start a new key thread for the decorated function.""" + self.t = threading.Thread(target=_key_loop_thread(f, run_when_no_events=False)) self.t.start() def __call__(self, *args, **kwargs): @@ -173,13 +196,10 @@

                Navigation

                Python API

                Bash API

                  diff --git a/docs/_modules/displayarray/window/subscriber_windows/index.html b/docs/_modules/displayarray/window/subscriber_windows/index.html index 0a059c9..8e5f4a9 100644 --- a/docs/_modules/displayarray/window/subscriber_windows/index.html +++ b/docs/_modules/displayarray/window/subscriber_windows/index.html @@ -31,7 +31,9 @@

                  Source code for displayarray.window.subscriber_windows

                  -import warnings
                  +"""OpenCV windows that will display the arrays."""
                  +
                  +import warnings
                   from threading import Thread
                   from typing import List, Union, Callable, Any, Dict, Iterable, Optional
                   
                  @@ -40,17 +42,17 @@
                   from localpubsub import NoData
                   
                   from displayarray.callbacks import global_cv_display_callback
                  -from displayarray.uid import uid_for_source
                  +from displayarray._uid import uid_for_source
                   from displayarray.frame import subscriber_dictionary
                   from displayarray.frame.frame_updater import FrameCallable
                   from displayarray.frame.frame_updater import FrameUpdater
                   from displayarray.input import MouseEvent
                   from displayarray.window import window_commands
                  -from displayarray.util import WeakMethod
                  +from displayarray._util import WeakMethod
                   from displayarray.effects.select_channels import SelectChannels
                   
                   
                  -
                  [docs]class SubscriberWindows(object): +
                  [docs]class SubscriberWindows(object): """Windows that subscribe to updates to cameras, videos, and arrays.""" FRAME_DICT: Dict[str, np.ndarray] = {} @@ -62,6 +64,7 @@ video_sources: Iterable[Union[str, int]] = (0,), callbacks: Optional[List[Callable[[np.ndarray], Any]]] = None, ): + """Create the array displaying window.""" self.source_names: List[Union[str, int]] = [] self.close_threads: Optional[List[Thread]] = [] self.frames: List[np.ndarray] = [] @@ -84,12 +87,13 @@ self.update() return not self.exited - def block(self): +
                  [docs] def block(self): + """Update the window continuously while blocking the outer program.""" self.loop() for ct in self.close_threads: - ct.join() + ct.join()
                  -
                  [docs] def add_source(self, name): +
                  [docs] def add_source(self, name): """Add another source for this class to display.""" uid = uid_for_source(name) self.source_names.append(uid) @@ -97,7 +101,7 @@ self.input_cams.append(name) return self
                  -
                  [docs] def add_window(self, name): +
                  [docs] 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)") @@ -105,7 +109,7 @@ cv2.setMouseCallback(name + " (press ESC to quit)", m) return self
                  -
                  [docs] def add_callback(self, callback): +
                  [docs] def add_callback(self, callback): """Add a callback for this class to apply to videos.""" self.callbacks.append(callback) return self
                  @@ -114,9 +118,7 @@ for c in self.source_names: subscriber_dictionary.stop_cam(c) -
                  [docs] def handle_keys( - self, key_input # type: int - ): +
                  [docs] def handle_keys(self, key_input: 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: @@ -135,7 +137,7 @@ ) )
                  -
                  [docs] def handle_mouse(self, event, x, y, flags, param): +
                  [docs] 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)
                  @@ -188,7 +190,7 @@ self.frames[fr] = self.callbacks[-1](self.frames[fr]) break -
                  [docs] def update_window_frames(self): +
                  [docs] def update_window_frames(self): """Update the windows with the newest data for all frames.""" self.frames = [] for i in range(len(self.input_vid_global_names)): @@ -204,9 +206,10 @@ if frame is not None: self.frames[-1] = frame self.__check_too_many_channels() + self.FRAME_DICT[self.input_vid_global_names[i]] = NoData() self._display_frames(self.frames)
                  -
                  [docs] def update(self, arr: np.ndarray = None, id: str = None): +
                  [docs] def update(self, arr: np.ndarray = None, id: str = 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) @@ -219,7 +222,7 @@ key = self.handle_keys(cv2.waitKey(1)) return msg_cmd, key
                  -
                  [docs] def wait_for_init(self): +
                  [docs] 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 = "" @@ -227,7 +230,7 @@ msg_cmd, key = self.update() return self
                  -
                  [docs] def end(self): +
                  [docs] def end(self): """Close all threads. Should be used with non-blocking mode.""" window_commands.quit(force_all_read=False) self.__stop_all_cams() @@ -246,7 +249,7 @@ def __delete__(self, instance): self.end() -
                  [docs] def loop(self): +
                  [docs] def loop(self): """Continually update window frame. OpenCV only allows this in the main thread.""" sub_cmd = window_commands.win_cmd_sub() msg_cmd = "" @@ -259,7 +262,10 @@ def _get_video_callback_dict_threads( - *vids, callbacks: Optional[Dict[Any, FrameCallable]] = None, fps=240, size=(-1, -1) + *vids, + callbacks: Optional[Dict[Any, Union[FrameCallable, List[FrameCallable]]]] = None, + fps=240, + size=(-1, -1), ): assert callbacks is not None vid_threads = [] @@ -268,14 +274,14 @@ v_callbacks: List[Callable[[np.ndarray], Any]] = [] if v_name in callbacks: if isinstance(callbacks[v_name], List): - v_callbacks.extend(callbacks[v_name]) + v_callbacks.extend(callbacks[v_name]) # type: ignore elif callable(callbacks[v_name]): - v_callbacks.append(callbacks[v_name]) + v_callbacks.append(callbacks[v_name]) # type: ignore if v in callbacks: if isinstance(callbacks[v], List): - v_callbacks.extend(callbacks[v]) + v_callbacks.extend(callbacks[v]) # type: ignore elif callable(callbacks[v]): - v_callbacks.append(callbacks[v]) + v_callbacks.append(callbacks[v]) # type: ignore vid_threads.append( FrameUpdater(v, callbacks=v_callbacks, fps_limit=fps, request_size=size) ) @@ -285,7 +291,11 @@ def _get_video_threads( *vids, callbacks: Optional[ - Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable] + Union[ + Dict[Any, Union[FrameCallable, List[FrameCallable]]], + List[FrameCallable], + FrameCallable, + ] ] = None, fps=240, size=(-1, -1), @@ -312,10 +322,14 @@ return vid_threads -
                  [docs]def display( +
                  [docs]def display( *vids, callbacks: Optional[ - Union[Dict[Any, FrameCallable], List[FrameCallable], FrameCallable] + Union[ + Dict[Any, Union[FrameCallable, List[FrameCallable]]], + List[FrameCallable], + FrameCallable, + ] ] = None, window_names=None, blocking=False, @@ -346,7 +360,7 @@ return s
                  -
                  [docs]def breakpoint_display(*args, **kwargs): +
                  [docs]def breakpoint_display(*args, **kwargs): """Display all the arrays, cameras, and videos passed in. Stops code execution until the window is closed.""" return display(*args, **kwargs, blocking=True)
                  @@ -369,13 +383,10 @@

                  Navigation

                  Python API

                  Bash API

                    diff --git a/docs/_modules/index.html b/docs/_modules/index.html index 9c9b24c..bfd20e6 100644 --- a/docs/_modules/index.html +++ b/docs/_modules/index.html @@ -31,7 +31,8 @@

                    All modules for which code is available

                    -
                    • displayarray.effects.crop
                    • +
                      • displayarray.__main__
                      • +
                      • displayarray.effects.crop
                      • displayarray.effects.lens
                      • displayarray.effects.select_channels
                      • displayarray.frame.frame_publishing
                      • @@ -60,13 +61,10 @@

                        Navigation

                        Python API

                        Bash API

                          diff --git a/docs/docsrc/displayarray.rst b/docs/_sources/display.rst.txt similarity index 59% rename from docs/docsrc/displayarray.rst rename to docs/_sources/display.rst.txt index b54dc88..1a324d2 100644 --- a/docs/docsrc/displayarray.rst +++ b/docs/_sources/display.rst.txt @@ -1,12 +1,11 @@ -displayarray +displayarray.display =================================== .. automodule:: displayarray -Display -------- .. autofunction:: display .. autofunction:: breakpoint_display -Frames ------- -.. autofunction:: read_updates \ No newline at end of file +Windows +------- +.. autoclass:: displayarray.window.SubscriberWindows + :members: \ No newline at end of file diff --git a/docs/_sources/displayarray.rst.txt b/docs/_sources/displayarray.rst.txt index b54dc88..7406c76 100644 --- a/docs/_sources/displayarray.rst.txt +++ b/docs/_sources/displayarray.rst.txt @@ -9,4 +9,13 @@ Display Frames ------ -.. autofunction:: read_updates \ No newline at end of file +.. autofunction:: read_updates + +Effects +------- +.. autoclass:: displayarray.effects.crop.Crop + :members: +.. autoclass:: displayarray.effects.lens.Barrel + :members: +.. autoclass:: displayarray.effects.select_channels.SelectChannels + :members: diff --git a/docs/_sources/effects.rst.txt b/docs/_sources/effects.rst.txt new file mode 100644 index 0000000..4e81234 --- /dev/null +++ b/docs/_sources/effects.rst.txt @@ -0,0 +1,30 @@ +displayarray.effects +==================== + +Crop +---- +.. currentmodule:: displayarray.effects.crop + +.. autoclass:: Crop + :members: + +Lens +---- +.. currentmodule:: displayarray.effects.lens + +.. automodule:: displayarray.effects.lens + :members: + +.. autoclass:: Barrel + :members: + +.. autoclass:: Mustache + :members: + +Select Channels +--------------- +.. currentmodule:: displayarray.effects.select_channels + +.. autoclass:: SelectChannels + :members: + diff --git a/docs/_sources/frame.rst.txt b/docs/_sources/frame.rst.txt index 3d9e51e..8d8412f 100644 --- a/docs/_sources/frame.rst.txt +++ b/docs/_sources/frame.rst.txt @@ -1,6 +1,14 @@ displayarray.frame =================================== +Read Updates +------------ +.. currentmodule:: displayarray + +.. autofunction:: read_updates + +Frame Passing +------------- .. currentmodule:: displayarray.frame .. automodule:: displayarray.frame diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt index 9bed90f..8b5c696 100644 --- a/docs/_sources/index.rst.txt +++ b/docs/_sources/index.rst.txt @@ -14,13 +14,10 @@ DisplayArray displays arrays. :maxdepth: 3 :caption: Python API - displayarray - displayarray.effects.crop - displayarray.effects.lens - displayarray.effects.select_channels - displayarray.frame - displayarray.window - displayarray.input + display + frame + effects + input .. toctree:: :maxdepth: 2 diff --git a/docs/crop/index.html b/docs/crop/index.html index 8d62983..0761e6f 100644 --- a/docs/crop/index.html +++ b/docs/crop/index.html @@ -38,10 +38,22 @@
                          class Crop(output_size=(64, 64, 3), center=None)[source]

                          A callback class that will return the input array cropped to the output size. N-dimensional.

                          +
                          +
                          +property center
                          +

                          Get the center.

                          +
                          +
                          enable_mouse_control()[source]
                          -

                          Move the mouse to move where the crop is from on the original image

                          +

                          Move the mouse to move where the crop is from on the original image.

                          +
                          + +
                          +
                          +property output_size
                          +

                          Get the output size.

                          diff --git a/docs/display/index.html b/docs/display/index.html new file mode 100644 index 0000000..1cc6500 --- /dev/null +++ b/docs/display/index.html @@ -0,0 +1,212 @@ + + + + + + + displayarray.display — DisplayArray documentation + + + + + + + + + + + + + + + + + + + + +
                          +
                          +
                          + + +
                          + +
                          +

                          displayarray.display

                          +

                          Display any array, webcam, or video file.

                          +

                          display is a function that displays these in their own windows.

                          +
                          +
                          +display(*vids, callbacks: Union[Dict[Any, Union[Callable[[numpy.ndarray], Optional[numpy.ndarray]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, window_names=None, blocking=False, fps_limit=240, size=(-1, -1))[source]
                          +

                          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

                          +
                          + +
                          +
                          +breakpoint_display(*args, **kwargs)[source]
                          +

                          Display all the arrays, cameras, and videos passed in. Stops code execution until the window is closed.

                          +
                          + +
                          +

                          Windows

                          +
                          +
                          +class SubscriberWindows(window_names: Iterable[str] = ('displayarray',), video_sources: Iterable[Union[str, int]] = (0,), callbacks: Optional[List[Callable[[numpy.ndarray], Any]]] = None)[source]
                          +

                          Windows that subscribe to updates to cameras, videos, and arrays.

                          +
                          +
                          +add_callback(callback)[source]
                          +

                          Add a callback for this class to apply to videos.

                          +
                          + +
                          +
                          +add_source(name)[source]
                          +

                          Add another source for this class to display.

                          +
                          + +
                          +
                          +add_window(name)[source]
                          +

                          Add another window for this class to display sources with. The name will be the title.

                          +
                          + +
                          +
                          +block()[source]
                          +

                          Update the window continuously while blocking the outer program.

                          +
                          + +
                          +
                          +end()[source]
                          +

                          Close all threads. Should be used with non-blocking mode.

                          +
                          + +
                          +
                          +handle_keys(key_input: int)[source]
                          +

                          Capture key input for the escape function and passing to key control subscriber threads.

                          +
                          + +
                          +
                          +handle_mouse(event, x, y, flags, param)[source]
                          +

                          Capture mouse input for mouse control subscriber threads.

                          +
                          + +
                          +
                          +loop()[source]
                          +

                          Continually update window frame. OpenCV only allows this in the main thread.

                          +
                          + +
                          +
                          +update(arr: numpy.ndarray = None, id: str = None)[source]
                          +

                          Update window frames once. Optionally add a new input and input id.

                          +
                          + +
                          +
                          +update_window_frames()[source]
                          +

                          Update the windows with the newest data for all frames.

                          +
                          + +
                          +
                          +wait_for_init()[source]
                          +

                          Update window frames in a loop until they’re actually updated. Useful for waiting for cameras to init.

                          +
                          + +
                          + +
                          +
                          + + +
                          + +
                          +
                          + +
                          +
                          + + + + + + + \ No newline at end of file diff --git a/docs/displayarray/index.html b/docs/displayarray/index.html index 05308a7..e2bc6a4 100644 --- a/docs/displayarray/index.html +++ b/docs/displayarray/index.html @@ -14,7 +14,6 @@ - @@ -40,7 +39,7 @@

                          Display

                          -display(*vids, callbacks: Union[Dict[Any, Callable[[numpy.ndarray], Optional[numpy.ndarray]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, window_names=None, blocking=False, fps_limit=240, size=(-1, -1))[source]
                          +display(*vids, callbacks: Union[Dict[Any, Union[Callable[[numpy.ndarray], Optional[numpy.ndarray]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, window_names=None, blocking=False, fps_limit=240, size=(-1, -1))[source]

                          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.

                          @@ -60,8 +59,8 @@

                          Frames

                          -read_updates(*vids, callbacks: Union[Dict[Any, Callable[[numpy.ndarray], Optional[numpy.ndarray]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, fps_limit=inf, size=(-1, -1), end_callback: Callable[[], bool] = <function <lambda>>, blocking=True)[source]
                          -

                          Reads back all updates from the requested videos.

                          +read_updates(*vids, callbacks: Union[Dict[Any, Union[Callable[[numpy.ndarray], Optional[numpy.ndarray]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, fps_limit=inf, size=(-1, -1), end_callback: Callable[[], bool] = <function <lambda>>, blocking=True)[source] +

                          Read back all updates from the requested videos.

                          Example usage: >>> from examples.videos import test_video >>> f = 0 @@ -69,6 +68,86 @@ … print(f”Frame:{f}. Array:{r}”)

                          +
                    +
                    +

                    Effects

                    +
                    +
                    +class Crop(output_size=(64, 64, 3), center=None)[source]
                    +

                    A callback class that will return the input array cropped to the output size. N-dimensional.

                    +
                    +
                    +property center
                    +

                    Get the center.

                    +
                    + +
                    +
                    +enable_mouse_control()[source]
                    +

                    Move the mouse to move where the crop is from on the original image.

                    +
                    + +
                    +
                    +property output_size
                    +

                    Get the output size.

                    +
                    + +
                    + +
                    +
                    +class Barrel(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                    +

                    A barrel lens distortion callback.

                    +
                    +
                    +property barrel_power
                    +

                    Guarded barrel power. Avoids divide by zero conditions.

                    +
                    + +
                    +
                    +property center
                    +

                    Guarded get center. Limits to within input.

                    +
                    + +
                    +
                    +enable_mouse_control(crop_size=None)[source]
                    +

                    Enable the default mouse controls.

                    +

                    Move the mouse to center the image +scroll to increase/decrease barrel +ctrl+scroll to increase/decrease zoom

                    +
                    + +
                    +
                    +property zoom
                    +

                    Guarded zoom. Avoids divide by zero conditions.

                    +
                    + +
                    + +
                    +
                    +class SelectChannels(selected_channels: Iterable[int] = None)[source]
                    +

                    Select channels to display from an array with too many colors.

                    +
                    +
                    Parameters
                    +

                    selected_channels – the list of channels to display.

                    +
                    +
                    +
                    +
                    +enable_mouse_control()[source]
                    +

                    Enable mouse control.

                    +

                    Alt+Scroll to increase/decrease channel 2. +Shift+Scroll to increase/decrease channel 1. +Ctrl+scroll to increase/decrease channel 0.

                    +
                    + +
                    +
                  @@ -94,9 +173,9 @@
                • displayarray
                • -
                • displayarray.effects.crop
                • displayarray.effects.lens
                • displayarray.effects.select_channels
                • displayarray.frame
                • @@ -113,7 +192,6 @@
                diff --git a/docs/displayarray_bash/index.html b/docs/displayarray_bash/index.html index de18b08..d8feef0 100644 --- a/docs/displayarray_bash/index.html +++ b/docs/displayarray_bash/index.html @@ -33,8 +33,8 @@

                displayarray cli

                -

                DisplayArray. -Display NumPy arrays.

                +

                DisplayArray.

                +

                Display NumPy arrays.

                Usage:

                displayarray (-w <webcam-number> | -v <video-filename> | -t <topic-name>[,dtype])… [-m <msg-backend>] displayarray -h @@ -69,6 +69,12 @@ Currently supported: ROS, ZeroMQ

                +
                +
                +main(argv=None)[source]
                +

                Process command line arguments.

                +
                +
                diff --git a/docs/docsrc/conf.py b/docs/docsrc/conf.py index e505c11..11561d4 100644 --- a/docs/docsrc/conf.py +++ b/docs/docsrc/conf.py @@ -10,16 +10,16 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath(f'..{os.sep}..')) # -- Project information ----------------------------------------------------- -project = 'DisplayArray' -copyright = '2019, Simulator Leek' -author = 'Simulator Leek' +project = "DisplayArray" +copyright = "2019, Simulator Leek" +author = "Simulator Leek" # -- General configuration --------------------------------------------------- @@ -27,15 +27,15 @@ author = 'Simulator Leek' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc'] +extensions = ["sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] add_module_names = False @@ -45,9 +45,9 @@ add_module_names = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ["_static"] diff --git a/docs/docsrc/crop.rst b/docs/docsrc/crop.rst deleted file mode 100644 index 3f5b9fd..0000000 --- a/docs/docsrc/crop.rst +++ /dev/null @@ -1,7 +0,0 @@ -displayarray.effects.crop -=================================== - -.. currentmodule:: displayarray.effects.crop - -.. autoclass:: Crop - :members: \ No newline at end of file diff --git a/docs/docsrc/display.rst b/docs/docsrc/display.rst new file mode 100644 index 0000000..1a324d2 --- /dev/null +++ b/docs/docsrc/display.rst @@ -0,0 +1,11 @@ +displayarray.display +=================================== +.. automodule:: displayarray + +.. autofunction:: display +.. autofunction:: breakpoint_display + +Windows +------- +.. autoclass:: displayarray.window.SubscriberWindows + :members: \ No newline at end of file diff --git a/docs/docsrc/effects.rst b/docs/docsrc/effects.rst new file mode 100644 index 0000000..4e81234 --- /dev/null +++ b/docs/docsrc/effects.rst @@ -0,0 +1,30 @@ +displayarray.effects +==================== + +Crop +---- +.. currentmodule:: displayarray.effects.crop + +.. autoclass:: Crop + :members: + +Lens +---- +.. currentmodule:: displayarray.effects.lens + +.. automodule:: displayarray.effects.lens + :members: + +.. autoclass:: Barrel + :members: + +.. autoclass:: Mustache + :members: + +Select Channels +--------------- +.. currentmodule:: displayarray.effects.select_channels + +.. autoclass:: SelectChannels + :members: + diff --git a/docs/docsrc/frame.rst b/docs/docsrc/frame.rst index 3d9e51e..8d8412f 100644 --- a/docs/docsrc/frame.rst +++ b/docs/docsrc/frame.rst @@ -1,6 +1,14 @@ displayarray.frame =================================== +Read Updates +------------ +.. currentmodule:: displayarray + +.. autofunction:: read_updates + +Frame Passing +------------- .. currentmodule:: displayarray.frame .. automodule:: displayarray.frame diff --git a/docs/docsrc/index.rst b/docs/docsrc/index.rst index 9bed90f..8b5c696 100644 --- a/docs/docsrc/index.rst +++ b/docs/docsrc/index.rst @@ -14,13 +14,10 @@ DisplayArray displays arrays. :maxdepth: 3 :caption: Python API - displayarray - displayarray.effects.crop - displayarray.effects.lens - displayarray.effects.select_channels - displayarray.frame - displayarray.window - displayarray.input + display + frame + effects + input .. toctree:: :maxdepth: 2 diff --git a/docs/docsrc/lens.rst b/docs/docsrc/lens.rst deleted file mode 100644 index e59939b..0000000 --- a/docs/docsrc/lens.rst +++ /dev/null @@ -1,13 +0,0 @@ -displayarray.effects.lens -============================ - -.. currentmodule:: displayarray.effects.lens - -.. automodule:: displayarray.effects.lens - :members: - -.. autoclass:: Barrel - :members: - -.. autoclass:: Mustache - :members: \ No newline at end of file diff --git a/docs/docsrc/select_channels.rst b/docs/docsrc/select_channels.rst deleted file mode 100644 index 4188e74..0000000 --- a/docs/docsrc/select_channels.rst +++ /dev/null @@ -1,7 +0,0 @@ -displayarray.effects.select_channels -====================================== - -.. currentmodule:: displayarray.effects.select_channels - -.. autoclass:: SelectChannels - :members: \ No newline at end of file diff --git a/docs/docsrc/window.rst b/docs/docsrc/window.rst deleted file mode 100644 index bcbfbf3..0000000 --- a/docs/docsrc/window.rst +++ /dev/null @@ -1,7 +0,0 @@ -displayarray.window -=================================== - -.. currentmodule:: displayarray.window - -.. autoclass:: SubscriberWindows - :members: diff --git a/docs/effects/index.html b/docs/effects/index.html new file mode 100644 index 0000000..9ba7bb3 --- /dev/null +++ b/docs/effects/index.html @@ -0,0 +1,288 @@ + + + + + + + displayarray.effects — DisplayArray documentation + + + + + + + + + + + + + + + + + + + + +
                +
                +
                + + +
                + +
                +

                displayarray.effects

                +
                +

                Crop

                +
                +
                +class Crop(output_size=(64, 64, 3), center=None)[source]
                +

                A callback class that will return the input array cropped to the output size. N-dimensional.

                +
                +
                +property center
                +

                Get the center.

                +
                + +
                +
                +enable_mouse_control()[source]
                +

                Move the mouse to move where the crop is from on the original image.

                +
                + +
                +
                +property output_size
                +

                Get the output size.

                +
                + +
                + +
                +
                +

                Lens

                +

                Create lens effects. Currently only 2D+color arrays are supported.

                +
                +
                +class Barrel(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A barrel lens distortion callback.

                +
                +
                +property barrel_power
                +

                Guarded barrel power. Avoids divide by zero conditions.

                +
                + +
                +
                +property center
                +

                Guarded get center. Limits to within input.

                +
                + +
                +
                +enable_mouse_control(crop_size=None)[source]
                +

                Enable the default mouse controls.

                +

                Move the mouse to center the image +scroll to increase/decrease barrel +ctrl+scroll to increase/decrease zoom

                +
                + +
                +
                +property zoom
                +

                Guarded zoom. Avoids divide by zero conditions.

                +
                + +
                + +
                +
                +class BarrelPyTorch(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A barrel distortion callback class accelerated by PyTorch.

                +
                + +
                +
                +class ControllableLens(use_bleed=False, zoom=1, center=None)[source]
                +

                A lens callback that can be controlled by the program or the user.

                +
                +
                +run_bleed(arr, x, y)[source]
                +

                Spread color outwards, like food coloring in water.

                +
                + +
                + +
                +
                +class Mustache(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A mustache distortion callback.

                +
                +
                +enable_mouse_control()[source]
                +

                Enable the default mouse loop to control the mustache distortion.

                +

                ctrl+scroll wheel: zoom in and out +shift+scroll wheel: adjust pincushion power +scroll wheel: adjust barrel power

                +
                + +
                + +
                +
                +class Barrel(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A barrel lens distortion callback.

                +
                +
                +property barrel_power
                +

                Guarded barrel power. Avoids divide by zero conditions.

                +
                + +
                +
                +property center
                +

                Guarded get center. Limits to within input.

                +
                + +
                +
                +enable_mouse_control(crop_size=None)[source]
                +

                Enable the default mouse controls.

                +

                Move the mouse to center the image +scroll to increase/decrease barrel +ctrl+scroll to increase/decrease zoom

                +
                + +
                +
                +property zoom
                +

                Guarded zoom. Avoids divide by zero conditions.

                +
                + +
                + +
                +
                +class Mustache(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A mustache distortion callback.

                +
                +
                +enable_mouse_control()[source]
                +

                Enable the default mouse loop to control the mustache distortion.

                +

                ctrl+scroll wheel: zoom in and out +shift+scroll wheel: adjust pincushion power +scroll wheel: adjust barrel power

                +
                + +
                + +
                +
                +

                Select Channels

                +
                +
                +class SelectChannels(selected_channels: Iterable[int] = None)[source]
                +

                Select channels to display from an array with too many colors.

                +
                +
                Parameters
                +

                selected_channels – the list of channels to display.

                +
                +
                +
                +
                +enable_mouse_control()[source]
                +

                Enable mouse control.

                +

                Alt+Scroll to increase/decrease channel 2. +Shift+Scroll to increase/decrease channel 1. +Ctrl+scroll to increase/decrease channel 0.

                +
                + +
                + +
                +
                + + +
                + +
                +
                + +
                +
                + + + + + + + \ No newline at end of file diff --git a/docs/frame/index.html b/docs/frame/index.html index 186f0cf..ea9a7b7 100644 --- a/docs/frame/index.html +++ b/docs/frame/index.html @@ -14,8 +14,8 @@ - - + + @@ -32,8 +32,26 @@
                -
                -

                displayarray.frame

                +
                +

                displayarray.frame

                +
                +

                Read Updates

                +
                +
                +read_updates(*vids, callbacks: Union[Dict[Any, Union[Callable[[numpy.ndarray], Optional[numpy.ndarray]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, fps_limit=inf, size=(-1, -1), end_callback: Callable[[], bool] = <function <lambda>>, blocking=True)[source]
                +

                Read back all updates from the requested videos.

                +

                Example usage:

                +
                >>> from examples.videos import test_video
                +>>> f = 0
                +>>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)):
                +...   print(f"Frame:{f}. Array:{r}")
                +
                +
                +
                + +
                +
                +

                Frame Passing

                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 @@ -66,13 +84,15 @@ np_cam simulates numpy arrays as OpenCV cameras

                -read_updates(*vids, callbacks: Union[Dict[Any, Callable[[numpy.ndarray], Optional[numpy.ndarray]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, fps_limit=inf, size=(-1, -1), end_callback: Callable[[], bool] = <function <lambda>>, blocking=True)[source]
                -

                Reads back all updates from the requested videos.

                -

                Example usage: ->>> from examples.videos import test_video ->>> f = 0 ->>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)): -… print(f”Frame:{f}. Array:{r}”)

                +read_updates(*vids, callbacks: Union[Dict[Any, Union[Callable[[numpy.ndarray], Optional[numpy.ndarray]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]]]], List[Callable[[numpy.ndarray], Optional[numpy.ndarray]]], Callable[[numpy.ndarray], Optional[numpy.ndarray]], None] = None, fps_limit=inf, size=(-1, -1), end_callback: Callable[[], bool] = <function <lambda>>, blocking=True)[source] +

                Read back all updates from the requested videos.

                +

                Example usage:

                +
                >>> from examples.videos import test_video
                +>>> f = 0
                +>>> for f, r in enumerate(read_updates(test_video, end_callback=lambda :f==2)):
                +...   print(f"Frame:{f}. Array:{r}")
                +
                +
                @@ -123,6 +143,7 @@ np_cam simulates numpy arrays as OpenCV cameras

                Run pub_cam_loop in a new thread. Starts on creation.

                +
                @@ -144,13 +165,14 @@ np_cam simulates numpy arrays as OpenCV cameras

                Navigation

                Python API

                Bash API

                diff --git a/docs/genindex/index.html b/docs/genindex/index.html index 979c7e6..288e791 100644 --- a/docs/genindex/index.html +++ b/docs/genindex/index.html @@ -48,23 +48,25 @@ | L | M | N + | O | P | R | S | U | W + | Z

                A

                @@ -72,11 +74,17 @@

                B

                @@ -84,7 +92,17 @@

                C

                +
                @@ -95,16 +113,16 @@
              • display() (FrameUpdater method)
              • -
              • displayarray (module) +
              • displayarray (module)
              @@ -190,13 +212,15 @@

              M

              @@ -209,6 +233,14 @@
            +

            O

            + + +
            +

            P

              @@ -222,7 +254,7 @@
            @@ -238,13 +272,13 @@

            S

            @@ -252,11 +286,11 @@

            U

            @@ -264,7 +298,15 @@

            W

            +
            + +

            Z

            + +
            @@ -289,13 +331,10 @@

            Navigation

            Python API

            Bash API

            @@ -79,13 +89,10 @@

            Navigation

            Python API

              -
            • displayarray
            • -
            • displayarray.effects.crop
            • -
            • displayarray.effects.lens
            • -
            • displayarray.effects.select_channels
            • -
            • displayarray.frame
            • -
            • displayarray.window
            • -
            • displayarray.input
                +
              • display
              • +
              • frame
              • +
              • effects
              • +
              • input @@ -100,7 +107,7 @@

                Related Topics

                diff --git a/docs/lens/index.html b/docs/lens/index.html index 9ba4162..88a8165 100644 --- a/docs/lens/index.html +++ b/docs/lens/index.html @@ -34,13 +34,54 @@

                displayarray.effects.lens

                +

                Create lens effects. Currently only 2D+color arrays are supported.

                class Barrel(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                -
                +

                A barrel lens distortion callback.

                +
                +
                +property barrel_power
                +

                Guarded barrel power. Avoids divide by zero conditions.

                +
                + +
                +
                +property center
                +

                Guarded get center. Limits to within input.

                +
                + +
                -enable_mouse_control()[source]
                -

                Move the mouse to center the image, scroll to increase/decrease barrel, ctrl+scroll to increase/decrease zoom

                +enable_mouse_control(crop_size=None)[source] +

                Enable the default mouse controls.

                +

                Move the mouse to center the image +scroll to increase/decrease barrel +ctrl+scroll to increase/decrease zoom

                +
                + +
                +
                +property zoom
                +

                Guarded zoom. Avoids divide by zero conditions.

                +
                + +
                + +
                +
                +class BarrelPyTorch(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A barrel distortion callback class accelerated by PyTorch.

                +
                + +
                +
                +class ControllableLens(use_bleed=False, zoom=1, center=None)[source]
                +

                A lens callback that can be controlled by the program or the user.

                +
                +
                +run_bleed(arr, x, y)[source]
                +

                Spread color outwards, like food coloring in water.

                @@ -48,7 +89,65 @@
                class Mustache(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                -
                +

                A mustache distortion callback.

                +
                +
                +enable_mouse_control()[source]
                +

                Enable the default mouse loop to control the mustache distortion.

                +

                ctrl+scroll wheel: zoom in and out +shift+scroll wheel: adjust pincushion power +scroll wheel: adjust barrel power

                +
                + +
                + +
                +
                +class Barrel(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A barrel lens distortion callback.

                +
                +
                +property barrel_power
                +

                Guarded barrel power. Avoids divide by zero conditions.

                +
                + +
                +
                +property center
                +

                Guarded get center. Limits to within input.

                +
                + +
                +
                +enable_mouse_control(crop_size=None)[source]
                +

                Enable the default mouse controls.

                +

                Move the mouse to center the image +scroll to increase/decrease barrel +ctrl+scroll to increase/decrease zoom

                +
                + +
                +
                +property zoom
                +

                Guarded zoom. Avoids divide by zero conditions.

                +
                + +
                + +
                +
                +class Mustache(use_bleed=False, barrel_power=1, pincushion_power=1, zoom=1, center=None)[source]
                +

                A mustache distortion callback.

                +
                +
                +enable_mouse_control()[source]
                +

                Enable the default mouse loop to control the mustache distortion.

                +

                ctrl+scroll wheel: zoom in and out +shift+scroll wheel: adjust pincushion power +scroll wheel: adjust barrel power

                +
                + +
                diff --git a/docs/objects.inv b/docs/objects.inv index 22362040bd209eab05b8e5b112f92da923575b28..d06227a145b1fa6b05f503f486255683b1da543d 100644 GIT binary patch delta 663 zcmV;I0%-lu1%?KYdw*9=kJ}&+z57=nwbzjLy0|L^PcKwk{h-cFH+jsIzp zOj2t-tW%DGVFt>92|EU^(0n2s6wM`&Qr8v;)5;kwXOrpLRkb5lJiHU8TE{;oLfu0S732Ofb+i|CV|6W~(EGCC6bquS7Nyr;n&AHYq01s3*a1?t7g z9Ru|3XbwmMwJ;fHCq?G>fiPMU!R?El_d2es^MG?jAAj6vXbT{9$o1HF3g!C|-nN(E z%U@rd(`*ObRm>3@(x0bsh=6!P-wyelqsyMfwM+JF(h;KjNcAZ=d9PVr`jSc8s+5H? xmKw*phA!)1=BI>#w^z-oSFF)C4i?c53$&dw&k(GmY>jqN_dpMj{Rc%+DA#w2N0k5o delta 607 zcmV-l0-*hd2G0eMdw*8Jj@uv*z56SW+G|Mny0M&~`TJsU zTI|FdoaAE5GxHw9FpM7;wvqHmjiE=>9G`2=TM6hB;)!GVA(|H-(V|Eln2?$ZMaU$S zl+;$!_|gE~H(Dr1dV9yTZ54AuEA%u<;|zZj2)0{b&SGYCgMU~4(ZQLd)_QDb%)ww# zw3I+<-C7__D`&I}WdK)cew|5zvV*d9hUrSBF#N^aK&EL&a1QEv;yGOF);h{6n3018 z>EDD^R4I@aZ^HMg|4fNSkrz0(hSo5sUa(A;FSzk*_x#y#>cFH6_TUUu+HwC1HUlno zY`0A8TeG5det)JR9Phx*T>=aHy#WP$Hqc+-TR;-1h1obeYiGU=gwdJ^Zbt*+eO%L) z0q3gI=CYw3fVAPPbJrBAX&qj7xAE(5P%b^~;G4wk;Yd#Xl!u6SJ{R-OX0A-wgHT)_ z@TM&-Gol3ZD;NfsJjTobW6HTd7b#21_JcOKjmqlQFn@ReQFPu(1d89u;HM?`bKQ4^ zeGF6RmOH7==G}aX*ntlr>5qzpyLfMv4pca@)rJTq+$2}00+4n5nQMj?y(&PH+G4PF ziW}UxQI0`mO$A2 - displayarray + displayarray @@ -58,7 +58,7 @@     - displayarray.effects.lens + displayarray.effects.lens @@ -86,13 +86,10 @@

                Navigation

                Python API

                Bash API

                  diff --git a/docs/search/index.html b/docs/search/index.html index 4ff96c6..2c9735c 100644 --- a/docs/search/index.html +++ b/docs/search/index.html @@ -77,13 +77,10 @@

                  Navigation

                  Python API

                  Bash API

                    diff --git a/docs/searchindex.js b/docs/searchindex.js index 93f7251..edd8c42 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["crop","displayarray","displayarray_bash","frame","index","input","lens","select_channels","window"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["crop.rst","displayarray.rst","displayarray_bash.rst","frame.rst","index.rst","input.rst","lens.rst","select_channels.rst","window.rst"],objects:{"":{displayarray:[1,0,0,"-"]},"displayarray.effects":{lens:[6,0,0,"-"]},"displayarray.effects.crop":{Crop:[0,2,1,""]},"displayarray.effects.crop.Crop":{enable_mouse_control:[0,3,1,""]},"displayarray.effects.lens":{Barrel:[6,2,1,""],Mustache:[6,2,1,""]},"displayarray.effects.lens.Barrel":{enable_mouse_control:[6,3,1,""]},"displayarray.effects.select_channels":{SelectChannels:[7,2,1,""]},"displayarray.frame":{FrameUpdater:[3,2,1,""],NpCam:[3,2,1,""],get_cam_ids:[3,1,1,""],pub_cam_thread:[3,1,1,""],read_updates:[3,1,1,""]},"displayarray.frame.FrameUpdater":{display:[3,3,1,""],loop:[3,3,1,""]},"displayarray.frame.NpCam":{get:[3,3,1,""],isOpened:[3,3,1,""],read:[3,3,1,""],release:[3,3,1,""],set:[3,3,1,""]},"displayarray.input":{MouseEvent:[5,2,1,""],key_loop:[5,2,1,""],mouse_loop:[5,2,1,""]},"displayarray.window":{SubscriberWindows:[8,2,1,""]},"displayarray.window.SubscriberWindows":{add_callback:[8,3,1,""],add_source:[8,3,1,""],add_window:[8,3,1,""],end:[8,3,1,""],handle_keys:[8,3,1,""],handle_mouse:[8,3,1,""],loop:[8,3,1,""],update:[8,3,1,""],update_window_frames:[8,3,1,""],wait_for_init:[8,3,1,""]},displayarray:{__main__:[2,0,0,"-"],breakpoint_display:[1,1,1,""],display:[1,1,1,""],frame:[3,0,0,"-"],read_updates:[1,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method"},terms:{"class":[0,3,5,6,7,8],"default":[2,3],"float":3,"function":[1,3,5,8],"import":[1,3],"int":[3,8],"new":[3,5,8],"return":0,"static":3,"true":[1,3],For:3,ROS:2,The:8,Use:2,Useful:8,actual:8,adapt:4,add:[3,8],add_callback:8,add_sourc:8,add_window:8,all:[1,3,5,8],allow:8,ani:[1,3,8],anoth:8,api:4,appli:8,arg:[1,3],arr:8,arrai:[0,1,2,3,4,8],back:[1,3],backend:2,barrel:6,barrel_pow:6,bash:4,becom:1,befor:[1,3],block:[1,3,8],bool:[1,3],breakpoint_displai:1,broker:2,call:3,callabl:[1,3,5,8],callback:[0,1,3,8],cam_id:3,camctrl:3,camera:[1,3,8],can:[1,3,4],cap_prop_frame_count:3,cap_prop_frame_height:3,cap_prop_frame_width:3,captur:8,center:[0,6],choos:2,chosen:2,close:[1,8],code:1,com:4,command:3,complet:4,contain:4,continu:[3,5,8],control:[3,8],creation:3,crop:4,ctrl:6,current:[2,3],data:[1,8],decreas:6,detect:3,dict:[1,3],dictionari:1,did:3,dimension:0,direct:4,displai:[2,3,4,8],display:3,dtype:2,effect:4,enable_mouse_control:[0,6],end:[1,3,8],end_callback:[1,3],enumer:[1,3],escap:8,event:[5,8],exampl:[1,3],execut:1,fake:3,fals:[1,5,6],file:[1,2,4],filenam:2,finish:3,fix:3,flag:[5,8],format:3,fps_limit:[1,3],frame:[2,4,8],frameupdat:3,from:[0,1,2,3],get:3,get_cam_id:3,github:4,github_url:4,global:3,hack:3,handl:3,handle_kei:8,handle_mous:8,help:2,high_spe:3,hold:5,http:4,ids:3,imag:[0,6],img:3,increas:6,index:4,inf:[1,3],inform:5,init:8,input:[0,4,8],isopen:3,iter:8,kei:[4,8],key_input:8,key_loop:5,know:3,kwarg:[1,3],lambda:[1,3],least:4,len:4,let:3,like:4,link:1,list:[1,3,8],listen:3,loop:[3,8],main:[3,8],messag:2,mode:8,modul:4,mous:[0,4,6,8],mouse_loop:5,mouseev:5,move:[0,6],msg:2,multipl:3,mustach:6,name:[1,2,8],ndarrai:[1,3,8],newest:8,non:8,none:[0,1,3,5,6,7,8],nov:4,np_cam:3,npcam:3,number:2,numpi:[1,2,3,8],onc:8,onli:[3,8],open:3,opencv:[3,5,8],oper:[1,3],option:[1,2,3,8],origin:0,output:0,output_s:0,outsid:3,own:1,page:4,param:[5,8],paramet:3,pass:[1,8],pincushion_pow:6,pleas:3,print:[1,3],pub_cam_loop:3,pub_cam_thread:3,publish:3,python:4,quickstart:4,read:[1,3],read_upd:[1,3],receiv:[3,5],releas:3,request:[1,3],request_:3,request_s:3,root:4,ros:2,run:[3,5],run_when_no_ev:5,scale:3,screen:3,scroll:6,search:4,select_channel:4,selectchannel:7,selected_channel:7,send:3,set:3,should:[4,8],show:2,simleek:4,simul:3,sinc:3,size:[0,1,3],someth:3,sourc:[0,1,3,5,6,7,8],specif:3,sphinx:4,standard:3,start:3,stop:1,str:[3,5,8],subscrib:8,subscriberwindow:8,support:2,tell:3,test_video:[1,3],text:2,thei:8,them:3,thi:[2,3,4,8],thread:[3,5,8],thu:4,titl:[1,8],toctre:4,topic:2,tupl:3,union:[1,3,8],until:[1,3,8],updat:[1,3,8],update_window_fram:8,usag:[1,2,3],use:3,use_ble:6,used:8,using:2,version:2,vid:[1,3],video:[1,2,3,8],video_sourc:[3,8],videohandlerthread:3,wait:8,wait_for_init:8,webcam:[1,2],where:0,window:[1,4,5],window_nam:[1,8],work:3,you:4,your:4,zeromq:2,zoom:6},titles:["displayarray.effects.crop","displayarray","displayarray cli","displayarray.frame","DisplayArray Documentation","displayarray.input","displayarray.effects.lens","displayarray.effects.select_channels","displayarray.window"],titleterms:{cli:2,crop:0,displai:1,displayarrai:[0,1,2,3,4,5,6,7,8],document:4,effect:[0,6,7],frame:[1,3],indic:4,input:5,kei:5,len:6,mous:5,select_channel:7,tabl:4,window:8}}) \ No newline at end of file +Search.setIndex({docnames:["display","displayarray_bash","effects","frame","index","input"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["display.rst","displayarray_bash.rst","effects.rst","frame.rst","index.rst","input.rst"],objects:{"":{displayarray:[0,0,0,"-"]},"displayarray.__main__":{main:[1,1,1,""]},"displayarray.effects":{lens:[2,0,0,"-"]},"displayarray.effects.crop":{Crop:[2,2,1,""]},"displayarray.effects.crop.Crop":{center:[2,3,1,""],enable_mouse_control:[2,3,1,""],output_size:[2,3,1,""]},"displayarray.effects.lens":{Barrel:[2,2,1,""],BarrelPyTorch:[2,2,1,""],ControllableLens:[2,2,1,""],Mustache:[2,2,1,""]},"displayarray.effects.lens.Barrel":{barrel_power:[2,3,1,""],center:[2,3,1,""],enable_mouse_control:[2,3,1,""],zoom:[2,3,1,""]},"displayarray.effects.lens.ControllableLens":{run_bleed:[2,3,1,""]},"displayarray.effects.lens.Mustache":{enable_mouse_control:[2,3,1,""]},"displayarray.effects.select_channels":{SelectChannels:[2,2,1,""]},"displayarray.effects.select_channels.SelectChannels":{enable_mouse_control:[2,3,1,""]},"displayarray.frame":{FrameUpdater:[3,2,1,""],NpCam:[3,2,1,""],get_cam_ids:[3,1,1,""],pub_cam_thread:[3,1,1,""],read_updates:[3,1,1,""]},"displayarray.frame.FrameUpdater":{display:[3,3,1,""],loop:[3,3,1,""]},"displayarray.frame.NpCam":{get:[3,3,1,""],isOpened:[3,3,1,""],read:[3,3,1,""],release:[3,3,1,""],set:[3,3,1,""]},"displayarray.input":{MouseEvent:[5,2,1,""],key_loop:[5,2,1,""],mouse_loop:[5,2,1,""]},"displayarray.window":{SubscriberWindows:[0,2,1,""]},"displayarray.window.SubscriberWindows":{add_callback:[0,3,1,""],add_source:[0,3,1,""],add_window:[0,3,1,""],block:[0,3,1,""],end:[0,3,1,""],handle_keys:[0,3,1,""],handle_mouse:[0,3,1,""],loop:[0,3,1,""],update:[0,3,1,""],update_window_frames:[0,3,1,""],wait_for_init:[0,3,1,""]},displayarray:{__main__:[1,0,0,"-"],breakpoint_display:[0,1,1,""],display:[0,1,1,""],frame:[3,0,0,"-"],read_updates:[3,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method"},terms:{"class":[0,2,3,5],"default":[1,2,3],"float":3,"function":[0,3,5],"import":3,"int":[0,2,3],"new":[0,3,5],"return":2,"static":3,"true":3,"while":0,For:3,ROS:1,The:0,Use:1,Useful:0,acceler:2,actual:0,adapt:4,add:[0,3],add_callback:0,add_sourc:0,add_window:0,adjust:2,alia:[],all:[0,3,5],allow:0,alt:2,ani:[0,3],anoth:0,api:4,appli:0,arg:[0,3],argument:1,argv:1,arr:[0,2],arrai:[0,1,2,3,4],avoid:2,back:3,backend:1,barrel:2,barrel_pow:2,barrelpytorch:2,bash:4,becom:0,befor:[0,3],block:[0,3],bool:3,breakpoint_displai:0,broker:1,call:3,callabl:[0,3,5],callback:[0,2,3],cam_id:3,camctrl:3,camera:[0,3],can:[0,2,3,4],cap_prop_frame_count:3,cap_prop_frame_height:3,cap_prop_frame_width:3,captur:0,center:2,channel:4,choos:1,chosen:1,close:0,code:0,color:2,com:4,command:[1,3],complet:4,condit:2,contain:4,continu:[0,3,5],control:[0,2,3],controllablelen:2,creat:2,creation:3,crop:4,crop_siz:2,ctrl:2,current:[1,2,3],data:0,decreas:2,def:5,detect:3,dict:[0,3],dictionari:0,did:3,dimension:2,direct:4,displai:[1,2,3,4],display:3,distort:2,divid:2,doctest:[],dtype:1,effect:4,enabl:2,enable_mouse_control:2,end:[0,3],end_callback:3,enumer:3,escap:0,event:[0,5],exampl:3,execut:0,eyebal:[],fake:3,fals:[0,2],file:[0,1,4],filenam:1,finish:3,fix:3,flag:[0,5],food:2,format:[3,5],fps_limit:[0,3],frame:[0,1,4],frameupdat:3,from:[1,2,3],fun:5,get:[2,3],get_cam_id:3,github:4,github_url:4,global:3,guard:2,hack:3,handl:3,handle_kei:0,handle_mous:0,help:1,high_spe:3,hold:5,http:4,ids:3,imag:2,img:3,increas:2,index:4,inf:3,inform:5,init:0,input:[0,2,4],isopen:3,iter:[0,2],kei:[0,4],key_input:0,key_loop:5,know:3,kwarg:[0,3],lambda:3,least:4,len:4,let:3,like:[2,4],limit:2,line:1,link:0,list:[0,2,3],listen:3,loop:[0,2,3],main:[0,1,3],mani:2,messag:1,mode:0,modul:4,mous:[0,2,4],mouse_ev:5,mouse_loop:5,mouseev:5,move:2,msg:1,multipl:3,mustach:2,name:[0,1],ndarrai:[0,3],newest:0,non:0,none:[0,1,2,3,5],nov:4,np_cam:3,npcam:3,number:1,numpi:[0,1,3],onc:0,onli:[0,2,3],open:3,opencv:[0,3,5],oper:[0,3],option:[0,1,3],origin:2,out:2,outer:0,output:2,output_s:2,outsid:3,outward:2,own:0,page:4,param:[0,5],paramet:[2,3],pass:[0,4],pincushion:2,pincushion_pow:2,pleas:3,power:2,press:5,print:[3,5],process:1,program:[0,2],properti:2,pub_cam_loop:3,pub_cam_thread:3,publish:3,python:4,pytorch:2,quickstart:4,read:4,read_upd:3,receiv:[3,5],reduc:[],releas:3,request:3,request_:3,request_s:3,root:4,ros:1,run:[3,5],run_ble:2,scale:3,screen:3,scroll:2,search:4,see:[],select:4,select_channel:[],selectchannel:2,selected_channel:2,send:3,set:3,shift:2,should:[0,4],show:1,simleek:4,simul:3,sinc:3,size:[0,2,3],someth:3,sourc:[0,1,2,3,5],specif:3,sphinx:4,spread:2,standard:3,start:3,stop:0,str:[0,3,5],subscrib:0,subscriberwindow:0,support:[1,2],tell:3,test_video:3,text:1,thei:0,them:3,thi:[0,1,3,4],thread:[0,3,5],three:[],thu:4,titl:0,toctre:4,too:2,topic:1,tupl:3,union:[0,3],until:[0,3],updat:[0,4],update_window_fram:0,usag:[1,3],use:3,use_ble:2,used:0,user:2,using:1,version:1,vid:[0,3],video:[0,1,3],video_sourc:[0,3],videohandlerthread:3,wait:0,wait_for_init:0,water:2,webcam:[0,1],wheel:2,where:2,window:[4,5],window_nam:0,within:2,work:3,you:4,your:4,zero:2,zeromq:1,zoom:2},titles:["displayarray.display","displayarray cli","displayarray.effects","displayarray.frame","DisplayArray Documentation","displayarray.input"],titleterms:{channel:2,cli:1,crop:2,displai:0,displayarrai:[0,1,2,3,4,5],document:4,effect:2,frame:3,indic:4,input:5,kei:5,len:2,mous:5,pass:3,read:3,select:2,select_channel:[],tabl:4,updat:3,window:0}}) \ No newline at end of file diff --git a/docs/select_channels/index.html b/docs/select_channels/index.html index c3e57c9..11367ed 100644 --- a/docs/select_channels/index.html +++ b/docs/select_channels/index.html @@ -36,8 +36,23 @@

                    displayarray.effects.select_channels

                    -class SelectChannels(selected_channels=None)[source]
                    -
                    +class SelectChannels(selected_channels: Iterable[int] = None)[source] +

                    Select channels to display from an array with too many colors.

                    +
                    +
                    Parameters
                    +

                    selected_channels – the list of channels to display.

                    +
                    +
                    +
                    +
                    +enable_mouse_control()[source]
                    +

                    Enable mouse control.

                    +

                    Alt+Scroll to increase/decrease channel 2. +Shift+Scroll to increase/decrease channel 1. +Ctrl+scroll to increase/decrease channel 0.

                    +
                    + +
            diff --git a/docs/window/index.html b/docs/window/index.html index 293643d..fcede89 100644 --- a/docs/window/index.html +++ b/docs/window/index.html @@ -56,6 +56,12 @@

            Add another window for this class to display sources with. The name will be the title.

            +
            +
            +block()[source]
            +

            Update the window continuously while blocking the outer program.

            +
            +
            end()[source]
            @@ -64,7 +70,7 @@
            -handle_keys(key_input)[source]
            +handle_keys(key_input: int)[source]

            Capture key input for the escape function and passing to key control subscriber threads.

            diff --git a/examples/documentation/fractal.py b/examples/documentation/fractal.py index e61a200..9c15b84 100644 --- a/examples/documentation/fractal.py +++ b/examples/documentation/fractal.py @@ -10,17 +10,19 @@ def mandel( source: https://thesamovar.wordpress.com/2009/03/22/fast-fractals-with-python-and-numpy/ - >>> img = mandel() - >>> center = (0, -1.78) - >>> length = 3.2 - >>> d = display(img) - >>> while d: - ... length*=.9 - ... y_min = center[1]-length/2.0 - ... y_max = center[1]+length/2.0 - ... x_min = center[0]-length/2.0 - ... x_max = center[0]+length/2.0 - ... img[...] = mandel(y_min=y_min, y_max=y_max, x_min=x_min, x_max=x_max) + .. code-block:: python + + >>> img = mandel() + >>> center = (0, -1.78) + >>> length = 3.2 + >>> d = display(img) + >>> while d: + ... length*=.9 + ... y_min = center[1]-length/2.0 + ... y_max = center[1]+length/2.0 + ... x_min = center[0]-length/2.0 + ... x_max = center[0]+length/2.0 + ... img[...] = mandel(y_min=y_min, y_max=y_max, x_min=x_min, x_max=x_max) """ ix, iy = np.mgrid[0:height, 0:width] diff --git a/examples/effects/lens_crop.py b/examples/effects/lens_crop.py index 7db1f2e..556adfc 100644 --- a/examples/effects/lens_crop.py +++ b/examples/effects/lens_crop.py @@ -6,10 +6,11 @@ from examples.videos import test_video d = ( display(test_video) - .add_callback(crop.Crop()) - .add_callback(lens.Barrel().enable_mouse_control()) + .add_callback(lens.BarrelPyTorch().enable_mouse_control(crop_size=(256, 256))) + .add_callback(crop.Crop(output_size=(256, 256, 3))) .wait_for_init() ) while d: - print(d.frames[0].shape) + if len(d.frames) > 0: + pass diff --git a/examples/effects/manual_control.py b/examples/effects/manual_control.py new file mode 100644 index 0000000..758ce86 --- /dev/null +++ b/examples/effects/manual_control.py @@ -0,0 +1,36 @@ +from displayarray.effects import crop, lens +from displayarray import display +from examples.videos import test_video +import math as m + +# Move the mouse to center the image, scroll to increase/decrease barrel, ctrl+scroll to increase/decrease zoom + +pre_crop_callback = crop.Crop(output_size=(480, 640, 3)).enable_mouse_control() +lens_callback = lens.BarrelPyTorch() +post_crop_callback = crop.Crop(output_size=(256, 256, 3)).enable_mouse_control() + +d = ( + display(0, size=(99999, 99999)) + .add_callback(pre_crop_callback) + .add_callback(lens_callback) + .add_callback(post_crop_callback) + .wait_for_init() +) + +i = 0 +while d: + if len(d.frames) > 0: + i += 1 + frame = d.frames[0] + center_sin = [(m.sin(m.pi * (i / 70.0))), (m.cos(m.pi * (i / 120.0)))] + pre_crop_callback.center = [ + center_sin[0] * 720 / 2 + 720 / 2, + center_sin[1] * 1280 / 2 + 1280 / 2, + ] + lens_callback.center = [ + center_sin[0] * 480 / 2 + 480 / 2, + center_sin[1] * 640 / 2 + 640 / 2, + ] + post_crop_callback.center = [480 / 2, 640 / 2] + lens_callback.zoom = m.sin(m.pi * ((i + 25) / 50.0)) + 1.01 + lens_callback.barrel_power = m.sin((m.pi * (i + 33) / 25)) + 1.5 diff --git a/pyproject.toml b/pyproject.toml index db49003..f32c23d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'displayarray' -version = '0.7.1' +version = '0.7.2' description = 'Tool for displaying numpy arrays.' authors = ['SimLeek '] license = 'MIT' diff --git a/tests/effects/test_crop.py b/tests/effects/test_crop.py index 108dcde..b3529ee 100644 --- a/tests/effects/test_crop.py +++ b/tests/effects/test_crop.py @@ -8,7 +8,7 @@ def test_init_defaults(): c = crop.Crop() assert np.all(c.output_size == (64, 64, 3)) - assert c.center is None + assert all(c.center == [32, 32, 1]) assert c.odd_center is None assert c.input_size is None @@ -16,7 +16,7 @@ def test_init_defaults(): def test_init(): c = crop.Crop((32, 32, 3), (16, 16, 1)) - c(np.ndarray((64,64,3))) + c(np.ndarray((64, 64, 3))) assert np.all(c.output_size == (32, 32, 3)) assert np.all(c.center == (16, 16, 1))