You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

tkview.py 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # Copyright (c) 2016-2017 Anki, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License in the file LICENSE.txt or at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. '''This module provides a simple GUI viewer for Cozmo's camera.
  15. It uses Tkinter, the standard Python GUI toolkit which is optionally available
  16. on most platforms, and also depends on the Pillow and numpy libraries for
  17. image processing.
  18. See the online SDK documentation for details on how to install these extra
  19. packages on your platform.
  20. The easiest way to make use of this viewer is to call
  21. :func:`cozmo.run.connect_with_tkviewer`.
  22. Warning:
  23. This package requires Python to have Tkinter installed to display the GUI.
  24. '''
  25. # __all__ should order by constants, event classes, other classes, functions.
  26. __all__ = ['TkImageViewer']
  27. import cozmo
  28. import collections
  29. import functools
  30. import queue
  31. import platform
  32. import time
  33. from PIL import Image, ImageDraw, ImageTk
  34. import tkinter
  35. from . import world
  36. class TkThreadable:
  37. '''A mixin for adding threadsafe calls to tkinter methods.'''
  38. #pylint: disable=no-member
  39. # no-member errors are raised in pylint regarding members/methods called but not defined in our mixin.
  40. def __init__(self, *a, **kw):
  41. self._thread_queue = queue.Queue()
  42. self.after(50, self._thread_call_dispatch)
  43. def call_threadsafe(self, method, *a, **kw):
  44. self._thread_queue.put((method, a, kw))
  45. def _thread_call_dispatch(self):
  46. while True:
  47. try:
  48. method, a, kw = self._thread_queue.get(block=False)
  49. self.after_idle(method, *a, **kw)
  50. except queue.Empty:
  51. break
  52. self.after(50, self._thread_call_dispatch)
  53. class TkImageViewer(tkinter.Frame, TkThreadable):
  54. '''Simple Tkinter camera viewer.'''
  55. # TODO: rewrite this whole thing. Make a generic camera widget
  56. # that can be used in other Tk applications. Also handle resizing
  57. # the window properly.
  58. def __init__(self,
  59. tk_root=None, refresh_interval=10, image_scale = 2,
  60. window_name = "CozmoView", force_on_top=True):
  61. if tk_root is None:
  62. tk_root = tkinter.Tk()
  63. tkinter.Frame.__init__(self, tk_root)
  64. TkThreadable.__init__(self)
  65. self._img_queue = collections.deque(maxlen=1)
  66. self._refresh_interval = refresh_interval
  67. self.scale = image_scale
  68. self.width = None
  69. self.height = None
  70. self.tk_root = tk_root
  71. tk_root.wm_title(window_name)
  72. # Tell the TK root not to resize based on the contents of the window.
  73. # Necessary to get the resizing to function properly
  74. tk_root.pack_propagate(False)
  75. # Set the starting window size
  76. tk_root.geometry('{}x{}'.format(720, 540))
  77. self.label = tkinter.Label(self.tk_root,image=None)
  78. self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window)
  79. self._isRunning = True
  80. self.robot = None
  81. self.handler = None
  82. self._first_image = True
  83. tk_root.aspect(4,3,4,3)
  84. if force_on_top:
  85. # force window on top of all others, regardless of focus
  86. tk_root.wm_attributes("-topmost", 1)
  87. self.tk_root.bind("<Configure>", self.configure)
  88. self._repeat_draw_frame()
  89. async def connect(self, coz_conn):
  90. self.robot = await coz_conn.wait_for_robot()
  91. self.robot.camera.image_stream_enabled = True
  92. self.handler = self.robot.world.add_event_handler(
  93. world.EvtNewCameraImage, self.image_event)
  94. def disconnect(self):
  95. if self.handler:
  96. self.handler.disable()
  97. self.call_threadsafe(self.quit)
  98. # The base class configure doesn't take an event
  99. #pylint: disable=arguments-differ
  100. def configure(self, event):
  101. if event.width < 50 or event.height < 50:
  102. return
  103. self.height = event.height
  104. self.width = event.width
  105. def image_event(self, evt, *, image, **kw):
  106. if self._first_image or self.width is None:
  107. img = image.annotate_image(scale=self.scale)
  108. else:
  109. img = image.annotate_image(fit_size=(self.width, self.height))
  110. self._img_queue.append(img)
  111. def _delete_window(self):
  112. self.tk_root.destroy()
  113. self.quit()
  114. self._isRunning = False
  115. def _draw_frame(self):
  116. if ImageTk is None:
  117. return
  118. try:
  119. image = self._img_queue.popleft()
  120. except IndexError:
  121. # no new image
  122. return
  123. self._first_image = False
  124. photoImage = ImageTk.PhotoImage(image)
  125. self.label.configure(image=photoImage)
  126. self.label.image = photoImage
  127. # Dynamically expand the image to fit the window. And fill in both X and Y directions.
  128. self.label.pack(fill=tkinter.BOTH, expand=True)
  129. def _repeat_draw_frame(self, event=None):
  130. self._draw_frame()
  131. self.after(self._refresh_interval, self._repeat_draw_frame)