File size: 6,810 Bytes
0e9faee ab75598 6eaf92d ab75598 55b311e ab75598 0e9faee ab75598 a668f53 ab75598 a668f53 ab75598 a668f53 00d5206 55b311e 0e9faee 55b311e 0e9faee 55b311e 0e9faee 55b311e ab75598 0e9faee ab75598 55b311e 0e9faee 55b311e 0e9faee 55b311e ab75598 0e9faee ab75598 0e9faee ab75598 0e9faee 55b311e ab75598 55b311e 0e9faee 55b311e ab75598 55b311e 0e9faee 55b311e 0e9faee 55b311e 0e9faee 55b311e ab75598 55b311e ab75598 0b75be1 ab75598 0b75be1 55b311e 0b75be1 ab75598 0b75be1 |
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 |
# app.py (versión con logging, manejo de TileOutsideBounds y endpoint debug hillshade)
import io
import os
import logging
import numpy as np
from fastapi import FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
from rio_tiler.io import COGReader
from rio_tiler.errors import TileOutsideBounds
from PIL import Image
COG_URL = os.environ.get("COG_URL", "https://huggingface.co/datasets/Pingul/mexico-dem/resolve/main/Mexico_DEM_COG.tif")
# COG_URL = os.environ.get("COG_URL", "https://huggingface.co/datasets/Pingul/mexico-dem/resolve/main/CEM_15m_COG.tif")
TILE_SIZE = 256
# Logging simple
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("dem-tiler")
app = FastAPI(title="Mexico DEM TerrainRGB API (debuggable)")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def encode_terrainrgb(arr: np.ndarray) -> np.ndarray:
"""
Convierte elevación en metros a Terrain-RGB (Mapbox spec).
"""
arr = np.array(arr, dtype=np.float32)
arr = np.clip(arr, -10000, 9000)
arr = (arr + 10000.0) * 10.0 # mapbox scale
R = np.floor(arr / (256.0 * 256.0))
G = np.floor((arr - R * 256.0 * 256.0) / 256.0)
B = np.floor(arr - R * 256.0 * 256.0 - G * 256.0)
rgb = np.stack([R, G, B], axis=-1).astype(np.uint8)
return rgb
def elev_to_grayscale(arr: np.ndarray) -> np.ndarray:
"""
Convierte elevación a imagen grayscale para debug: mapea min->0 max->255.
NaNs se convierten a 0 (transparente later).
"""
a = np.array(arr, dtype=np.float32)
a = np.where(np.isfinite(a), a, np.nan)
# clamp to sensible range (optional)
vmin = np.nanpercentile(a, 2) if np.isfinite(np.nanpercentile(a, 2)) else np.nanmin(a)
vmax = np.nanpercentile(a, 98) if np.isfinite(np.nanpercentile(a, 98)) else np.nanmax(a)
if np.isnan(vmin) or np.isnan(vmax) or vmin == vmax:
vmin, vmax = np.nanmin(a), np.nanmax(a)
# handle edge cases
if np.isnan(vmin): vmin = 0.0
if np.isnan(vmax): vmax = vmin + 1.0
norm = (a - vmin) / (vmax - vmin)
norm = np.clip(norm, 0.0, 1.0)
img = (norm * 255).astype(np.uint8)
# convert to RGB
rgb = np.stack([img, img, img], axis=-1)
return rgb
@app.get("/")
def root():
return {"message": "Mexico DEM TerrainRGB API running", "cog_url": COG_URL}
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/info")
def get_info():
try:
with COGReader(COG_URL) as cog:
meta = cog.info()
# return a small, JSON-serializable subset
out = {
"bounds": meta.bounds, # [left, bottom, right, top] in CRS of dataset (EPSG:3857)
"minzoom": meta.minzoom,
"maxzoom": meta.maxzoom,
"band_descriptions": meta.band_descriptions,
"statistics": {
"min": getattr(meta, "min", None),
"max": getattr(meta, "max", None)
},
"crs": str(meta.crs)
}
return out
except Exception as e:
logger.exception("Error reading COG info")
return {"error": str(e)}
@app.get("/tiles/{z}/{x}/{y}.png")
def get_tile(z: int, x: int, y: int):
"""
Tile endpoint: devuelve Terrain-RGB PNG (Mapbox spec).
Si el tile está fuera de bounds, devuelve PNG transparente.
"""
try:
with COGReader(COG_URL) as cog:
data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
elev = data[0].astype(np.float32)
# Replace nodata values with NaN if mask indicates no-data
if mask is not None:
# rio-tiler mask: 255 valid, 0 invalid (varies), handle robustly
invalid = (mask == 0)
elev = elev.astype(np.float32)
elev[invalid] = np.nan
# Handle all-nodata -> return transparent
if np.all(np.isnan(elev)):
raise TileOutsideBounds(f"All NaN for tile {z}/{x}/{y}")
rgb = encode_terrainrgb(np.nan_to_num(elev, nan=-10000.0))
img = Image.fromarray(rgb, "RGB")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return Response(buf.getvalue(), media_type="image/png")
except TileOutsideBounds:
# tile fuera de bounds: PNG transparente
blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
buf = io.BytesIO()
blank.save(buf, format="PNG")
buf.seek(0)
return Response(buf.getvalue(), media_type="image/png")
except Exception as e:
logger.exception(f"Unexpected error generating tile {z}/{x}/{y}")
# Devuelve 500 con mensaje corto (no stacktrace)
return Response(status_code=500, content=f"Server error generating tile: {str(e)}")
@app.get("/debug/{z}/{x}/{y}.png")
def debug_tile(z: int, x: int, y: int):
"""
Endpoint de debug: devuelve una imagen en escala de grises (hillshade-like) para ver visualmente
si la tile solicitada coincide con tu DEM.
"""
try:
with COGReader(COG_URL) as cog:
data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
elev = data[0].astype(np.float32)
# mask nodata
if mask is not None:
elev[mask == 0] = np.nan
if np.all(np.isnan(elev)):
raise TileOutsideBounds()
# convert elevation to grayscale RGB for debug
rgb = elev_to_grayscale(elev)
# if any nodata, set those pixels transparent by returning RGBA
alpha = np.where(np.isfinite(elev), 255, 0).astype(np.uint8)
rgba = np.dstack([rgb, alpha])
img = Image.fromarray(rgba, "RGBA")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return Response(buf.getvalue(), media_type="image/png")
except TileOutsideBounds:
blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
buf = io.BytesIO()
blank.save(buf, format="PNG")
buf.seek(0)
return Response(buf.getvalue(), media_type="image/png")
except Exception as e:
logger.exception("Error in debug_tile")
return Response(status_code=500, content=f"Error: {e}")
@app.get("/point")
def get_point(lon: float, lat: float):
try:
with COGReader(COG_URL) as cog:
val = cog.point(lon, lat)
elevation = float(val.data[0])
return {"lon": lon, "lat": lat, "elevation_m": elevation}
except Exception as e:
logger.exception("Error sampling point")
return Response(status_code=500, content=f"Error sampling point: {e}") |