UAV Coverage Path Planner
specify_rect.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # Copyright (c) 2017 Takaki Ueno
4 # Released under the MIT license
5 
6 """! @package cpp_uav
7 This module offers GUI to specify a polygon for coverage path planning.
8 """
9 
10 # Import python3's print to suppress warning raised by pylint
11 from __future__ import print_function
12 
13 # python libraries
14 from math import tan
15 
16 import numpy as np
17 
18 # rospy
19 import rospy
20 
21 # messages
22 from geometry_msgs.msg import Point
23 from std_msgs.msg import Float64
24 
25 # service
26 from cpp_uav.srv import Torres16
27 
28 # Check maplotlib's version
29 # Exit if older than 2.1.0
30 import matplotlib
31 if matplotlib.__version__ >= "2.1.0":
32  # matplotlib
33  # matplotlib 2.1.0 or newer is required to import TextBox
34  from matplotlib import pyplot as plt
35  from matplotlib.patches import Polygon
36  from matplotlib.widgets import Button
37  from matplotlib.widgets import TextBox
38 else:
39  print("Matplotlib 2.1.0 or newer is required to run this node.")
40  print("Please update or install Matplotlib.")
41  exit(1)
42 
43 
44 class PolygonBuilder(object):
45  """!
46  GUI class to specify a polygon
47  """
48 
49  def __init__(self):
50  """! Constructor
51  """
52 
53  # @var fig
54  # Figure instance
55  self.fig = plt.figure(figsize=(10, 8))
56 
57  # @var axis
58  # Axis object where polygon is shown
59  self.axis = self.fig.add_subplot(111)
60 
61  # adjust the size of graph
62  self.axis.set_ylim([-100, 100])
63  self.axis.set_xlim([-100, 100])
64 
65  # set aspect ratio so that aspect ration of graph become 1
66  self.axis.set_aspect('equal', adjustable="box")
67 
68  self.fig.subplots_adjust(top=0.95, bottom=0.35, right=0.79)
69 
70  # @var is_polygon_drawn
71  # True if polygon is illustrated on window
72  self.is_polygon_drawn = False
73 
74  # @var is_path_drawn
75  # True if path is illustrated on window
76  self.is_path_drawn = False
77 
78  # @var server_node
79  # Instance of server
80  self.server_node = None
81 
82  # @var lines
83  # Dictionary to store Line2D objects
84  # - line: Line2D object representing edges of a polygon
85  # - dot: Line2D object representing vertices of a polygon
86  # - path: Line2D object representing coverage path
87  self.lines = {"line": self.axis.plot([], [], "-")[0],
88  "dot": self.axis.plot([], [], "o")[0],
89  "path": self.axis.plot([], [], "-")[0],
90  "subpolygons": None}
91 
92  # Register __call__ as callback function for line and dot
93  self.lines["line"].figure.canvas.mpl_connect(
94  'button_press_event', self)
95  self.lines["dot"].figure.canvas.mpl_connect('button_press_event', self)
96 
97  # @var points
98  # Dictionary to store points
99  # - vertices_x: List of x coordinates of vertices
100  # - vertices_y: List of y coordinates of vertices
101  # - start: geometry_msgs/Point object stores info about start point
102  # - waypoints: List of waypoints returned by a coverage path planner
103  self.points = {"vertices_x": list(),
104  "vertices_y": list(),
105  "start": None,
106  "waypoints": list()}
107 
108  self.subpolygons = []
109  self.patches = []
110 
111  # @var shooting_cond
112  # Dictionary of shooting condition
113  # - image_resolution_h [px]: Vertical resolution of image
114  # - image_resolution_w [px]: Horizontal resolution of image
115  # - angle_of_view [deg]: Camera's angle of view
116  # - height [m]: Flight height
117  self.shooting_cond = {"image_resolution_h": 640,
118  "image_resolution_w": 320,
119  "angle_of_view": 45.0,
120  "height": 30.0}
121 
122  footprint_width = Float64(2 * self.shooting_cond["height"] *
123  tan(self.shooting_cond["angle_of_view"] / 2))
124  aspect_ratio = \
125  float(self.shooting_cond["image_resolution_w"]) \
126  / self.shooting_cond["image_resolution_h"]
127 
128  # @var coverage_params
129  # Dictionary of coverage params
130  # - footprint_width [m]: Width of footprint
131  # - footprint_length [m]: Length of footprint
132  # - aspect_ratio []: Aspect of image
133  # - horizontal_overwrap [%]: Horizontal overwrap of footprint
134  # - vertical_overwrap [%]: Vertical overwrap of footprint
135  # cf. torres et, al. 2016
136  self.coverage_params = {"footprint_width":
137  footprint_width,
138  "footprint_length":
139  Float64(footprint_width.data *
140  aspect_ratio),
141  "aspect_ratio":
142  aspect_ratio,
143  "horizontal_overwrap": Float64(0.3),
144  "vertical_overwrap": Float64(0.2)}
145 
146  # @var buttons
147  # Dictionary of buttons
148  # - draw_button: Button object to evoke draw_polygon method
149  # - calc_button: Button object to evoke calculate_path method
150  # - clear_button: Button object to evoke clear_figure method
151  self.buttons = {"draw_button":
152  Button(plt.axes([0.8, 0.80, 0.15, 0.075]),
153  'Draw Polygon'),
154  "calc_button":
155  Button(plt.axes([0.8, 0.69, 0.15, 0.075]),
156  'Calculate CP'),
157  "clear_button":
158  Button(plt.axes([0.8, 0.58, 0.15, 0.075]),
159  'Clear')}
160 
161  # Register callback functions for buttons
162  self.buttons["draw_button"].on_clicked(self.draw_polygon)
163  self.buttons["calc_button"].on_clicked(self.calculate_path)
164  self.buttons["clear_button"].on_clicked(self.clear_figure)
165 
166  # Create textboxes
167  # @var text_boxes
168  # Dictionary of text boxes
169  # - image_resolution_h_box: TextBox object to get input for image_resolution_h
170  # - image_resolution_w_box: TextBox object to get input for image_resolution_w
171  # - angle_of_view_box: TextBox object to get input for angle_of_view
172  # - height_box: TextBox object to get input for height
173  # - horizontal_overwrap_box: TextBox object to get input for horizontal_overwrap
174  # - vertical_overwrap_box: TextBox object to get input for vertical_overwrap
175  self.text_boxes = {"image_resolution_h_box":
176  TextBox(plt.axes([0.25, 0.25, 0.1, 0.05]),
177  "Image Height [px]",
178  initial=str(self.shooting_cond["image_resolution_h"])),
179  "image_resolution_w_box":
180  TextBox(plt.axes([0.6, 0.25, 0.1, 0.05]),
181  "Image Width [px]",
182  initial=str(self.shooting_cond["image_resolution_w"])),
183  "angle_of_view_box":
184  TextBox(plt.axes([0.25, 0.175, 0.1, 0.05]),
185  "Angle of view [deg]",
186  initial=str(self.shooting_cond["angle_of_view"])),
187  "height_box":
188  TextBox(plt.axes([0.6, 0.175, 0.1, 0.05]),
189  "Height [m]",
190  initial=str(self.shooting_cond["height"])),
191  "horizontal_overwrap_box":
192  TextBox(plt.axes([0.25, 0.1, 0.1, 0.05]),
193  "Horizontal Overwrap [0.0 - 1.0]",
194  initial=str(self.coverage_params["horizontal_overwrap"].data)),
195  "vertical_overwrap_box":
196  TextBox(plt.axes([0.6, 0.1, 0.1, 0.05]),
197  "Vertical Overwrap [0.0 - 1.0]",
198  initial=str(self.coverage_params["vertical_overwrap"].data))}
199 
200  # Register callback functions for textboxes
201  self.text_boxes["image_resolution_h_box"].on_submit(
203  self.text_boxes["image_resolution_w_box"].on_submit(
205  self.text_boxes["angle_of_view_box"].on_submit(
207  self.text_boxes["height_box"].on_submit(self.height_update)
208  self.text_boxes["horizontal_overwrap_box"].on_submit(
210  self.text_boxes["vertical_overwrap_box"].on_submit(
212 
213  # @var labels
214  # Dictionary of labels on figure
215  # - aspect_ratio_text: Text object to display aspect_ratio
216  # - footprint_length_text: Text object to display footprint_length
217  # - footprint_width_text: Text object to display footprint_width
218  # - mode_text: Text object to display mode
219  # - start_point: Text object to display start point
220  # - SP: Start of waypoints
221  # - EP: End of waypoints
222  self.labels = {"aspect_ratio_text":
223  plt.text(2, 5,
224  "Aspect ratio:\n " + str(self.coverage_params["aspect_ratio"])),
225  "footprint_length_text":
226  plt.text(2, 4,
227  "Footprint Length [m]:\n " +
228  str(round(self.coverage_params["footprint_length"].data, 2))),
229  "footprint_width_text":
230  plt.text(2, 3,
231  "Footprint Width [m]:\n " +
232  str(round(self.coverage_params["footprint_width"].data, 2))),
233  "start_point": None,
234  "SP": None,
235  "EP": None}
236 
237  # plot a figure
238  plt.show()
239 
240  def __call__(self, event):
241  """!
242  Callback function for button_press_event
243  @param event MouseEvent object
244  """
245  # Return if click event doesn't happen in same axis as which a line lies on
246  if event.inaxes != self.lines["line"].axes:
247  return
248  # true if polygon is not drawn
249  if not self.is_polygon_drawn:
250  self.points["vertices_x"].append(event.xdata)
251  self.points["vertices_y"].append(event.ydata)
252  # illustrate a dot
253  self.lines["dot"].set_data(self.points["vertices_x"],
254  self.points["vertices_y"])
255  self.lines["dot"].figure.canvas.draw()
256  # true if start point is not set
257  elif not self.points["start"]:
258  # set start point
259  self.points["start"] = Point()
260  self.points["start"].x = event.xdata
261  self.points["start"].y = event.ydata
262 
263  # set and display start point and its label
264  self.labels["start_point"] = self.axis.text(
265  event.xdata, event.ydata, "Start", color="red", fontsize=16)
266  self.lines["dot"].set_data(self.points["vertices_x"] + [event.xdata],
267  self.points["vertices_y"] + [event.ydata])
268  self.lines["dot"].figure.canvas.draw()
269  else:
270  rospy.logwarn("Unable to register points anymore.")
271 
272  def update_params(self):
273  """!
274  Update values of coverage parameters
275  """
276  self.coverage_params["aspect_ratio"] = float(
277  self.shooting_cond["image_resolution_w"]) / self.shooting_cond["image_resolution_h"]
278  self.coverage_params["footprint_width"] = Float64(
279  2 * self.shooting_cond["height"] * tan(self.shooting_cond["angle_of_view"] / 2))
280  self.coverage_params["footprint_length"] = Float64(
281  self.coverage_params["footprint_width"].data / self.coverage_params["aspect_ratio"])
282 
284  """!
285  Update texts of coverage parameters
286  """
287  self.labels["aspect_ratio_text"].set_text("Aspect ratio:\n " +
288  str(self.coverage_params["aspect_ratio"]))
289  self.labels["footprint_length_text"].set_text("Footprint Length [m]:\n " +
290  str(round(self.coverage_params["footprint_length"].data, 2)))
291  self.labels["footprint_width_text"].set_text("Footprint Width [m]:\n " +
292  str(round(self.coverage_params["footprint_width"].data, 2)))
293  self.labels["footprint_length_text"].figure.canvas.draw()
294 
295  def draw_polygon(self, event):
296  """!
297  Callback function for "Draw Polygon" button
298  @param event MouseEvent object
299  """
300  # Return if vertices is less than 3
301  if len(self.points["vertices_x"]) < 3:
302  rospy.logerr("Unable to make a polygon.")
303  return
304  # Draw a polygon
305  self.lines["line"].set_data(self.points["vertices_x"] + self.points["vertices_x"][0:1],
306  self.points["vertices_y"] + self.points["vertices_y"][0:1])
307  self.lines["line"].figure.canvas.draw()
308  # Set flag as True
309  self.is_polygon_drawn = True
310 
311  def calculate_path(self, event):
312  """!
313  Callback function for "Calculate CP" button
314  @param event MouseEvent object
315  """
316  if not self.is_polygon_drawn:
317  return
318 
319  if not self.points["start"]:
320  rospy.logwarn("Choose start point.")
321  return
322 
323  if self.is_path_drawn:
324  return
325 
326  # assign server node if server node is None
327  if not self.server_node:
328  rospy.loginfo("Waiting for Server Node.")
329  try:
330  rospy.wait_for_service("cpp_torres16",
331  timeout=5.0)
332  except rospy.ROSException:
333  rospy.logerr("Server not found.")
334  return
335  try:
336  self.server_node = rospy.ServiceProxy(
337  "cpp_torres16",
338  Torres16)
339  except rospy.ServiceException as ex:
340  rospy.logerr(str(ex))
341  return
342 
343  # Create a list of vertices
344  vertices = []
345  waypoint_xs = []
346  waypoint_ys = []
347 
348  # fill the list of vertices that is passed to server node
349  for x_coord, y_coord in zip(self.points["vertices_x"],
350  self.points["vertices_y"]):
351  point = Point()
352  point.x = x_coord
353  point.y = y_coord
354  vertices.append(point)
355 
356  # Call service
357  try:
358  ret = self.server_node(vertices,
359  self.points["start"],
360  self.coverage_params["footprint_length"],
361  self.coverage_params["footprint_width"],
362  self.coverage_params["horizontal_overwrap"],
363  self.coverage_params["vertical_overwrap"])
364  self.points["waypoints"] = ret.path
365  self.subpolygons = ret.subpolygons
366 
367  # fill the lists of waypoints' coordinate to draw path
368  for num, point in enumerate(self.points["waypoints"]):
369  if num == 0:
370  waypoint_xs.append(self.points["start"].x)
371  waypoint_ys.append(self.points["start"].y)
372  self.labels["SP"] = self.axis.text(
373  point.x, point.y, "SP", color="red", fontsize=16)
374  waypoint_xs.append(point.x)
375  waypoint_ys.append(point.y)
376  if num == len(self.points["waypoints"]) - 1:
377  waypoint_xs.append(self.points["start"].x)
378  waypoint_ys.append(self.points["start"].y)
379  self.labels["EP"] = self.axis.text(
380  point.x, point.y, "EP", color="red", fontsize=16)
381  for subpolygon in self.subpolygons:
382  arr = np.ndarray([len(subpolygon.points), 2])
383  for num, point in enumerate(subpolygon.points):
384  arr[num][0] = point.x
385  arr[num][1] = point.y
386  patch = Polygon(xy=arr, alpha=0.5, edgecolor="navy")
387  self.axis.add_patch(patch)
388  self.patches.append(patch)
389 
390  self.lines["path"].set_data(waypoint_xs, waypoint_ys)
391  self.lines["path"].figure.canvas.draw()
392 
393  self.is_path_drawn = True
394  except rospy.ServiceException as ex:
395  rospy.logerr(str(ex))
396  return
397 
398  def clear_figure(self, event):
399  """!
400  Callback function for "Clear" button
401  @param event MouseEvent object
402  """
403 
404  # Clear lists of vertices' coordinates
405  self.points["vertices_x"] = []
406  self.points["vertices_y"] = []
407  # Set flag as False
408  self.is_polygon_drawn = False
409  self.is_path_drawn = False
410  # Clear start point
411  self.points["start"] = None
412 
413  # Clear waypoints
414  self.points["waypoints"] = []
415  self.points["subpolygons"] = []
416 
417  # Clear point data
418  self.lines["dot"].set_data(
419  self.points["vertices_x"], self.points["vertices_y"])
420  self.lines["line"].set_data(
421  self.points["vertices_x"], self.points["vertices_y"])
422  self.lines["path"].set_data([], [])
423 
424  if self.labels["start_point"]:
425  self.labels["start_point"].remove()
426  if self.labels["SP"]:
427  try:
428  self.labels["SP"].remove()
429  self.labels["EP"].remove()
430  except ValueError:
431  pass
432 
433  for patch in self.patches:
434  patch.remove()
435 
436  self.patches = []
437 
438  # Refresh a canvas
439  self.lines["dot"].figure.canvas.draw()
440  self.lines["line"].figure.canvas.draw()
441  self.lines["path"].figure.canvas.draw()
442 
443  def image_resolution_h_update(self, event):
444  """!
445  Called when content of "Image Height" is submitted
446  @param event Content of TextBox
447  """
448  # Update parameter if input is digit
449  try:
450  self.shooting_cond["image_resolution_h"] = int(event)
451  self.update_params()
452  self.update_param_texts()
453  except TypeError:
454  self.text_boxes["image_resolution_h_box"].\
455  set_val(str(self.shooting_cond["image_resolution_h"]))
456 
457  def image_resolution_w_update(self, event):
458  """!
459  Called when content of "Image Width" is submitted
460  @param event Content of TextBox
461  """
462  # Update parameter if input is digit
463  try:
464  self.shooting_cond["image_resolution_w"] = int(event)
465  self.update_params()
466  self.update_param_texts()
467  except TypeError:
468  self.text_boxes["image_resolution_w_box"].\
469  set_val(str(self.shooting_cond["image_resolution_w"]))
470 
471  def angle_of_view_update(self, event):
472  """!
473  Called when content of "Angle of view" is submitted
474  @param event Content of TextBox
475  """
476  # Update parameter if input is digit
477  try:
478  self.shooting_cond["angle_of_view"] = float(event)
479  self.update_params()
480  self.update_param_texts()
481  except TypeError:
482  self.text_boxes["angle_of_view_box"]\
483  .set_val(str(self.shooting_cond["angle_of_view"]))
484 
485  def height_update(self, event):
486  """!
487  Called when content of "Height" is submitted
488  @param event Content of TextBox
489  """
490  # Update parameter if input is digit
491  try:
492  self.shooting_cond["height"] = float(event)
493  self.update_params()
494  self.update_param_texts()
495  except TypeError:
496  self.text_boxes["height_box"].\
497  set_val(str(self.shooting_cond["height"]))
498 
499  def horizontal_overwrap_update(self, event):
500  """!
501  Called when content of "Horizontal overwrap" is submitted
502  @param event Content of TextBox
503  """
504  # Update parameter if input is digit and valid value
505  try:
506  if 0 < float(event) < 1.0:
507  self.coverage_params["horizontal_overwrap"].data = float(event)
508  else:
509  self.text_boxes["horizontal_overwrap_box"].\
510  set_val(str(self.coverage_params["horizontal_overwrap"].data))
511  except TypeError:
512  self.text_boxes["horizontal_overwrap_box"].\
513  set_val(str(self.coverage_params["horizontal_overwrap"].data))
514 
515  def vertical_overwrap_update(self, event):
516  """!
517  Called when content of "Vertical overwrap" is submitted
518  @param event Content of TextBox
519  """
520  # Update parameter if input is digit and valid value
521  try:
522  if 0 < float(event) < 1.0:
523  self.coverage_params["vertical_overwrap"].data = float(event)
524  else:
525  self.text_boxes["vertical_overwrap_box"].\
526  set_val(str(self.coverage_params["vertical_overwrap"].data))
527  except TypeError:
528  self.text_boxes["vertical_overwrap_box"].\
529  set_val(str(self.coverage_params["vertical_overwrap"].data))
530 
531 
532 def init_node():
533  """!
534  Initialize a node
535  """
536  rospy.init_node('specify_node', anonymous=True)
537 
538  # Call PolygonBuilder's constructor
540 
541 
542 if __name__ == '__main__':
543  init_node()
def vertical_overwrap_update(self, event)
Called when content of "Vertical overwrap" is submitted.
def image_resolution_w_update(self, event)
Called when content of "Image Width" is submitted.
def update_param_texts(self)
Update texts of coverage parameters.
def height_update(self, event)
Called when content of "Height" is submitted.
def init_node()
Initialize a node.
def angle_of_view_update(self, event)
Called when content of "Angle of view" is submitted.
def horizontal_overwrap_update(self, event)
Called when content of "Horizontal overwrap" is submitted.
def clear_figure(self, event)
Callback function for "Clear" button.
def draw_polygon(self, event)
Callback function for "Draw Polygon" button.
def image_resolution_h_update(self, event)
Called when content of "Image Height" is submitted.
def update_params(self)
Update values of coverage parameters.
def calculate_path(self, event)
Callback function for "Calculate CP" button.
def __call__(self, event)
Callback function for button_press_event.
def __init__(self)
Constructor.
Definition: specify_rect.py:49
GUI class to specify a polygon.
Definition: specify_rect.py:44