123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- # 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)
|