-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathoxpath.py
428 lines (365 loc) · 15.6 KB
/
oxpath.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# This module generates an ox-style (boustrophedon) coverage path
# for a field that has already been decomposed and linked into an
# ordered list of cells.
import shapely.affinity
from shapely.geometry import Point, LineString, LinearRing
from heapq import heappush, heappop # priority queue
import copy
class CellPath:
"""
A wrapper class containing a list of waypoints for the drone to
cover a single cell.
"""
def __init__(self, waypoints):
"""
waypoints: a list of shapely Point objects
"""
self.waypoints = waypoints
self.start_point = waypoints[0]
self.end_point = waypoints[-1]
class Transition:
"""
A class that generates a list of waypoints for the drone to pass
from one point in the polygon to another point in the polygon.
Used to generate transitions between the coverage path for one
cell and the coverage path for the following cell (or a path to
return to the starting point).
"""
def __init__(self, start_node, end_node, start_point, end_point,
graph_traps, eroded_polygon):
"""
start_node: The CellNode in which the transition begins
end_node: The CellNode in which the transition ends
start_point: The point (shapely Point object) at which the
transition begins. Should be within the start
node.
end_point: The point at which the transition ends.
Should be within the end node.
graph_traps: A list of TrapNode objects representing the
graph of trapezoids, as returned by
cellgrapher.build_graph().
eroded_polygon: The polygon representing the field, with its
edges eroded to ensure a safe radius for the
drone.
"""
self.start_node = start_node
self.end_node = end_node
self.start_point = start_point
self.end_point = end_point
self.graph_traps = graph_traps
self.eroded_polygon = eroded_polygon
# Determine a path through the graph of trapezoids to get
# from the start point to the end point
self.trap_start, self.trap_end = self.find_start_and_end()
trap_sequence = [self.trap_start]
self.mark_traps_unvisited()
self.find_seq_to_end(trap_sequence)
# Generate a list of transition waypoints using this path
self.generate_waypoints(trap_sequence)
# Calculate the length of this transition
self.update_length()
def find_start_and_end(self):
# Determine which trapezoid contains the start point
for trap_start in self.start_node.children:
if trap_start.polygon.contains(self.start_point):
break
else:
raise Exception("None of the children of start node "
"contains start point")
# Determine which trapezoid contains the end point
for trap_end in self.end_node.children:
if trap_end.polygon.contains(self.end_point):
break
else:
raise Exception("None of the children of end node "
"contains end point")
return (trap_start, trap_end)
def mark_traps_unvisited(self):
for node in self.graph_traps:
node.visited = False
def find_seq_to_end(self, trap_sequence):
"""
Returns True if a sequence to the next cell was successfully
found (i.e. trap_sequence is now complete), False otherwise.
"""
trap_curr = trap_sequence[-1]
trap_curr.visited = True
if trap_curr is self.trap_end:
return True
neighbors = []
for edge in trap_curr.edges:
if edge.node_a is trap_curr:
neighbor = edge.node_b
else:
neighbor = edge.node_a
if not neighbor.visited:
neighbors.append(neighbor)
neighbors.sort(key=lambda n : n.polygon.distance(
self.trap_end.polygon))
for neighbor in neighbors:
trap_sequence.append(neighbor)
success = self.find_seq_to_end(trap_sequence)
if success:
return True
else:
trap_sequence.pop()
return False
def generate_waypoints(self, trap_sequence):
self.waypoints = [self.start_point]
trap_index = 0
while True:
# try to go directly to target
# TODO: deal with case where, even once in the final
# cell, the drone isn't able to fly directly to the
# target for some reason. This should never happen, but
# given the potential imprecision of shapely, it seems
# worth checking for.
current_point = self.waypoints[-1]
if current_point.equals(self.end_point):
break
line = LineString([(current_point.x, current_point.y),
(self.end_point.x, self.end_point.y)])
assert line.is_valid
if self.eroded_polygon.contains(line):
self.waypoints.append(self.end_point)
break
curr_trap = trap_sequence[trap_index]
next_trap = trap_sequence[trap_index + 1]
# find the edge between this trapezoid and the next
for edge in curr_trap.edges:
if (edge.node_a == next_trap or
edge.node_b == next_trap):
break
else:
raise Exception('No edge found between this '
'trapezoid and next trapezoid in '
'sequence')
border = edge.border_line
passing_point = border.interpolate(border.project(
self.end_point))
if curr_trap.eroded != None:
exterior = curr_trap.eroded.exterior
point = exterior.interpolate(exterior.project(
passing_point))
self.waypoints.append(point)
if next_trap.eroded == None:
self.waypoints.append(next_trap.polygon.centroid)
else:
exterior = next_trap.eroded.exterior
point = exterior.interpolate(exterior.project(
passing_point))
self.waypoints.append(point)
trap_index += 1
def update_length(self):
"""
Set the self.length property to the total of all the
straight-line distances between consecutive waypoints in this
transition
"""
self.length = 0
prev_point = self.waypoints[0]
for point in self.waypoints[1:]:
self.length += prev_point.distance(point)
prev_point = point
def rotate_path(path, angle, origin):
"""
Rotate all the waypoints in a cell path or transition by the
given angle around the given origin.
"""
for i in xrange(0, len(path.waypoints)):
path.waypoints[i] = shapely.affinity.rotate(
path.waypoints[i], angle,
origin=origin)
class CompletePath:
"""
A coverage path for an entire field.
The coverage path is represented as a list of CellPaths and a
list of Transitions. Each CellPath contains the waypoints to
cover a given cell. Each Transition contains the waypoints to
move from that cell to the subsequent cell without exiting the
bounds of the field or touching obstacles.
The drone should follow the Transition at self.transitions[0],
followed by the CellPath at self.cells[0], then the transition at
transitions[1], cell at cells[1], etc. The cell at index i
is always intended to follow the transition at index i. In
order to ensure this, the user of this class should alternate calls
to add_cell() and add_transition(), starting with add_transition()
(since the first transition and cell are added by the constructor).
Not obeying this alternation will cause an Exception to be raised.
"""
def __init__(self, initial_cell, graph_traps, initial_cell_path,
eroded_polygon):
self.transitions = []
self.cells = []
self.length = 0 # total length of
# transitions between cells (i.e. not
# including the distance spent covering each
# cell, just the distance going from one cell
# to the next one)
self.polygon = eroded_polygon
# Add transition from home point (0, 0) to start point
self.add_transition(initial_cell, initial_cell, Point(0, 0),
initial_cell_path.start_point, graph_traps)
# Add coverage path for first cell
self.add_cell_path(initial_cell_path)
def cells_traversed(self):
return len(self.cells)
def add_cell_path(self, cell_path):
if not self.has_transition():
raise Exception("Tried to append cell path without "
"first adding a transition")
self.cells.append(cell_path)
def start_point(self):
return self.transitions[0].start_point
def end_point(self):
if self.has_transition():
return self.transitions[-1].end_point
else:
return self.cells[-1].end_point
def has_transition(self):
return len(self.transitions) == len(self.cells) + 1
def add_transition(self, this_node, next_node,
start_point, end_point, graph_traps):
if self.has_transition():
raise Exception("Tried to add transition when "
"transition was already present")
transition = Transition(this_node, next_node, start_point,
end_point, graph_traps, self.polygon)
self.transitions.append(transition)
self.length += transition.length
def copy(self):
"""
Return a shallow copy of this CompletePath, with additional
shallow copies of the cell and transition lists so that they
can safely be modified in the copy.
"""
path_copy = copy.copy(self)
path_copy.cells = copy.copy(self.cells)
path_copy.transitions = copy.copy(self.transitions)
return path_copy
def rotate(self, angle, origin):
"""
Rotate all the waypoints that make up the cell paths and
transitions by the given angle around the given origin.
"""
for cell_path in self.cells:
rotate_path(cell_path, angle, origin)
for transition in self.transitions:
rotate_path(transition, angle, origin)
def traversal_endpoints(polygon, x, miny, maxy):
line = LineString([(x, miny - 1), (x, maxy + 1)])
intersection = line.intersection(polygon)
if isinstance(intersection, Point):
return intersection, intersection
else:
# TODO: better error handling
assert isinstance(intersection, LineString)
endpoints = intersection.boundary
return (min(endpoints, key=lambda point: point.y),
max(endpoints, key=lambda point: point.y))
def generate_cell_path(bottoms, tops, start_top, start_left):
waypoints = []
top_to_bottom = start_top
if start_left:
indices = range(0, len(bottoms))
else:
indices = range(len(bottoms) - 1, -1, -1)
for i in indices:
if top_to_bottom:
waypoints += [tops[i], bottoms[i]]
else:
waypoints += [bottoms[i], tops[i]]
top_to_bottom = not top_to_bottom
return CellPath(waypoints)
def possible_paths(node, path_radius):
"""
Note: the node passed should already have a .eroded property,
as generated by generate_eroded_nodes() (and not equal to
None).
"""
polygon = node.eroded
minx, miny, maxx, maxy = polygon.bounds
bottoms = []
tops = []
x = minx
while x <= maxx:
bottom, top = traversal_endpoints(polygon, x, miny, maxy)
bottoms.append(bottom)
tops.append(top)
x += path_radius * 2
assert(len(bottoms) > 0)
return [
generate_cell_path(bottoms, tops, True, True),
generate_cell_path(bottoms, tops, True, False),
generate_cell_path(bottoms, tops, False, True),
generate_cell_path(bottoms, tops, False, False)
]
def generate_eroded_nodes(stack, path_radius):
"""
For each cell (node) in the stack, generate an eroded version
of its polygon, as well as an eroded version of each of its
child trapezoids. These eroded versions are used when generating
the path to make sure the drone does not pass too close to the
edge of the field.
"""
for node in stack:
node.eroded = node.polygon.buffer(-path_radius)
if not isinstance(node.eroded.exterior, LinearRing):
# After eroding, the cell disappeared completely,
# meaning it was too small for the drone to
# safely drop pesticides on it
node.eroded = None
for child in node.children:
child.eroded = child.polygon.buffer(-path_radius)
if not isinstance(child.eroded.exterior, LinearRing):
child.eroded = None
def remove_uncoverable_cells(stack):
"""
Remove all the cells in the stack that cannot be covered
because they are too small (the eroded version has zero area)
"""
stack[:] = [node for node in stack
if node.eroded != None]
def generate_path(stack, path_radius, polygon, graph_traps):
eroded_polygon = polygon.buffer(-path_radius / 2)
generate_eroded_nodes(stack, path_radius)
remove_uncoverable_cells(stack)
# Generate all possible ox paths for each of the individual
# cells (linear time), as CellPath objects. The indices of
# stack and cell_paths are the same, i.e. the possible ox
# paths for the cell at stack[i] are stored in a list at
# cell_paths[i].
cell_paths = []
for node in stack:
cell_paths.append(possible_paths(node, path_radius))
# Find a CompletePath that covers the field in the minimum
# distance.
# heap is a priority queue containing partially complete coverage
# paths, prioritized so that a pop will remove the shortest path
heap = []
for initial_cell_path in cell_paths[0]:
heappush(heap, (0, CompletePath(stack[0], graph_traps,
initial_cell_path,
eroded_polygon)))
while True:
# pop the shortest path from the heap
priority, path = heappop(heap)
index = path.cells_traversed()
if index == len(stack):
if path.has_transition():
return path
else:
# add transition back to start point
path.add_transition(stack[-1], stack[0],
path.end_point(), path.start_point(),
graph_traps)
heappush(heap, (path.length, path))
continue
for cell_path in cell_paths[index]:
new_path = path.copy()
new_path.add_transition(stack[index-1], stack[index],
new_path.end_point(),
cell_path.start_point,
graph_traps)
new_path.add_cell_path(cell_path)
heappush(heap, (new_path.length, new_path))