Pingul commited on
Commit
55b311e
·
verified ·
1 Parent(s): a668f53

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -124
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py (versión optimizada - ARREGLADA)
2
  import io
3
  import os
4
  import logging
@@ -12,12 +12,12 @@ from PIL import Image
12
 
13
  COG_URL = os.environ.get("COG_URL", "https://huggingface.co/datasets/Pingul/mexico-dem/resolve/main/Mexico_DEM_COG.tif")
14
  TILE_SIZE = 256
15
- CACHE_SIZE = 512 # Cachear últimas 512 tiles
16
 
 
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("dem-tiler")
19
 
20
- app = FastAPI(title="Mexico DEM TerrainRGB API (optimizado)")
21
 
22
  app.add_middleware(
23
  CORSMiddleware,
@@ -27,21 +27,6 @@ app.add_middleware(
27
  allow_headers=["*"],
28
  )
29
 
30
- # ============================================================================
31
- # OPTIMIZACIÓN 1: COGReader global reutilizable
32
- # ============================================================================
33
- _cog_reader = None
34
-
35
- def get_cog_reader():
36
- """Reutiliza la misma conexión COG en lugar de abrir/cerrar cada vez"""
37
- global _cog_reader
38
- if _cog_reader is None:
39
- _cog_reader = COGReader(COG_URL)
40
- return _cog_reader
41
-
42
- # ============================================================================
43
- # OPTIMIZACIÓN 2: Encoding Terrain-RGB (tu versión original, funciona bien)
44
- # ============================================================================
45
  def encode_terrainrgb(arr: np.ndarray) -> np.ndarray:
46
  """
47
  Convierte elevación en metros a Terrain-RGB (Mapbox spec).
@@ -57,55 +42,65 @@ def encode_terrainrgb(arr: np.ndarray) -> np.ndarray:
57
  rgb = np.stack([R, G, B], axis=-1).astype(np.uint8)
58
  return rgb
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  # ============================================================================
61
- # OPTIMIZACIÓN 3: Cache LRU para tiles
62
  # ============================================================================
63
- @lru_cache(maxsize=CACHE_SIZE)
64
- def get_tile_cached(z: int, x: int, y: int) -> bytes:
65
  """
66
- Cachea tiles generadas. Retorna bytes directamente.
67
- LRU cache guarda las tiles más solicitadas en memoria.
68
  """
69
  try:
70
- cog = get_cog_reader()
71
- data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
72
- elev = data[0].astype(np.float32)
73
-
74
- # Aplicar mask si existe
75
- if mask is not None:
76
- invalid = (mask == 0)
77
- elev[invalid] = np.nan
78
-
79
- # Check si todo es nodata
80
- if np.all(np.isnan(elev)):
81
- raise TileOutsideBounds(f"All NaN for tile {z}/{x}/{y}")
82
-
83
- # Reemplazar NaN con valor base
84
- elev = np.nan_to_num(elev, nan=-10000.0)
85
-
86
- # Encode a Terrain-RGB (tu función original)
87
- rgb = encode_terrainrgb(elev)
88
-
89
- # Convertir a PNG con compresión rápida
90
- img = Image.fromarray(rgb, "RGB")
91
- buf = io.BytesIO()
92
- img.save(buf, format="PNG", compress_level=1) # compress_level=1 es clave
93
-
94
- return buf.getvalue()
95
 
96
  except TileOutsideBounds:
97
- # Tile transparente (cacheable también)
98
  blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
99
  buf = io.BytesIO()
100
  blank.save(buf, format="PNG", compress_level=1)
101
  return buf.getvalue()
102
 
103
  # ============================================================================
104
- # ENDPOINTS
105
  # ============================================================================
106
  @app.get("/")
107
  def root():
108
- return {"message": "Mexico DEM TerrainRGB API (optimizado)", "cog_url": COG_URL}
109
 
110
  @app.get("/health")
111
  def health():
@@ -113,8 +108,8 @@ def health():
113
 
114
  @app.get("/cache/stats")
115
  def cache_stats():
116
- """Endpoint para ver estadísticas del cache"""
117
- info = get_tile_cached.cache_info()
118
  return {
119
  "hits": info.hits,
120
  "misses": info.misses,
@@ -123,29 +118,23 @@ def cache_stats():
123
  "hit_rate": f"{info.hits / (info.hits + info.misses) * 100:.1f}%" if (info.hits + info.misses) > 0 else "0%"
124
  }
125
 
126
- @app.post("/cache/clear")
127
- def clear_cache():
128
- """Limpia el cache de tiles"""
129
- get_tile_cached.cache_clear()
130
- return {"status": "cache cleared"}
131
-
132
  @app.get("/info")
133
  def get_info():
134
  try:
135
- cog = get_cog_reader()
136
- meta = cog.info()
137
- out = {
138
- "bounds": meta.bounds,
139
- "minzoom": meta.minzoom,
140
- "maxzoom": meta.maxzoom,
141
- "band_descriptions": meta.band_descriptions,
142
- "statistics": {
143
- "min": getattr(meta, "min", None),
144
- "max": getattr(meta, "max", None)
145
- },
146
- "crs": str(meta.crs)
147
- }
148
- return out
149
  except Exception as e:
150
  logger.exception("Error reading COG info")
151
  return {"error": str(e)}
@@ -153,57 +142,43 @@ def get_info():
153
  @app.get("/tiles/{z}/{x}/{y}.png")
154
  def get_tile(z: int, x: int, y: int):
155
  """
156
- Tile endpoint optimizado con cache.
157
  """
158
  try:
159
- tile_bytes = get_tile_cached(z, x, y)
160
  return Response(tile_bytes, media_type="image/png")
161
  except Exception as e:
162
  logger.exception(f"Unexpected error generating tile {z}/{x}/{y}")
163
- return Response(status_code=500, content=f"Server error: {str(e)}")
164
 
165
  @app.get("/debug/{z}/{x}/{y}.png")
166
  def debug_tile(z: int, x: int, y: int):
167
- """Endpoint de debug (sin cache para ver cambios en tiempo real)"""
 
 
168
  try:
169
- cog = get_cog_reader()
170
- data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
171
- elev = data[0].astype(np.float32)
172
-
173
- if mask is not None:
174
- elev[mask == 0] = np.nan
175
-
176
- if np.all(np.isnan(elev)):
177
- raise TileOutsideBounds()
178
-
179
- # Grayscale simple
180
- a = np.where(np.isfinite(elev), elev, 0)
181
- if np.any(a != 0):
182
- vmin = np.nanpercentile(a[a != 0], 2)
183
- vmax = np.nanpercentile(a[a != 0], 98)
184
- else:
185
- vmin, vmax = 0, 1
186
-
187
- if vmin == vmax:
188
- vmax = vmin + 1
189
-
190
- norm = np.clip((a - vmin) / (vmax - vmin), 0, 1)
191
- gray = (norm * 255).astype(np.uint8)
192
-
193
- rgb = np.stack([gray, gray, gray], axis=-1)
194
- alpha = np.where(np.isfinite(elev), 255, 0).astype(np.uint8)
195
- rgba = np.dstack([rgb, alpha])
196
-
197
- img = Image.fromarray(rgba, "RGBA")
198
- buf = io.BytesIO()
199
- img.save(buf, format="PNG", compress_level=1)
200
- buf.seek(0)
201
- return Response(buf.getvalue(), media_type="image/png")
202
 
203
  except TileOutsideBounds:
204
  blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
205
  buf = io.BytesIO()
206
- blank.save(buf, format="PNG", compress_level=1)
207
  buf.seek(0)
208
  return Response(buf.getvalue(), media_type="image/png")
209
  except Exception as e:
@@ -213,18 +188,10 @@ def debug_tile(z: int, x: int, y: int):
213
  @app.get("/point")
214
  def get_point(lon: float, lat: float):
215
  try:
216
- cog = get_cog_reader()
217
- val = cog.point(lon, lat)
218
- elevation = float(val.data[0])
219
- return {"lon": lon, "lat": lat, "elevation_m": elevation}
220
  except Exception as e:
221
  logger.exception("Error sampling point")
222
- return Response(status_code=500, content=f"Error: {e}")
223
-
224
- # Cleanup al cerrar
225
- @app.on_event("shutdown")
226
- def shutdown_event():
227
- global _cog_reader
228
- if _cog_reader is not None:
229
- _cog_reader.close()
230
- _cog_reader = None
 
1
+ # app.py (versión CONSERVADORA - solo cache, nada más)
2
  import io
3
  import os
4
  import logging
 
12
 
13
  COG_URL = os.environ.get("COG_URL", "https://huggingface.co/datasets/Pingul/mexico-dem/resolve/main/Mexico_DEM_COG.tif")
14
  TILE_SIZE = 256
 
15
 
16
+ # Logging simple
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("dem-tiler")
19
 
20
+ app = FastAPI(title="Mexico DEM TerrainRGB API (con cache)")
21
 
22
  app.add_middleware(
23
  CORSMiddleware,
 
27
  allow_headers=["*"],
28
  )
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def encode_terrainrgb(arr: np.ndarray) -> np.ndarray:
31
  """
32
  Convierte elevación en metros a Terrain-RGB (Mapbox spec).
 
42
  rgb = np.stack([R, G, B], axis=-1).astype(np.uint8)
43
  return rgb
44
 
45
+ def elev_to_grayscale(arr: np.ndarray) -> np.ndarray:
46
+ """
47
+ Convierte elevación a imagen grayscale para debug: mapea min->0 max->255.
48
+ NaNs se convierten a 0 (transparente later).
49
+ """
50
+ a = np.array(arr, dtype=np.float32)
51
+ a = np.where(np.isfinite(a), a, np.nan)
52
+ vmin = np.nanpercentile(a, 2) if np.isfinite(np.nanpercentile(a, 2)) else np.nanmin(a)
53
+ vmax = np.nanpercentile(a, 98) if np.isfinite(np.nanpercentile(a, 98)) else np.nanmax(a)
54
+ if np.isnan(vmin) or np.isnan(vmax) or vmin == vmax:
55
+ vmin, vmax = np.nanmin(a), np.nanmax(a)
56
+ if np.isnan(vmin): vmin = 0.0
57
+ if np.isnan(vmax): vmax = vmin + 1.0
58
+
59
+ norm = (a - vmin) / (vmax - vmin)
60
+ norm = np.clip(norm, 0.0, 1.0)
61
+ img = (norm * 255).astype(np.uint8)
62
+ rgb = np.stack([img, img, img], axis=-1)
63
+ return rgb
64
+
65
  # ============================================================================
66
+ # ÚNICA OPTIMIZACIÓN: Cache de tiles
67
  # ============================================================================
68
+ @lru_cache(maxsize=512)
69
+ def get_tile_data(z: int, x: int, y: int) -> bytes:
70
  """
71
+ Genera y cachea tiles. Esta es la única optimización añadida.
 
72
  """
73
  try:
74
+ with COGReader(COG_URL) as cog:
75
+ data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
76
+ elev = data[0].astype(np.float32)
77
+
78
+ if mask is not None:
79
+ invalid = (mask == 0)
80
+ elev[invalid] = np.nan
81
+
82
+ if np.all(np.isnan(elev)):
83
+ raise TileOutsideBounds(f"All NaN for tile {z}/{x}/{y}")
84
+
85
+ rgb = encode_terrainrgb(np.nan_to_num(elev, nan=-10000.0))
86
+ img = Image.fromarray(rgb, "RGB")
87
+ buf = io.BytesIO()
88
+ # compress_level=1 hace PNG más rápido (vs default 6)
89
+ img.save(buf, format="PNG", compress_level=1)
90
+ return buf.getvalue()
 
 
 
 
 
 
 
 
91
 
92
  except TileOutsideBounds:
 
93
  blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
94
  buf = io.BytesIO()
95
  blank.save(buf, format="PNG", compress_level=1)
96
  return buf.getvalue()
97
 
98
  # ============================================================================
99
+ # ENDPOINTS (tu código original, sin cambios)
100
  # ============================================================================
101
  @app.get("/")
102
  def root():
103
+ return {"message": "Mexico DEM TerrainRGB API running (with cache)", "cog_url": COG_URL}
104
 
105
  @app.get("/health")
106
  def health():
 
108
 
109
  @app.get("/cache/stats")
110
  def cache_stats():
111
+ """Nuevo: Ver estadísticas del cache"""
112
+ info = get_tile_data.cache_info()
113
  return {
114
  "hits": info.hits,
115
  "misses": info.misses,
 
118
  "hit_rate": f"{info.hits / (info.hits + info.misses) * 100:.1f}%" if (info.hits + info.misses) > 0 else "0%"
119
  }
120
 
 
 
 
 
 
 
121
  @app.get("/info")
122
  def get_info():
123
  try:
124
+ with COGReader(COG_URL) as cog:
125
+ meta = cog.info()
126
+ out = {
127
+ "bounds": meta.bounds,
128
+ "minzoom": meta.minzoom,
129
+ "maxzoom": meta.maxzoom,
130
+ "band_descriptions": meta.band_descriptions,
131
+ "statistics": {
132
+ "min": getattr(meta, "min", None),
133
+ "max": getattr(meta, "max", None)
134
+ },
135
+ "crs": str(meta.crs)
136
+ }
137
+ return out
138
  except Exception as e:
139
  logger.exception("Error reading COG info")
140
  return {"error": str(e)}
 
142
  @app.get("/tiles/{z}/{x}/{y}.png")
143
  def get_tile(z: int, x: int, y: int):
144
  """
145
+ Tile endpoint con cache - LA ÚNICA DIFERENCIA con tu código original
146
  """
147
  try:
148
+ tile_bytes = get_tile_data(z, x, y)
149
  return Response(tile_bytes, media_type="image/png")
150
  except Exception as e:
151
  logger.exception(f"Unexpected error generating tile {z}/{x}/{y}")
152
+ return Response(status_code=500, content=f"Server error generating tile: {str(e)}")
153
 
154
  @app.get("/debug/{z}/{x}/{y}.png")
155
  def debug_tile(z: int, x: int, y: int):
156
+ """
157
+ Endpoint de debug (sin cambios de tu código original)
158
+ """
159
  try:
160
+ with COGReader(COG_URL) as cog:
161
+ data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
162
+ elev = data[0].astype(np.float32)
163
+ if mask is not None:
164
+ elev[mask == 0] = np.nan
165
+
166
+ if np.all(np.isnan(elev)):
167
+ raise TileOutsideBounds()
168
+
169
+ rgb = elev_to_grayscale(elev)
170
+ alpha = np.where(np.isfinite(elev), 255, 0).astype(np.uint8)
171
+ rgba = np.dstack([rgb, alpha])
172
+ img = Image.fromarray(rgba, "RGBA")
173
+ buf = io.BytesIO()
174
+ img.save(buf, format="PNG")
175
+ buf.seek(0)
176
+ return Response(buf.getvalue(), media_type="image/png")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  except TileOutsideBounds:
179
  blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
180
  buf = io.BytesIO()
181
+ blank.save(buf, format="PNG")
182
  buf.seek(0)
183
  return Response(buf.getvalue(), media_type="image/png")
184
  except Exception as e:
 
188
  @app.get("/point")
189
  def get_point(lon: float, lat: float):
190
  try:
191
+ with COGReader(COG_URL) as cog:
192
+ val = cog.point(lon, lat)
193
+ elevation = float(val.data[0])
194
+ return {"lon": lon, "lat": lat, "elevation_m": elevation}
195
  except Exception as e:
196
  logger.exception("Error sampling point")
197
+ return Response(status_code=500, content=f"Error sampling point: {e}")