Skip to content

Commit 1e69da3

Browse files
authored
Merge pull request #245 from joshuacortez/feat/scanline_gridding
`FastBingTileGenerator` for significantly speeding up grid generation
2 parents f47e063 + 165ab28 commit 1e69da3

File tree

11 files changed

+2234
-324
lines changed

11 files changed

+2234
-324
lines changed

geowrangler/_modidx.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@
109109
'geowrangler/distance_zonal_stats.py'),
110110
'geowrangler.distance_zonal_stats.create_distance_zonal_stats': ( 'distance_zonal_stats.html#create_distance_zonal_stats',
111111
'geowrangler/distance_zonal_stats.py')},
112+
'geowrangler.gridding_utils.polygon_fill': { 'geowrangler.gridding_utils.polygon_fill.interpolate_x': ( 'polygon_fill.html#interpolate_x',
113+
'geowrangler/gridding_utils/polygon_fill.py'),
114+
'geowrangler.gridding_utils.polygon_fill.scanline_fill': ( 'polygon_fill.html#scanline_fill',
115+
'geowrangler/gridding_utils/polygon_fill.py'),
116+
'geowrangler.gridding_utils.polygon_fill.voxel_traversal_2d': ( 'polygon_fill.html#voxel_traversal_2d',
117+
'geowrangler/gridding_utils/polygon_fill.py'),
118+
'geowrangler.gridding_utils.polygon_fill.voxel_traversal_scanline_fill': ( 'polygon_fill.html#voxel_traversal_scanline_fill',
119+
'geowrangler/gridding_utils/polygon_fill.py')},
112120
'geowrangler.grids': { 'geowrangler.grids.BingTileGridGenerator': ('grids.html#bingtilegridgenerator', 'geowrangler/grids.py'),
113121
'geowrangler.grids.BingTileGridGenerator.__init__': ( 'grids.html#bingtilegridgenerator.__init__',
114122
'geowrangler/grids.py'),
@@ -122,6 +130,28 @@
122130
'geowrangler/grids.py'),
123131
'geowrangler.grids.BingTileGridGenerator.tile_to_polygon': ( 'grids.html#bingtilegridgenerator.tile_to_polygon',
124132
'geowrangler/grids.py'),
133+
'geowrangler.grids.FastBingTileGridGenerator': ( 'grids.html#fastbingtilegridgenerator',
134+
'geowrangler/grids.py'),
135+
'geowrangler.grids.FastBingTileGridGenerator.__init__': ( 'grids.html#fastbingtilegridgenerator.__init__',
136+
'geowrangler/grids.py'),
137+
'geowrangler.grids.FastBingTileGridGenerator._lat_to_ytile': ( 'grids.html#fastbingtilegridgenerator._lat_to_ytile',
138+
'geowrangler/grids.py'),
139+
'geowrangler.grids.FastBingTileGridGenerator._latlng_to_xy': ( 'grids.html#fastbingtilegridgenerator._latlng_to_xy',
140+
'geowrangler/grids.py'),
141+
'geowrangler.grids.FastBingTileGridGenerator._lng_to_xtile': ( 'grids.html#fastbingtilegridgenerator._lng_to_xtile',
142+
'geowrangler/grids.py'),
143+
'geowrangler.grids.FastBingTileGridGenerator._polygons_to_vertices': ( 'grids.html#fastbingtilegridgenerator._polygons_to_vertices',
144+
'geowrangler/grids.py'),
145+
'geowrangler.grids.FastBingTileGridGenerator._xtile_to_lng': ( 'grids.html#fastbingtilegridgenerator._xtile_to_lng',
146+
'geowrangler/grids.py'),
147+
'geowrangler.grids.FastBingTileGridGenerator._xy_to_bbox': ( 'grids.html#fastbingtilegridgenerator._xy_to_bbox',
148+
'geowrangler/grids.py'),
149+
'geowrangler.grids.FastBingTileGridGenerator._xyz_to_quadkey': ( 'grids.html#fastbingtilegridgenerator._xyz_to_quadkey',
150+
'geowrangler/grids.py'),
151+
'geowrangler.grids.FastBingTileGridGenerator._ytile_to_lat': ( 'grids.html#fastbingtilegridgenerator._ytile_to_lat',
152+
'geowrangler/grids.py'),
153+
'geowrangler.grids.FastBingTileGridGenerator.generate_grid': ( 'grids.html#fastbingtilegridgenerator.generate_grid',
154+
'geowrangler/grids.py'),
125155
'geowrangler.grids.H3GridGenerator': ('grids.html#h3gridgenerator', 'geowrangler/grids.py'),
126156
'geowrangler.grids.H3GridGenerator.__init__': ( 'grids.html#h3gridgenerator.__init__',
127157
'geowrangler/grids.py'),
@@ -165,7 +195,6 @@
165195
'geowrangler/raster_zonal_stats.py')},
166196
'geowrangler.spatialjoin_highest_intersection': { 'geowrangler.spatialjoin_highest_intersection.get_highest_intersection': ( 'spatialjoin_highest_intersection.html#get_highest_intersection',
167197
'geowrangler/spatialjoin_highest_intersection.py')},
168-
'geowrangler.test_module': {},
169198
'geowrangler.tile_clustering': { 'geowrangler.tile_clustering.TileClustering': ( 'tile_clustering.html#tileclustering',
170199
'geowrangler/tile_clustering.py'),
171200
'geowrangler.tile_clustering.TileClustering.__init__': ( 'tile_clustering.html#tileclustering.__init__',

geowrangler/gridding_utils/__init__.py

Whitespace-only changes.
+261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../notebooks/15_polygon_fill.ipynb.
2+
3+
# %% auto 0
4+
__all__ = ['voxel_traversal_2d', 'scanline_fill', 'voxel_traversal_scanline_fill']
5+
6+
# %% ../../notebooks/15_polygon_fill.ipynb 5
7+
from typing import List, Tuple, Set, Optional, Dict, Union
8+
9+
import numpy as np
10+
import pandas as pd
11+
import polars as pl
12+
13+
# %% ../../notebooks/15_polygon_fill.ipynb 11
14+
def voxel_traversal_2d(
15+
start_vertex: Tuple[int, int],
16+
end_vertex: Tuple[int, int],
17+
debug: bool = False, # if true, prints diagnostic info for the algorithm
18+
) -> List[Tuple[int, int]]:
19+
"""Returns all pixels between two points as inspired by Amanatides & Woo's “A Fast Voxel Traversal Algorithm For Ray Tracing”"""
20+
21+
# epsilon is a constant for correcting near-misses in voxel traversal
22+
EPSILON = 1e-14
23+
24+
# Setup initial conditions
25+
x1, y1 = start_vertex
26+
x2, y2 = end_vertex
27+
28+
direction_x = 1 if x2 > x1 else -1
29+
direction_y = 1 if y2 > y1 else -1
30+
31+
# Single point
32+
if (x1 == x2) and (y1 == y2):
33+
pixels = [(x1, y1)]
34+
return pixels
35+
36+
# Vertical line
37+
elif x1 == x2:
38+
pixels = [(x1, y) for y in range(y1, y2 + direction_y, direction_y)]
39+
return pixels
40+
41+
# Horizontal line
42+
elif y1 == y2:
43+
pixels = [(x, y1) for x in range(x1, x2 + direction_x, direction_x)]
44+
return pixels
45+
46+
dy = y2 - y1
47+
dx = x2 - x1
48+
slope = dy / dx
49+
inv_slope = dx / dy
50+
51+
# reverse order if negative slope to preserve symmetry in floating point calculations
52+
if slope < 0:
53+
x1, y1 = end_vertex
54+
x2, y2 = start_vertex
55+
56+
direction_x = 1 if x2 > x1 else -1
57+
direction_y = 1 if y2 > y1 else -1
58+
59+
slope_multiplier = np.sqrt(1 + slope**2)
60+
inv_slope_multiplier = np.sqrt(1 + inv_slope**2)
61+
62+
pixel_x, pixel_y = x1, y1
63+
ray_x, ray_y = pixel_x, pixel_y
64+
pixels = [(pixel_x, pixel_y)]
65+
66+
is_finished = False
67+
68+
if debug:
69+
print(f"\nTraversing from ({x1},{y1}) to ({x2},{y2})")
70+
71+
# number of steps should not exceed the perimeter of the rectangle enclosing the line segment
72+
max_steps = 2 * (abs(dx) + abs(dy))
73+
n_steps = 0
74+
while not is_finished:
75+
# this prevents infinite loops
76+
n_steps += 1
77+
if n_steps > max_steps:
78+
raise Exception(
79+
f"Traversal has exceeded steps limit {max_steps:,}. Please recheck inputs"
80+
)
81+
82+
# get the next x or y integer that the next ray would hit
83+
if direction_x == 1:
84+
next_ray_x = np.floor(ray_x) + 1
85+
elif direction_x == -1:
86+
next_ray_x = np.ceil(ray_x) - 1
87+
88+
if direction_y == 1:
89+
next_ray_y = np.floor(ray_y) + 1
90+
elif direction_y == -1:
91+
next_ray_y = np.ceil(ray_y) - 1
92+
93+
# get distance between the 2 candidates and check which one is closer
94+
# there is an epsilon to account near-misses due to floating point differences
95+
96+
# y coordinate line formula is next_ray_y = ray_y + slope*(next_ray_x-ray_x)
97+
# squred distance is (next_ray_x - ray_x)**2 + (slope*(next_ray_x-ray_x))**2
98+
# distance simplifies to abs(next_ray_x - ray_x)* sqrt(1+slope**2)
99+
100+
ray_candidate_1 = (
101+
next_ray_x,
102+
ray_y + slope * (next_ray_x - ray_x) + direction_y * EPSILON,
103+
)
104+
# unsimplified square distance
105+
# dist_1 = (ray_candidate_1[0] - ray_x)**2 + (ray_candidate_1[1] - ray_y)**2
106+
# simplified distance
107+
dist_1 = abs(next_ray_x - ray_x) * slope_multiplier
108+
109+
# x coordinate line formula is next_ray_x = ray_x + inv_slope*(next_ray_y-y)
110+
# squared distance is (inv_slope*(next_ray_y-ray_y))**2 + (next_ray_y-ray_y)**2
111+
# distance simplifies to abs(next_ray_y-ray_y)* sqrt(1 + inv_slope**2)
112+
113+
ray_candidate_2 = (
114+
ray_x + inv_slope * (next_ray_y - ray_y) + direction_x * EPSILON,
115+
next_ray_y,
116+
)
117+
# unsimplified square distance
118+
# dist_2 = (ray_candidate_2[0] - ray_x)**2 + (ray_candidate_2[1] - ray_y)**2
119+
# simplified distance
120+
dist_2 = abs(next_ray_y - ray_y) * inv_slope_multiplier
121+
122+
# candidate 1 is closer
123+
if dist_1 < dist_2:
124+
pixel_x += direction_x
125+
ray_x, ray_y = ray_candidate_1
126+
127+
# candidate 2 is closer
128+
elif dist_1 > dist_2:
129+
pixel_y += direction_y
130+
ray_x, ray_y = ray_candidate_2
131+
132+
# line passes exactly on the corner
133+
elif dist_1 == dist_2:
134+
pixel_x += direction_x
135+
pixel_y += direction_y
136+
ray_x, ray_y = pixel_x, pixel_y
137+
else:
138+
raise ValueError(f"Erroneous distances {dist_1}, {dist_2}")
139+
140+
if debug:
141+
print(
142+
f"Next ray coords are ({ray_x}, {ray_y}) and tile coords are ({pixel_x}, {pixel_y})"
143+
)
144+
145+
pixels.append((pixel_x, pixel_y))
146+
147+
# checks to see if the loop is finished
148+
if direction_x == 1:
149+
is_x_finished = pixel_x >= x2
150+
elif direction_x == -1:
151+
is_x_finished = pixel_x <= x2
152+
153+
if direction_y == 1:
154+
is_y_finished = pixel_y >= y2
155+
elif direction_y == -1:
156+
is_y_finished = pixel_y <= y2
157+
158+
if is_x_finished and is_y_finished:
159+
break
160+
161+
return pixels
162+
163+
# %% ../../notebooks/15_polygon_fill.ipynb 15
164+
def interpolate_x(
165+
start_vertex: Tuple[int, int],
166+
end_vertex: Tuple[int, int],
167+
y: int,
168+
) -> float:
169+
"""Interpolate x value for a given y along the line segment defined by start_vertex and end_vertex."""
170+
x1, y1 = start_vertex
171+
x2, y2 = end_vertex
172+
if y1 == y2:
173+
# case when there is a horizontal line segment
174+
raise ValueError(f"The y value of the 2 vertices should not be the same")
175+
176+
inverse_slope = (x2 - x1) / (y2 - y1)
177+
interpolated_x = x1 + (y - y1) * inverse_slope
178+
return interpolated_x
179+
180+
# %% ../../notebooks/15_polygon_fill.ipynb 16
181+
def scanline_fill(
182+
vertices: List[
183+
Tuple[int, int]
184+
], # list of polygon vertices in order (either clockwise or counterclockwise)
185+
debug: bool = False, # if true, prints diagnostic info for the algorithm
186+
) -> Set[Tuple[int, int]]:
187+
"""Returns all pixels within the interior of a polygon defined by vertices"""
188+
189+
offset_vertices = vertices[1:] + vertices[:1]
190+
191+
if not vertices:
192+
return set()
193+
194+
if len(vertices) == 1:
195+
return set(vertices)
196+
197+
# Calculate the bounding box for the polygon
198+
min_y, max_y = min(y for x, y in vertices), max(y for x, y in vertices)
199+
200+
filled_pixels = set()
201+
# Process each horizontal scanline within the bounding box
202+
for scanline_y in range(min_y, max_y + 1):
203+
intersection_points = []
204+
205+
# Find intersections of the polygon with the current scanline
206+
for start_vertex, end_vertex in zip(vertices, offset_vertices):
207+
start_x, start_y = start_vertex
208+
end_x, end_y = end_vertex
209+
210+
if (end_y < scanline_y <= start_y) or (start_y < scanline_y <= end_y):
211+
# Calculate x-coordinate of intersection
212+
intersection_x = interpolate_x(start_vertex, end_vertex, scanline_y)
213+
intersection_points.append(intersection_x)
214+
215+
# Fill pixels between pairs of intersections
216+
if intersection_points:
217+
intersection_points.sort()
218+
219+
filled_pixels_in_row = set()
220+
for start_x, end_x in zip(
221+
intersection_points[::2], intersection_points[1::2]
222+
):
223+
start_x, end_x = int(round(start_x)), int(round(end_x))
224+
225+
_filled_pixels_in_row = [
226+
(x, scanline_y) for x in range(start_x, end_x + 1)
227+
]
228+
filled_pixels_in_row.update(_filled_pixels_in_row)
229+
230+
filled_pixels.update(filled_pixels_in_row)
231+
232+
if debug:
233+
print(f"Scanline y = {scanline_y}, Intersections: {intersection_points}")
234+
235+
return filled_pixels
236+
237+
# %% ../../notebooks/15_polygon_fill.ipynb 20
238+
def voxel_traversal_scanline_fill(
239+
vertices_df: Union[
240+
pd.DataFrame, pl.DataFrame
241+
], # dataframe with x_col and y_col for the polygon vertices
242+
x_col: str = "x",
243+
y_col: str = "y",
244+
debug: bool = False, # if true, prints diagnostic info for both voxel traversal and scanline fill algorithms
245+
) -> Set[Tuple[int, int]]:
246+
"""
247+
Returns pixels that intersect a polygon
248+
This uses voxel traversal to fill the boundary, and scanline fill for the interior. All coordinates are assumed to be nonnegative integers
249+
"""
250+
251+
vertices = list(zip(vertices_df[x_col].to_list(), vertices_df[y_col].to_list()))
252+
offset_vertices = vertices[1:] + vertices[:1]
253+
254+
polygon_pixels = set()
255+
256+
for start_vertex, end_vertex in zip(vertices, offset_vertices):
257+
polygon_pixels.update(voxel_traversal_2d(start_vertex, end_vertex, debug))
258+
259+
polygon_pixels.update(scanline_fill(vertices, debug))
260+
261+
return polygon_pixels

0 commit comments

Comments
 (0)