# 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("", 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)