Skip to content

Commit 7e5abb8

Browse files
authored
Merge pull request #255 from joshuacortez/fix/fast_gridding_corners
Fix missing tiles in corners for fast gridding by using off-boundary pixels
2 parents e6e18fb + 0668649 commit 7e5abb8

File tree

6 files changed

+761
-235
lines changed

6 files changed

+761
-235
lines changed

geowrangler/gridding_utils/polygon_fill.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@
1111
import geopandas as gpd
1212
import polars as pl
1313

14-
# %% ../../notebooks/15_polygon_fill.ipynb 11
14+
# %% ../../notebooks/15_polygon_fill.ipynb 12
1515
def voxel_traversal_2d(
1616
start_vertex: Tuple[int, int],
1717
end_vertex: Tuple[int, int],
1818
debug: bool = False, # if true, prints diagnostic info for the algorithm
19-
) -> List[Tuple[int, int]]:
19+
) -> Dict[str, List[Tuple[int, int]]]:
2020
"""
2121
Returns all pixels between two points as inspired by Amanatides & Woo's “A Fast Voxel Traversal Algorithm For Ray Tracing”
2222
2323
Implementation adapted from https://www.redblobgames.com/grids/line-drawing/ in the supercover lines section
24+
25+
This also returns the off-diagonal pixels that can be useful for correcting errors at the corners of polygons during polygon fill
2426
"""
2527

2628
# Setup initial conditions
@@ -30,26 +32,32 @@ def voxel_traversal_2d(
3032
direction_x = 1 if x2 > x1 else -1
3133
direction_y = 1 if y2 > y1 else -1
3234

35+
result = {"line_pixels": [], "off_diagonal_pixels": []}
36+
3337
# Single point
3438
if (x1 == x2) and (y1 == y2):
3539
pixels = [(x1, y1)]
36-
return pixels
40+
result["line_pixels"].extend(pixels)
41+
return result
3742

3843
# Vertical line
3944
elif x1 == x2:
4045
pixels = [(x1, y) for y in range(y1, y2 + direction_y, direction_y)]
41-
return pixels
46+
result["line_pixels"].extend(pixels)
47+
return result
4248

4349
# Horizontal line
4450
elif y1 == y2:
4551
pixels = [(x, y1) for x in range(x1, x2 + direction_x, direction_x)]
46-
return pixels
52+
result["line_pixels"].extend(pixels)
53+
return result
4754

4855
dy = y2 - y1
4956
dx = x2 - x1
5057

5158
pixel_x, pixel_y = x1, y1
5259
pixels = [(pixel_x, pixel_y)]
60+
off_diagonal_pixels = []
5361

5462
is_finished = False
5563

@@ -73,6 +81,10 @@ def voxel_traversal_2d(
7381

7482
decision = (1 + 2 * ix) * ny - (1 + 2 * iy) * nx
7583
if decision == 0:
84+
85+
off_diagonal_pixels.append((pixel_x + direction_x, pixel_y))
86+
off_diagonal_pixels.append((pixel_x, pixel_y + direction_y))
87+
7688
# diagonal step
7789
pixel_x += direction_x
7890
pixel_y += direction_y
@@ -106,9 +118,10 @@ def voxel_traversal_2d(
106118
if is_x_finished and is_y_finished:
107119
break
108120

109-
return pixels
121+
result = {"line_pixels": pixels, "off_diagonal_pixels": off_diagonal_pixels}
122+
return result
110123

111-
# %% ../../notebooks/15_polygon_fill.ipynb 15
124+
# %% ../../notebooks/15_polygon_fill.ipynb 16
112125
def interpolate_x(
113126
start_vertex: Tuple[int, int],
114127
end_vertex: Tuple[int, int],
@@ -125,7 +138,7 @@ def interpolate_x(
125138
interpolated_x = x1 + (y - y1) * inverse_slope
126139
return interpolated_x
127140

128-
# %% ../../notebooks/15_polygon_fill.ipynb 16
141+
# %% ../../notebooks/15_polygon_fill.ipynb 17
129142
def scanline_fill(
130143
vertices: List[
131144
Tuple[int, int]
@@ -182,38 +195,50 @@ def scanline_fill(
182195

183196
return filled_pixels
184197

185-
# %% ../../notebooks/15_polygon_fill.ipynb 20
198+
# %% ../../notebooks/15_polygon_fill.ipynb 21
186199
def voxel_traversal_scanline_fill(
187200
vertices_df: Union[
188201
pd.DataFrame, pl.DataFrame
189202
], # dataframe with x_col and y_col for the polygon vertices
190203
x_col: str = "x",
191204
y_col: str = "y",
192205
debug: bool = False, # if true, prints diagnostic info for both voxel traversal and scanline fill algorithms
193-
) -> Set[Tuple[int, int]]:
206+
) -> Dict[str, Set[Tuple[int, int]]]:
194207
"""
195208
Returns pixels that intersect a polygon.
196209
197210
This uses voxel traversal to fill the boundary, and scanline fill for the interior. All coordinates are assumed to be integers.
211+
212+
This also returns the off-boundary pixels that can be useful for correcting errors at the corners of polygons during polygon fill
198213
"""
199214

200215
vertices = list(zip(vertices_df[x_col].to_list(), vertices_df[y_col].to_list()))
201216
offset_vertices = vertices[1:] + vertices[:1]
202217

203218
polygon_pixels = set()
219+
off_boundary_pixels = set()
204220

205221
for start_vertex, end_vertex in zip(vertices, offset_vertices):
206-
polygon_pixels.update(voxel_traversal_2d(start_vertex, end_vertex, debug))
222+
voxel_traversal_results = voxel_traversal_2d(start_vertex, end_vertex, debug)
223+
polygon_pixels.update(voxel_traversal_results["line_pixels"])
224+
off_boundary_pixels.update(voxel_traversal_results["off_diagonal_pixels"])
207225

208226
polygon_pixels.update(scanline_fill(vertices, debug))
209227

210-
return polygon_pixels
228+
# removing off boundary tiles that are actually in the interior
229+
off_boundary_pixels = off_boundary_pixels - polygon_pixels
230+
231+
result = {
232+
"polygon_pixels": polygon_pixels,
233+
"off_boundary_pixels": off_boundary_pixels,
234+
}
235+
return result
211236

212-
# %% ../../notebooks/15_polygon_fill.ipynb 27
237+
# %% ../../notebooks/15_polygon_fill.ipynb 28
213238
SUBPOLYGON_ID_COL = "__subpolygon_id__"
214239
PIXEL_DTYPE = pl.Int32
215240

216-
# %% ../../notebooks/15_polygon_fill.ipynb 28
241+
# %% ../../notebooks/15_polygon_fill.ipynb 29
217242
def polygons_to_vertices(
218243
polys_gdf: gpd.GeoDataFrame,
219244
unique_id_col: Optional[
@@ -251,13 +276,13 @@ def polygons_to_vertices(
251276

252277
return vertices_df
253278

254-
# %% ../../notebooks/15_polygon_fill.ipynb 32
279+
# %% ../../notebooks/15_polygon_fill.ipynb 33
255280
def fast_polygon_fill(
256281
vertices_df: pl.DataFrame, # integer vertices of all polygons in the AOI
257282
unique_id_col: Optional[
258283
str
259284
] = None, # the ids under this column will be preserved in the output tiles
260-
) -> pl.DataFrame:
285+
) -> Dict[str, pl.DataFrame]:
261286

262287
if unique_id_col is not None:
263288
id_cols = [SUBPOLYGON_ID_COL, unique_id_col]
@@ -276,6 +301,7 @@ def fast_polygon_fill(
276301
polygon_ids = vertices_df.select(id_cols).unique(maintain_order=True).rows()
277302

278303
tiles_in_geom = set()
304+
tiles_off_boundary = set()
279305
for polygon_id in polygon_ids:
280306
subpolygon_id, unique_id = polygon_id
281307
filter_expr = (pl.col(SUBPOLYGON_ID_COL) == subpolygon_id) & (
@@ -284,14 +310,21 @@ def fast_polygon_fill(
284310
poly_vertices = vertices_df.filter(filter_expr)
285311

286312
poly_vertices = poly_vertices.unique(maintain_order=True)
287-
_tiles_in_geom = voxel_traversal_scanline_fill(
313+
voxel_traversal_results = voxel_traversal_scanline_fill(
288314
poly_vertices, x_col="x", y_col="y"
289315
)
316+
_tiles_in_geom = voxel_traversal_results["polygon_pixels"]
317+
_tiles_off_boundary = voxel_traversal_results["off_boundary_pixels"]
290318

291319
if has_unique_id_col:
292320
_tiles_in_geom = [(x, y, unique_id) for (x, y) in _tiles_in_geom]
321+
_tiles_off_boundary = [(x, y, unique_id) for (x, y) in _tiles_off_boundary]
293322

294323
tiles_in_geom.update(_tiles_in_geom)
324+
tiles_off_boundary.update(_tiles_off_boundary)
325+
326+
# removing off boundary tiles that are actually in the interior
327+
tiles_off_boundary = tiles_off_boundary - tiles_in_geom
295328

296329
schema = {"x": PIXEL_DTYPE, "y": PIXEL_DTYPE}
297330
if has_unique_id_col:
@@ -303,4 +336,12 @@ def fast_polygon_fill(
303336
schema=schema,
304337
)
305338

306-
return tiles_in_geom
339+
tiles_off_boundary = pl.from_records(
340+
data=list(tiles_off_boundary),
341+
orient="row",
342+
schema=schema,
343+
)
344+
345+
result = {"tiles_in_geom": tiles_in_geom, "tiles_off_boundary": tiles_off_boundary}
346+
347+
return result

geowrangler/grids.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,25 @@ def generate_grid(
231231
vertices, boundary, northing_col="y", easting_col="x"
232232
)
233233

234-
tiles_in_geom = polygon_fill.fast_polygon_fill(vertices, unique_id_col)
235-
234+
polygon_fill_result = polygon_fill.fast_polygon_fill(vertices, unique_id_col)
235+
tiles_in_geom = polygon_fill_result["tiles_in_geom"]
236236
bboxes = self._xy_to_bbox(tiles_in_geom, boundary, "x", "y")
237237

238+
# this is error correction on the polygon boundary (not the square boundary)
239+
tiles_off_boundary = polygon_fill_result["tiles_off_boundary"]
240+
if not tiles_off_boundary.is_empty():
241+
off_boundary_bboxes = self._xy_to_bbox(tiles_off_boundary, boundary, "x", "y")
242+
all_polygon_boundary = reprojected_gdf.boundary.union_all(method="unary")
243+
intersects_boundary_bool = off_boundary_bboxes.intersects(all_polygon_boundary)
244+
245+
addtl_tiles_in_geom = tiles_off_boundary.filter(
246+
pl.Series(intersects_boundary_bool)
247+
)
248+
addtl_boundary_bboxes = off_boundary_bboxes[intersects_boundary_bool].copy()
249+
250+
tiles_in_geom = pl.concat([tiles_in_geom, addtl_tiles_in_geom])
251+
bboxes = pd.concat([bboxes, addtl_boundary_bboxes], ignore_index=True)
252+
238253
column_order = ["x", "y"]
239254
if unique_id_col is not None:
240255
column_order += [unique_id_col]
@@ -664,7 +679,20 @@ def generate_grid(
664679
vertices = polygon_fill.polygons_to_vertices(aoi_gdf, unique_id_col)
665680
vertices = self._latlng_to_xy(vertices, lat_col="y", lng_col="x")
666681

667-
tiles_in_geom = polygon_fill.fast_polygon_fill(vertices, unique_id_col)
682+
polygon_fill_result = polygon_fill.fast_polygon_fill(vertices, unique_id_col)
683+
tiles_in_geom = polygon_fill_result["tiles_in_geom"]
684+
685+
# this is error correction on the polygon boundary (not the square boundary)
686+
tiles_off_boundary = polygon_fill_result["tiles_off_boundary"]
687+
if not tiles_off_boundary.is_empty():
688+
off_boundary_bboxes = self._xy_to_bbox(tiles_off_boundary, "x", "y")
689+
all_polygon_boundary = aoi_gdf.boundary.union_all(method="unary")
690+
intersects_boundary_bool = off_boundary_bboxes.intersects(all_polygon_boundary)
691+
addtl_tiles_in_geom = tiles_off_boundary.filter(
692+
pl.Series(intersects_boundary_bool)
693+
)
694+
695+
tiles_in_geom = pl.concat([tiles_in_geom, addtl_tiles_in_geom])
668696

669697
quadkey_expr = self._xyz_to_quadkey(
670698
pl.col("x"),
@@ -675,6 +703,10 @@ def generate_grid(
675703
if self.return_geometry:
676704
bboxes = self._xy_to_bbox(tiles_in_geom, "x", "y")
677705

706+
if not tiles_off_boundary.is_empty():
707+
addtl_boundary_bboxes = off_boundary_bboxes[intersects_boundary_bool].copy()
708+
bboxes = pd.concat([bboxes, addtl_boundary_bboxes], ignore_index=True)
709+
678710
if not self.add_xyz_cols:
679711
tiles_in_geom = tiles_in_geom.drop(["x", "y"])
680712
else:

notebooks/00_grids.ipynb

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,9 @@
311311
"This uses these optimizations to speed up grid generation:\n",
312312
"\n",
313313
"1. Vectorized Translation Functions: Functions that translate between lat,lon and x,y are written in polars.\n",
314-
"2. Voxel Traversal and Scanline Fill Algorithms: Faster alternative to finding all pixels within a polygon without using polygon intersection operations. These are implemented in `polygon_fill.fast_polygon_fill()`"
314+
"2. Voxel Traversal and Scanline Fill Algorithms: Faster alternative to finding all pixels within a polygon without using polygon intersection operations. These are implemented in `polygon_fill.fast_polygon_fill()`\n",
315+
"\n",
316+
"This also does error correction on the polygon boundary using off-boundary pixels. Read more in the [polygon fill module reference](https://geowrangler.thinkingmachin.es/polygon_fill.html)"
315317
]
316318
},
317319
{
@@ -362,10 +364,23 @@
362364
" vertices = self._remove_out_of_bounds_polygons(vertices, boundary)\n",
363365
" vertices = self._northingeasting_to_xy(vertices, boundary, northing_col=\"y\", easting_col=\"x\")\n",
364366
" \n",
365-
" tiles_in_geom = polygon_fill.fast_polygon_fill(vertices, unique_id_col)\n",
366-
"\n",
367+
" polygon_fill_result = polygon_fill.fast_polygon_fill(vertices, unique_id_col)\n",
368+
" tiles_in_geom = polygon_fill_result[\"tiles_in_geom\"]\n",
367369
" bboxes = self._xy_to_bbox(tiles_in_geom, boundary, \"x\", \"y\")\n",
368370
"\n",
371+
" # this is error correction on the polygon boundary (not the square boundary) \n",
372+
" tiles_off_boundary = polygon_fill_result[\"tiles_off_boundary\"]\n",
373+
" if not tiles_off_boundary.is_empty():\n",
374+
" off_boundary_bboxes = self._xy_to_bbox(tiles_off_boundary, boundary, \"x\", \"y\") \n",
375+
" all_polygon_boundary = reprojected_gdf.boundary.union_all(method=\"unary\")\n",
376+
" intersects_boundary_bool = off_boundary_bboxes.intersects(all_polygon_boundary)\n",
377+
" \n",
378+
" addtl_tiles_in_geom = tiles_off_boundary.filter(pl.Series(intersects_boundary_bool))\n",
379+
" addtl_boundary_bboxes = off_boundary_bboxes[intersects_boundary_bool].copy()\n",
380+
" \n",
381+
" tiles_in_geom = pl.concat([tiles_in_geom, addtl_tiles_in_geom])\n",
382+
" bboxes = pd.concat([bboxes, addtl_boundary_bboxes], ignore_index=True)\n",
383+
"\n",
369384
" column_order = [\"x\",\"y\"]\n",
370385
" if unique_id_col is not None:\n",
371386
" column_order += [unique_id_col]\n",
@@ -854,7 +869,9 @@
854869
"This uses these optimizations to speed up grid generation:\n",
855870
"\n",
856871
"1. Vectorized Translation Functions: Functions that translate between lat,lon and x,y are written in polars.\n",
857-
"2. Voxel Traversal and Scanline Fill Algorithms: Faster alternative to finding all pixels within a polygon without using polygon intersection operations. These are implemented in `polygon_fill.fast_polygon_fill()`"
872+
"2. Voxel Traversal and Scanline Fill Algorithms: Faster alternative to finding all pixels within a polygon without using polygon intersection operations. These are implemented in `polygon_fill.fast_polygon_fill()`\n",
873+
"\n",
874+
"This also does error correction on the polygon boundary using off-boundary pixels. Read more in the [polygon fill module reference](https://geowrangler.thinkingmachin.es/polygon_fill.html)"
858875
]
859876
},
860877
{
@@ -902,7 +919,18 @@
902919
" vertices = polygon_fill.polygons_to_vertices(aoi_gdf, unique_id_col)\n",
903920
" vertices = self._latlng_to_xy(vertices, lat_col=\"y\", lng_col=\"x\")\n",
904921
"\n",
905-
" tiles_in_geom = polygon_fill.fast_polygon_fill(vertices, unique_id_col)\n",
922+
" polygon_fill_result = polygon_fill.fast_polygon_fill(vertices, unique_id_col)\n",
923+
" tiles_in_geom = polygon_fill_result[\"tiles_in_geom\"]\n",
924+
"\n",
925+
" # this is error correction on the polygon boundary (not the square boundary)\n",
926+
" tiles_off_boundary = polygon_fill_result[\"tiles_off_boundary\"]\n",
927+
" if not tiles_off_boundary.is_empty():\n",
928+
" off_boundary_bboxes = self._xy_to_bbox(tiles_off_boundary, \"x\", \"y\")\n",
929+
" all_polygon_boundary = aoi_gdf.boundary.union_all(method=\"unary\")\n",
930+
" intersects_boundary_bool = off_boundary_bboxes.intersects(all_polygon_boundary)\n",
931+
" addtl_tiles_in_geom = tiles_off_boundary.filter(pl.Series(intersects_boundary_bool))\n",
932+
" \n",
933+
" tiles_in_geom = pl.concat([tiles_in_geom, addtl_tiles_in_geom])\n",
906934
"\n",
907935
" quadkey_expr = self._xyz_to_quadkey(\n",
908936
" pl.col(\"x\"),\n",
@@ -912,6 +940,10 @@
912940
"\n",
913941
" if self.return_geometry:\n",
914942
" bboxes = self._xy_to_bbox(tiles_in_geom, \"x\", \"y\")\n",
943+
"\n",
944+
" if not tiles_off_boundary.is_empty():\n",
945+
" addtl_boundary_bboxes = off_boundary_bboxes[intersects_boundary_bool].copy()\n",
946+
" bboxes = pd.concat([bboxes, addtl_boundary_bboxes], ignore_index=True)\n",
915947
" \n",
916948
" if not self.add_xyz_cols:\n",
917949
" tiles_in_geom = tiles_in_geom.drop([\"x\",\"y\"])\n",

0 commit comments

Comments
 (0)