2019-04-28 11:16:27 +02:00

166 lines
5.4 KiB
Python

# Copyright (c) 2016-2017 Anki, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the file LICENSE.txt or at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''This module provides a simple GUI viewer for Cozmo's camera.
It uses Tkinter, the standard Python GUI toolkit which is optionally available
on most platforms, and also depends on the Pillow and numpy libraries for
image processing.
See the online SDK documentation for details on how to install these extra
packages on your platform.
The easiest way to make use of this viewer is to call
:func:`cozmo.run.connect_with_tkviewer`.
Warning:
This package requires Python to have Tkinter installed to display the GUI.
'''
# __all__ should order by constants, event classes, other classes, functions.
__all__ = ['TkImageViewer']
import cozmo
import collections
import functools
import queue
import platform
import time
from PIL import Image, ImageDraw, ImageTk
import tkinter
from . import world
class TkThreadable:
'''A mixin for adding threadsafe calls to tkinter methods.'''
#pylint: disable=no-member
# no-member errors are raised in pylint regarding members/methods called but not defined in our mixin.
def __init__(self, *a, **kw):
self._thread_queue = queue.Queue()
self.after(50, self._thread_call_dispatch)
def call_threadsafe(self, method, *a, **kw):
self._thread_queue.put((method, a, kw))
def _thread_call_dispatch(self):
while True:
try:
method, a, kw = self._thread_queue.get(block=False)
self.after_idle(method, *a, **kw)
except queue.Empty:
break
self.after(50, self._thread_call_dispatch)
class TkImageViewer(tkinter.Frame, TkThreadable):
'''Simple Tkinter camera viewer.'''
# TODO: rewrite this whole thing. Make a generic camera widget
# that can be used in other Tk applications. Also handle resizing
# the window properly.
def __init__(self,
tk_root=None, refresh_interval=10, image_scale = 2,
window_name = "CozmoView", force_on_top=True):
if tk_root is None:
tk_root = tkinter.Tk()
tkinter.Frame.__init__(self, tk_root)
TkThreadable.__init__(self)
self._img_queue = collections.deque(maxlen=1)
self._refresh_interval = refresh_interval
self.scale = image_scale
self.width = None
self.height = None
self.tk_root = tk_root
tk_root.wm_title(window_name)
# Tell the TK root not to resize based on the contents of the window.
# Necessary to get the resizing to function properly
tk_root.pack_propagate(False)
# Set the starting window size
tk_root.geometry('{}x{}'.format(720, 540))
self.label = tkinter.Label(self.tk_root,image=None)
self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window)
self._isRunning = True
self.robot = None
self.handler = None
self._first_image = True
tk_root.aspect(4,3,4,3)
if force_on_top:
# force window on top of all others, regardless of focus
tk_root.wm_attributes("-topmost", 1)
self.tk_root.bind("<Configure>", self.configure)
self._repeat_draw_frame()
async def connect(self, coz_conn):
self.robot = await coz_conn.wait_for_robot()
self.robot.camera.image_stream_enabled = True
self.handler = self.robot.world.add_event_handler(
world.EvtNewCameraImage, self.image_event)
def disconnect(self):
if self.handler:
self.handler.disable()
self.call_threadsafe(self.quit)
# The base class configure doesn't take an event
#pylint: disable=arguments-differ
def configure(self, event):
if event.width < 50 or event.height < 50:
return
self.height = event.height
self.width = event.width
def image_event(self, evt, *, image, **kw):
if self._first_image or self.width is None:
img = image.annotate_image(scale=self.scale)
else:
img = image.annotate_image(fit_size=(self.width, self.height))
self._img_queue.append(img)
def _delete_window(self):
self.tk_root.destroy()
self.quit()
self._isRunning = False
def _draw_frame(self):
if ImageTk is None:
return
try:
image = self._img_queue.popleft()
except IndexError:
# no new image
return
self._first_image = False
photoImage = ImageTk.PhotoImage(image)
self.label.configure(image=photoImage)
self.label.image = photoImage
# Dynamically expand the image to fit the window. And fill in both X and Y directions.
self.label.pack(fill=tkinter.BOTH, expand=True)
def _repeat_draw_frame(self, event=None):
self._draw_frame()
self.after(self._refresh_interval, self._repeat_draw_frame)