|
17 | 17 | It should be launched in a separate process to allow Vector to run freely while |
18 | 18 | the viewer is rendering. |
19 | 19 |
|
20 | | -It uses python-opencv, an image processing library which is available on most |
21 | | -platforms. It also depends on the Pillow library for image processing. |
| 20 | +It uses Tkinter, a standard Python GUI package. |
| 21 | +It also depends on the Pillow library for image processing. |
22 | 22 | """ |
23 | 23 |
|
24 | 24 | import multiprocessing as mp |
25 | | -import os |
26 | 25 | import sys |
| 26 | +import tkinter as tk |
27 | 27 |
|
28 | 28 | try: |
29 | | - import numpy as np |
30 | | -except ImportError as exc: |
31 | | - sys.exit("Cannot import numpy: Do `pip3 install numpy` to install") |
| 29 | + from PIL import ImageTk |
| 30 | +except ImportError: |
| 31 | + sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install") |
32 | 32 |
|
33 | | -try: |
34 | | - import cv2 |
35 | | -except ImportError as exc: |
36 | | - sys.exit("Cannot import opencv-python: Do `pip3 install opencv-python` to install") |
37 | 33 |
|
| 34 | +class TkCameraViewer: # pylint: disable=too-few-public-methods |
| 35 | + """A Tkinter based camera video feed. |
| 36 | +
|
| 37 | + :param queue: A queue to send frames between the user's main thread and the viewer process. |
| 38 | + :param event: An event to signal that the viewer process has closed. |
| 39 | + :param overlays: Overlays to be drawn on the images of the renderer. |
| 40 | + :param timeout: The time without a new frame before the process will exit. |
| 41 | + :param force_on_top: Specifies whether the window should be forced on top of all others. |
| 42 | + """ |
| 43 | + |
| 44 | + def __init__(self, queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = False): |
| 45 | + self.tk_root = tk.Tk() |
| 46 | + self.width = 640 |
| 47 | + self.height = 360 |
| 48 | + self.queue = queue |
| 49 | + self.event = event |
| 50 | + self.overlays = overlays |
| 51 | + self.timeout = timeout |
| 52 | + self.tk_root.title("Vector Camera Feed") |
| 53 | + self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window) |
| 54 | + self.tk_root.bind("<Configure>", self._resize_window) |
| 55 | + if force_on_top: |
| 56 | + self.tk_root.wm_attributes("-topmost", 1) |
| 57 | + self.label = tk.Label(self.tk_root, borderwidth=0) |
| 58 | + self.label.pack(fill=tk.BOTH, expand=True) |
| 59 | + |
| 60 | + def _delete_window(self) -> None: |
| 61 | + """Handle window close event.""" |
| 62 | + self.event.set() |
| 63 | + self.tk_root.destroy() |
| 64 | + |
| 65 | + def _resize_window(self, evt: tk.Event) -> None: |
| 66 | + """Handle window resize event. |
| 67 | +
|
| 68 | + :param evt: A Tkinter window event (keyboard, mouse events, etc). |
| 69 | + """ |
| 70 | + self.width = evt.width |
| 71 | + self.height = evt.height |
38 | 72 |
|
39 | | -def main(queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0) -> None: |
| 73 | + def draw_frame(self) -> None: |
| 74 | + """Display an image on to a Tkinter label widget.""" |
| 75 | + image = self.queue.get(True, timeout=self.timeout) |
| 76 | + while image: |
| 77 | + if self.event.is_set(): |
| 78 | + break |
| 79 | + if self.overlays: |
| 80 | + for overlay in self.overlays: |
| 81 | + overlay.apply_overlay(image) |
| 82 | + if (self.width, self.height) != image.size: |
| 83 | + image = image.resize((self.width, self.height)) |
| 84 | + tk_image = ImageTk.PhotoImage(image) |
| 85 | + self.label.config(image=tk_image) |
| 86 | + self.label.image = tk_image |
| 87 | + self.tk_root.update_idletasks() |
| 88 | + self.tk_root.update() |
| 89 | + image = self.queue.get(True, timeout=self.timeout) |
| 90 | + |
| 91 | + |
| 92 | +def main(queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = False) -> None: |
40 | 93 | """Rendering the frames in another process. This allows the UI to have the |
41 | 94 | main thread of its process while the user code continues to execute. |
42 | 95 |
|
43 | 96 | :param queue: A queue to send frames between the user's main thread and the viewer process. |
44 | 97 | :param event: An event to signal that the viewer process has closed. |
45 | | - :param overlays: overlays to be drawn on the images of the renderer. |
| 98 | + :param overlays: Overlays to be drawn on the images of the renderer. |
46 | 99 | :param timeout: The time without a new frame before the process will exit. |
| 100 | + :param force_on_top: Specifies whether the window should be forced on top of all others. |
47 | 101 | """ |
48 | | - is_windows = os.name == 'nt' |
49 | | - window_name = "Vector Camera Feed" |
50 | | - cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) |
| 102 | + |
51 | 103 | try: |
52 | | - image = queue.get(True, timeout=timeout) |
53 | | - while image: |
54 | | - if event.is_set(): |
55 | | - break |
56 | | - if overlays: |
57 | | - for overlay in overlays: |
58 | | - overlay.apply_overlay(image) |
59 | | - image = cv2.cvtColor(np.array(image), cv2.COLOR_BGR2RGB) |
60 | | - cv2.imshow(window_name, np.array(image)) |
61 | | - cv2.waitKey(1) |
62 | | - if not is_windows and cv2.getWindowProperty(window_name, cv2.WND_PROP_VISIBLE) < 1: |
63 | | - break |
64 | | - image = queue.get(True, timeout=timeout) |
| 104 | + tk_viewer = TkCameraViewer(queue, event, overlays, timeout, force_on_top) |
| 105 | + tk_viewer.draw_frame() |
65 | 106 | except TimeoutError: |
66 | 107 | pass |
67 | 108 | except KeyboardInterrupt: |
68 | 109 | pass |
69 | 110 | finally: |
70 | 111 | event.set() |
71 | | - cv2.destroyWindow(window_name) |
72 | | - cv2.waitKey(1) |
73 | 112 |
|
74 | 113 |
|
75 | | -__all__ = ['main'] |
| 114 | +__all__ = ['TkCameraViewer', 'main'] |
0 commit comments