Skip to content

Commit 11ff33f

Browse files
chrislyonsKYclaude
andcommitted
Fix read() bbox CRS default and add example output images
Bug fix: read() now defaults bbox CRS to EPSG:4326 per project convention. Previously, passing a 4326 bbox without explicit crs= would fail against EPSG:3089 COGs (empty window intersection). Generated output images from live KyFromAbove data: - stream_window.png: DEM elevation window, Frankfort - compare_dem_phases.png: Phase 1 vs Phase 3 side-by-side - search_results_map.png: 342 tile boundaries, Franklin County - ortho_rgb.png: 3-inch orthoimagery, KY State Capitol Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2c57ba1 commit 11ff33f

6 files changed

Lines changed: 172 additions & 4 deletions

File tree

391 KB
Loading

examples/output/ortho_rgb.png

1.76 MB
Loading
112 KB
Loading

examples/output/stream_window.png

195 KB
Loading
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Generate output PNG images for all examples.
2+
3+
Produces sample images that are checked into the repo for documentation.
4+
Requires: pip install abovepy[all] matplotlib
5+
6+
Usage:
7+
python generate_images.py
8+
"""
9+
10+
from pathlib import Path
11+
12+
import numpy as np
13+
14+
import abovepy
15+
16+
OUTPUT = Path(__file__).parent.parent / "output"
17+
OUTPUT.mkdir(parents=True, exist_ok=True)
18+
19+
20+
def generate_stream_window():
21+
"""Stream a DEM window and save elevation plot."""
22+
import matplotlib.pyplot as plt
23+
24+
print("=== Stream Window ===")
25+
tiles = abovepy.search(
26+
bbox=(-84.85, 38.18, -84.82, 38.21),
27+
product="dem_phase3",
28+
)
29+
url = tiles.iloc[0].asset_url
30+
data, profile = abovepy.read(
31+
url, bbox=(-84.85, 38.18, -84.82, 38.21)
32+
)
33+
print(f" Shape: {data.shape}, range: {data.min():.0f}-{data.max():.0f}")
34+
35+
fig, ax = plt.subplots(figsize=(10, 4))
36+
im = ax.imshow(data.squeeze(), cmap="terrain")
37+
fig.colorbar(im, ax=ax, label="Elevation (ft)")
38+
ax.set_title("Streamed DEM Window — Frankfort, KY")
39+
ax.axis("off")
40+
plt.tight_layout()
41+
path = OUTPUT / "stream_window.png"
42+
plt.savefig(str(path), dpi=150, bbox_inches="tight")
43+
plt.close()
44+
print(f" Saved: {path}")
45+
46+
47+
def generate_ortho_rgb():
48+
"""Extract ortho RGB and save composite."""
49+
import matplotlib.pyplot as plt
50+
51+
print("=== Ortho RGB Extract ===")
52+
tiles = abovepy.search(
53+
bbox=(-84.876, 38.196, -84.872, 38.200),
54+
product="ortho_phase3",
55+
)
56+
if len(tiles) == 0:
57+
print(" No ortho tiles found, skipping")
58+
return
59+
60+
url = tiles.iloc[0].asset_url
61+
data, profile = abovepy.read(
62+
url, bbox=(-84.876, 38.196, -84.872, 38.200)
63+
)
64+
print(f" Shape: {data.shape}")
65+
66+
if data.shape[0] >= 3:
67+
rgb = np.moveaxis(data[:3], 0, -1)
68+
fig, ax = plt.subplots(figsize=(8, 8))
69+
ax.imshow(rgb)
70+
ax.set_title("Ortho Phase 3 — KY State Capitol")
71+
ax.axis("off")
72+
plt.tight_layout()
73+
path = OUTPUT / "ortho_rgb.png"
74+
plt.savefig(str(path), dpi=150, bbox_inches="tight")
75+
plt.close()
76+
print(f" Saved: {path}")
77+
78+
79+
def generate_compare_phases():
80+
"""Compare DEM phases side by side."""
81+
import matplotlib.pyplot as plt
82+
83+
print("=== Compare DEM Phases ===")
84+
bbox = (-84.88, 38.18, -84.86, 38.20)
85+
86+
tiles1 = abovepy.search(bbox=bbox, product="dem_phase1")
87+
tiles3 = abovepy.search(bbox=bbox, product="dem_phase3")
88+
89+
if len(tiles1) == 0 or len(tiles3) == 0:
90+
print(" Missing tiles, skipping")
91+
return
92+
93+
data1, _ = abovepy.read(
94+
tiles1.iloc[0].asset_url, bbox=bbox
95+
)
96+
data3, _ = abovepy.read(
97+
tiles3.iloc[0].asset_url, bbox=bbox
98+
)
99+
print(f" Phase 1: {data1.shape}, Phase 3: {data3.shape}")
100+
101+
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
102+
vmin = min(data1.min(), data3.min())
103+
vmax = max(data1.max(), data3.max())
104+
105+
axes[0].imshow(
106+
data1.squeeze(), cmap="terrain", vmin=vmin, vmax=vmax
107+
)
108+
axes[0].set_title(f"DEM Phase 1 (5ft) — {data1.shape}")
109+
axes[0].axis("off")
110+
111+
im = axes[1].imshow(
112+
data3.squeeze(), cmap="terrain", vmin=vmin, vmax=vmax
113+
)
114+
axes[1].set_title(f"DEM Phase 3 (2ft) — {data3.shape}")
115+
axes[1].axis("off")
116+
117+
fig.colorbar(im, ax=axes, label="Elevation (ft)", shrink=0.8)
118+
plt.suptitle("DEM Phase Comparison — Frankfort, KY")
119+
plt.tight_layout()
120+
path = OUTPUT / "compare_dem_phases.png"
121+
plt.savefig(str(path), dpi=150, bbox_inches="tight")
122+
plt.close()
123+
print(f" Saved: {path}")
124+
125+
126+
def generate_search_map():
127+
"""Plot search results tile boundaries."""
128+
import matplotlib.pyplot as plt
129+
130+
print("=== Search Results Map ===")
131+
tiles = abovepy.search(
132+
county="Franklin", product="dem_phase3"
133+
)
134+
print(f" Found {len(tiles)} tiles")
135+
136+
fig, ax = plt.subplots(figsize=(10, 8))
137+
tiles.plot(
138+
ax=ax,
139+
edgecolor="teal",
140+
facecolor="teal",
141+
alpha=0.3,
142+
linewidth=0.5,
143+
)
144+
ax.set_title(
145+
f"Franklin County — {len(tiles)} DEM Phase 3 Tiles"
146+
)
147+
ax.set_xlabel("Longitude")
148+
ax.set_ylabel("Latitude")
149+
plt.tight_layout()
150+
path = OUTPUT / "search_results_map.png"
151+
plt.savefig(str(path), dpi=150, bbox_inches="tight")
152+
plt.close()
153+
print(f" Saved: {path}")
154+
155+
156+
if __name__ == "__main__":
157+
import matplotlib
158+
matplotlib.use("Agg")
159+
160+
generate_stream_window()
161+
generate_ortho_rgb()
162+
generate_compare_phases()
163+
generate_search_map()
164+
print("\nDone! All images saved to examples/output/")

src/abovepy/io/cog.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def read_cog(
2626
bbox : tuple, optional
2727
Bounding box (xmin, ymin, xmax, ymax) for windowed read.
2828
crs : str, optional
29-
CRS of the bbox. If different from the source CRS, the bbox
30-
is reprojected before windowing. Default None (use source CRS).
29+
CRS of the bbox. Defaults to EPSG:4326 per project convention.
30+
If different from the source CRS, the bbox is reprojected.
3131
3232
Returns
3333
-------
@@ -42,8 +42,12 @@ def read_cog(
4242
with rasterio.open(vsi_path) as src:
4343
if bbox is not None:
4444
read_bbox = bbox
45-
if crs is not None and str(src.crs) != crs:
46-
read_bbox = _reproject_bbox(bbox, crs, str(src.crs))
45+
# Default to EPSG:4326 per project convention
46+
bbox_crs = crs or "EPSG:4326"
47+
if str(src.crs) != bbox_crs:
48+
read_bbox = _reproject_bbox(
49+
bbox, bbox_crs, str(src.crs)
50+
)
4751

4852
window = from_bounds(*read_bbox, transform=src.transform)
4953
# Clamp window to dataset bounds

0 commit comments

Comments
 (0)