Skip to content

Commit a064083

Browse files
Rakesh Ravi ShankarMichelle Sintov
authored andcommitted
VIC-12912: Switch camera feed renderer from OpenCV to Tkinter (#125)
* Move from opencv to tkinter * Removed fps calculation * Update package imports * Handled image resizing * Handle window close events * Fixed a close event bug * Minor refactor * pylint update, removed unused library import * Added option to force window to top, doc updates
1 parent e84f07f commit a064083

4 files changed

Lines changed: 80 additions & 36 deletions

File tree

anki_vector/camera_viewer/__init__.py

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,59 +17,98 @@
1717
It should be launched in a separate process to allow Vector to run freely while
1818
the viewer is rendering.
1919
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.
2222
"""
2323

2424
import multiprocessing as mp
25-
import os
2625
import sys
26+
import tkinter as tk
2727

2828
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")
3232

33-
try:
34-
import cv2
35-
except ImportError as exc:
36-
sys.exit("Cannot import opencv-python: Do `pip3 install opencv-python` to install")
3733

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
3872

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:
4093
"""Rendering the frames in another process. This allows the UI to have the
4194
main thread of its process while the user code continues to execute.
4295
4396
:param queue: A queue to send frames between the user's main thread and the viewer process.
4497
: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.
4699
: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.
47101
"""
48-
is_windows = os.name == 'nt'
49-
window_name = "Vector Camera Feed"
50-
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
102+
51103
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()
65106
except TimeoutError:
66107
pass
67108
except KeyboardInterrupt:
68109
pass
69110
finally:
70111
event.set()
71-
cv2.destroyWindow(window_name)
72-
cv2.waitKey(1)
73112

74113

75-
__all__ = ['main']
114+
__all__ = ['TkCameraViewer', 'main']

anki_vector/viewer.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(self, robot):
5757
self._frame_queue: mp.Queue = None
5858
self._process = None
5959

60-
def show(self, timeout: float = 10.0) -> None:
60+
def show(self, timeout: float = 10.0, force_on_top: bool = False) -> None:
6161
"""Render a video stream using the images obtained from
6262
Vector's camera feed.
6363
@@ -71,7 +71,8 @@ def show(self, timeout: float = 10.0) -> None:
7171
time.sleep(10)
7272
7373
:param timeout: Render video for the given time. (Renders forever, if timeout not given.)
74-
"""
74+
:param force_on_top: Specifies whether the window should be forced on top of all others.
75+
"""
7576
from . import camera_viewer
7677

7778
self.robot.camera.init_camera_feed()
@@ -83,7 +84,8 @@ def show(self, timeout: float = 10.0) -> None:
8384
args=(self._frame_queue,
8485
self._close_event,
8586
self.overlays,
86-
timeout),
87+
timeout,
88+
force_on_top),
8789
daemon=True,
8890
name="Camera Viewer Process")
8991
self._process.start()

docs/source/install-linux.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ Python Installation
4242

4343
sudo apt install python3-pip
4444

45+
3. Last, install Tkinter::
46+
47+
sudo apt-get install python3-pil.imagetk
48+
4549
""""""""""""""""
4650
SDK Installation
4751
""""""""""""""""
@@ -98,7 +102,7 @@ Python and Module Installation
98102

99103
3. Install the following additional packages::
100104

101-
sudo apt-get install build-essential libssl-dev libffi-dev python3.6-dev
105+
sudo apt-get install build-essential libssl-dev libffi-dev python3.6-dev python3-pil.imagetk
102106

103107

104108
""""""""""""""""

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@ cryptography
33
flask
44
googleapis-common-protos
55
numpy>=1.11
6-
opencv-python>=3.4
76
Pillow>=3.3
87
requests

0 commit comments

Comments
 (0)