Skip to content

Commit 82d4798

Browse files
committed
Fix output grid computation for reproject (#1045)
Three fixes to _grid.py: - Resolution estimation now transforms each axis independently and uses the geometric mean for square pixels (was diagonal step) - Edge sampling increased from 21 to 101 points plus a 21x21 interior grid for better bounds coverage - ceil() replaced with round() for dimension calculation to prevent floating-point noise from adding spurious rows/columns
1 parent 52167a2 commit 82d4798

File tree

1 file changed

+32
-15
lines changed

1 file changed

+32
-15
lines changed

xrspatial/reproject/_grid.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,30 @@ def _compute_output_grid(source_bounds, source_shape, source_crs, target_crs,
5252
if src_bottom >= src_top:
5353
src_bottom, src_top = source_bounds[1], source_bounds[3]
5454

55-
n_edge = 21 # sample points along each edge
56-
xs = np.concatenate([
55+
# Sample edges densely plus an interior grid so that
56+
# projections with curvature (e.g. UTM near zone edges)
57+
# don't underestimate the output bounding box.
58+
n_edge = 101
59+
n_interior = 21
60+
edge_xs = np.concatenate([
5761
np.linspace(src_left, src_right, n_edge), # top edge
5862
np.linspace(src_left, src_right, n_edge), # bottom edge
5963
np.full(n_edge, src_left), # left edge
6064
np.full(n_edge, src_right), # right edge
6165
])
62-
ys = np.concatenate([
66+
edge_ys = np.concatenate([
6367
np.full(n_edge, src_top),
6468
np.full(n_edge, src_bottom),
6569
np.linspace(src_bottom, src_top, n_edge),
6670
np.linspace(src_bottom, src_top, n_edge),
6771
])
72+
# Interior grid catches cases where the projected extent
73+
# bulges beyond the edges (e.g. Mercator near the poles).
74+
ix = np.linspace(src_left, src_right, n_interior)
75+
iy = np.linspace(src_bottom, src_top, n_interior)
76+
ixx, iyy = np.meshgrid(ix, iy)
77+
xs = np.concatenate([edge_xs, ixx.ravel()])
78+
ys = np.concatenate([edge_ys, iyy.ravel()])
6879
tx, ty = transformer.transform(xs, ys)
6980
tx = np.asarray(tx)
7081
ty = np.asarray(ty)
@@ -131,29 +142,35 @@ def _compute_output_grid(source_bounds, source_shape, source_crs, target_crs,
131142
res_x = (right - left) / width
132143
res_y = (top - bottom) / height
133144
else:
134-
# Estimate from source resolution
145+
# Estimate from source resolution by transforming each axis
146+
# independently, then taking the geometric mean for a square pixel.
135147
src_h, src_w = source_shape
136148
src_left, src_bottom, src_right, src_top = source_bounds
137149
src_res_x = (src_right - src_left) / src_w
138150
src_res_y = (src_top - src_bottom) / src_h
139-
# Use the geometric mean of transformed pixel sizes
140151
center_x = (src_left + src_right) / 2
141152
center_y = (src_bottom + src_top) / 2
142-
tx1, ty1 = transformer.transform(center_x, center_y)
143-
tx2, ty2 = transformer.transform(
144-
center_x + src_res_x, center_y + src_res_y
145-
)
146-
res_x = abs(float(tx2) - float(tx1))
147-
res_y = abs(float(ty2) - float(ty1))
148-
if res_x == 0 or res_y == 0:
153+
tc_x, tc_y = transformer.transform(center_x, center_y)
154+
# Step along x only
155+
tx_x, tx_y = transformer.transform(center_x + src_res_x, center_y)
156+
dx = np.hypot(float(tx_x) - float(tc_x), float(tx_y) - float(tc_y))
157+
# Step along y only
158+
ty_x, ty_y = transformer.transform(center_x, center_y + src_res_y)
159+
dy = np.hypot(float(ty_x) - float(tc_x), float(ty_y) - float(tc_y))
160+
if dx == 0 or dy == 0:
149161
res_x = (right - left) / src_w
150162
res_y = (top - bottom) / src_h
163+
else:
164+
# Geometric mean for square pixels
165+
res_x = res_y = np.sqrt(dx * dy)
151166

152-
# Compute dimensions
167+
# Compute dimensions. Use round() instead of ceil() so that
168+
# floating-point noise (e.g. 677.0000000000001) does not add a
169+
# spurious extra row/column.
153170
if width is None:
154-
width = max(1, int(np.ceil((right - left) / res_x)))
171+
width = max(1, int(round((right - left) / res_x)))
155172
if height is None:
156-
height = max(1, int(np.ceil((top - bottom) / res_y)))
173+
height = max(1, int(round((top - bottom) / res_y)))
157174

158175
# Adjust bounds to be exact multiples of resolution
159176
right = left + width * res_x

0 commit comments

Comments
 (0)