-
Notifications
You must be signed in to change notification settings - Fork 0
/
number_of_islands.py
342 lines (265 loc) · 12 KB
/
number_of_islands.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
import os
import sys
import rasterio
import numpy as np
class NumberOfIslands:
def __init__(self, graph, algorithm="dfs", contiguity="rook", is_land=None, out=False, out_folder='data'):
# too low and dfs will hit python's recursion limit
# too high and the program will segfault
sys.setrecursionlimit(20000)
# copy data so subsequent calls have original data
# grid is a list of 3 numpy arrays pertaining to 3 bands (i.e. r, g, b)
self.graph = [band.copy() for band in graph]
self.height = graph[0].shape[0]
self.width = graph[0].shape[1]
self.visited = np.zeros(self.graph[0].shape) # keep record of visitations in memory -> no double-visiting
self.queue = [] # FIFO data structure used for bfs
self.stack = [] # LIFO data structure used for dfs'
self.neighbors_rook = [(-1, 0), (0, 1), (1, 0), (0, -1)] # N, E, S, W
self.neighbors_queen = [(-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (-1, 1), (0, -1), (-1, -1)] # N, NE, E, SE, S, SW, W, NW
self.algorithm = {"dfs": self.dfs, "bfs": self.bfs, "dfs'": self.dfs_prime}[algorithm]
self.contiguity = {"rook": self.neighbors_rook, "queen": self.neighbors_queen}[contiguity]
self.out = out
self.out_folder = out_folder
self.out_options = {
'driver': 'GTiff',
'dtype': 'uint8',
'nodata': None,
'width': self.width if (self.width % 2) == 0 else self.width-1, # must be even
'height': self.height if (self.height % 2) == 0 else self.height-1, # must be even
'count': 3, # 3 bands (i.e. rgb)
'crs': None,
'tiled': False,
'interleave': 'pixel',
'compress': 'lzw'
}
# cursor shape (+) around cell being visited for visualizations.
# does not necessarily reflect a cell's neighbors, just used for increased visibility
self.cursor = [
(-1, 0), (-2, 0), (-3, 0), (-4, 0), (-5, 0), (-6, 0), # N
(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), # E
(1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), # S
(0, -1), (0, -2), (0, -3), (0, -4), (0, -5), (0, -6), # W
]
self.colors = {
'cursor': [205, 0, 0],
'land': [50, 250, 50],
'shore': [250, 100, 50],
}
self.previous_color = [0, 0, 0]
self.cursor_previous_colors = [[0, 0, 0] for i in range(len(self.cursor))]
self.print_index = 0
# register a simple classification function of type ((r, g, b) -> boolean)
if is_land is None:
# this is terrible and should never be used
self.is_land = lambda r, g, b : r + g + b > 300
else:
# thats more like it
self.is_land = is_land
def write_raster(self):
"""
write the state of the search grid to a new raster for visualization
args:
effects: produces a new raster
returns:
"""
rs, gs, bs = self.graph
# write all 3 bands to a new raster
with rasterio.open(f'{self.out_folder}/noi_{self.print_index:06}.tif', 'w' , **self.out_options) as dst:
dst.write(rs.astype(rasterio.uint8), 1)
dst.write(gs.astype(rasterio.uint8), 2)
dst.write(bs.astype(rasterio.uint8), 3)
# increment the number of rasters produced
self.print_index += 1
def color_cursor(self, row, col):
"""
color the cell being visited and the nearby cells for visibility
args: (row, col), the location of the cell being visited
effects: colors current cell and nearby cells, resets colors of cells previously colored by cursor
returns:
"""
rs, gs, bs = self.graph
# save current color
self.previous_color = [rs[row][col], gs[row][col], bs[row][col]]
# color current cell
r, g, b = self.colors['shore']
rs[row][col] = r
gs[row][col] = g
bs[row][col] = b
# color the cursor (cell being visited & cardinal directions for visibility)
for k, n in enumerate(self.cursor):
n_r = row + n[0]
n_c = col + n[1]
if self.is_valid(n_r, n_c):
self.cursor_previous_colors[k] = [rs[n_r][n_c], gs[n_r][n_c], bs[n_r][n_c]]
rs[n_r][n_c] = r
gs[n_r][n_c] = g
bs[n_r][n_c] = b
self.write_raster()
# reset cell's color
r, g, b = self.previous_color
rs[row][col] = r
gs[row][col] = g
bs[row][col] = b
# reset cursor's colors
for k, n in enumerate(self.cursor):
n_r = row + n[0]
n_c = col + n[1]
if self.is_valid(n_r, n_c):
r, g, b = self.cursor_previous_colors[k]
rs[n_r][n_c] = r
gs[n_r][n_c] = g
bs[n_r][n_c] = b
def is_valid(self, row, col):
"""
ensure location is within the raster's bounds
args: (row, col), the location of the cell being visited
effects:
returns:
"""
# ensure location is within the raster's bounds
return (row >= 0) and (row < self.height) and (col >= 0) and (col < self.width)
def dfs(self, row, col):
"""
Depth First Search
see: https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/
note: due to python's recursion limitations, this will fail on large rasters.
workarounds exist but obscure the nature of the algorithm
see: https://stackoverflow.com/questions/28660685/recursion-depth-issue-using-python-with-dfs-algorithm
args: (row, col), the location of the cell being visited
effects: write state of search to a new raster if self.out is set
returns:
"""
rs, gs, bs = self.graph
# mark cell as visited
self.visited[row][col] = 1
# if visualizing, skip some cells to save memory
if self.out and (col % 4 == 0):
self.color_cursor(row, col)
for n in self.contiguity:
n_r = row + n[0]
n_c = col + n[1]
if self.is_valid(n_r, n_c) and not self.visited[n_r][n_c]:
# mark cell as visited
self.visited[n_r][n_c] = 1
# Case: land
if self.is_land(rs[n_r][n_c], gs[n_r][n_c], bs[n_r][n_c]):
self.dfs(n_r, n_c)
# Case: shore (water near land)
else:
r, g, b = self.colors['shore']
rs[n_r][n_c] = r
gs[n_r][n_c] = g
bs[n_r][n_c] = b
def dfs_prime(self, row, col):
"""
Depth First Search
see: https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/
note: this version uses a stack in memory instead of the call stack, which avoids issues with recursion depth
see: https://stackoverflow.com/questions/28660685/recursion-depth-issue-using-python-with-dfs-algorithm
args: (row, col), the location of the cell being visited
effects: write state of search to a new raster if self.out is set
returns:
"""
rs, gs, bs = self.graph
# mark cell as visited
self.visited[row][col] = 1
# push cell to visit it's neighbors in the future
self.stack.append([row, col])
while self.stack:
# pop last cell to visit it's neighbors
s = self.stack.pop()
# if visualizing, skip some cells to save memory
if self.out and (s[1] % 4 == 0):
self.color_cursor(s[0], s[1])
for n in self.contiguity:
n_r = s[0] + n[0]
n_c = s[1] + n[1]
if self.is_valid(n_r, n_c) and not self.visited[n_r][n_c]:
# mark cell as visited
self.visited[n_r][n_c] = 1
# Case: land
if self.is_land(rs[n_r][n_c], gs[n_r][n_c], bs[n_r][n_c]):
# push cell to visit it's neighbors in the future
self.stack.append([n_r, n_c])
# Case: shore (water near land)
else:
r, g, b = self.colors['shore']
rs[n_r][n_c] = r
gs[n_r][n_c] = g
bs[n_r][n_c] = b
def bfs(self, row, col):
"""
Breadth First Search
see: https://www.geeksforgeeks.org/breadth-first-search-or-bfs-for-a-graph/
args: (row, col), the location of the cell being visited
effects: write state of search to a new raster if self.out is set
returns:
"""
rs, gs, bs = self.graph
# mark cell as visited
self.visited[row][col] = 1
# enqueue cell to visit it's neighbors in the future
self.queue.append([row, col])
while self.queue:
# dequeue first cell to visit it's neighbors
s = self.queue.pop(0)
# if visualizing, skip some cells to save memory
if self.out and (s[1] % 4 == 0):
self.color_cursor(s[0], s[1])
for n in self.contiguity:
n_r = s[0] + n[0]
n_c = s[1] + n[1]
if self.is_valid(n_r, n_c) and not self.visited[n_r][n_c]:
# mark cell as visited
self.visited[n_r][n_c] = 1
# Case: land
if self.is_land(rs[n_r][n_c], gs[n_r][n_c], bs[n_r][n_c]):
# enqueue cell to visit it's neighbors in the future
self.queue.append([n_r, n_c])
# Case: shore (water near land)
else:
r, g, b = self.colors['shore']
rs[n_r][n_c] = r
gs[n_r][n_c] = g
bs[n_r][n_c] = b
def number_of_islands(self):
"""
traverse raster, trigger bfs or dfs when land is hit.
when unvisited land is hit, the counter is incremented and a search algorithm "visits" the rest of the contiguous land
args:
effects: write state of search to a new raster if self.out is set
returns: count (number of islands in input raster)
"""
# if input raster has incorrect shape do nothing
if not self.graph or not len(self.graph)==3 or not self.width or not self.height:
raise Exception("Input raster has incorrect shape")
rs, gs, bs = self.graph
self.visited = np.zeros(rs.shape) # re-init visited
row = self.height
col = self.width
count = 0
try:
# add some frames to beginning of visualization
if self.out:
for k in range(60):
self.color_cursor(0, 0)
for i in range(0, row):
for j in range(0, col):
if not self.visited[i][j]:
# if visualizing, skip some cells to save memory
if self.out and (j % 20 == 0):
self.color_cursor(i, j)
# Case: Land
if self.is_land(rs[i][j], gs[i][j], bs[i][j]):
self.algorithm(i, j)
count += 1
# Case: Water (not near land) (OPTIONAL)
# add some frames to beginning of visualization
if self.out:
for k in range(60):
self.color_cursor(row-1, col-1)
return count
except RecursionError:
# warn but continue execution (in case of subsequent calls)
raise Exception("Error: The Python Recursion Limit has been reached. Try bfs, dfs', or a smaller input raster.\n")