GeoTIFF: Imágenes Raster Georreferenciadas Explicadas
GeoTIFF es un archivo TIFF (Tagged Image File Format) que incrusta metadatos geoespaciales — sistema de referencia de coordenadas, extensión geográfica, resolución de píxel y proyección cartográfica — directamente en las etiquetas TIFF. El resultado es un archivo raster que "sabe dónde está": cada píxel puede mapearse a una ubicación geográfica precisa (latitud/longitud o coordenadas proyectadas). GeoTIFF es el formato fundamental para imágenes satelitales, fotografía aérea, Modelos Digitales de Elevación (MDE), cuadrículas climáticas, clasificaciones de cobertura del suelo y prácticamente cualquier conjunto de datos raster en la industria geoespacial.
Qué Hace que un TIFF sea un GeoTIFF
Un TIFF estándar almacena datos de píxeles sin contexto geográfico. GeoTIFF añade tres categorías de metadatos en etiquetas privadas TIFF:
1. Transformación Espacio Raster / Espacio Modelo
Las etiquetas ModelTiepointTag (33922) y ModelPixelScaleTag (33550) definen la transformación afín entre coordenadas de píxel y coordenadas geográficas:
X_Geográfico = TiePoint_X + (Col_Píxel × Escala_X)
Y_Geográfico = TiePoint_Y - (Fila_Píxel × Escala_Y) # Y decrece hacia abajo
Ejemplo: un raster de resolución 10 metros:
ModelTiepointTag: [0, 0, 0, 400000.0, 4500000.0, 0]
ModelPixelScaleTag: [10.0, 10.0, 0] ← 10 metros por píxel
2. GeoKeyDirectory (etiqueta 34735)
El GeoKeyDirectory almacena los parámetros del sistema de coordenadas como una lista de pares clave-valor:
GTModelTypeGeoKey (1024): ModelTypeProjected (1) = plano XY proyectado
GTRasterTypeGeoKey (1025): RasterPixelIsArea (1)
ProjectedCSTypeGeoKey (3072): 25830 (ETRS89 / UTM zone 30N)
ProjLinearUnitsGeoKey (3076): Linear_Meter (9001)
Estructura del Raster
Un raster GeoTIFF se organiza como una cuadrícula 2D de píxeles, posiblemente con múltiples bandas:
Diseño de bandas (Sentinel-2):
Banda 1: Aerosoles costeros (~443 nm)
Banda 2: Azul (~490 nm)
Banda 3: Verde (~560 nm)
Banda 4: Rojo (~665 nm)
Banda 8: NIR (~842 nm)
...
Cada banda es un array 2D de valores — típicamente uint8 (0-255) para imágenes listas para visualización, uint16 (0-65535) para datos de sensor con detalle radiométrico, float32 para datos continuos como elevación o variables climáticas.
Valores NoData
Las bandas GeoTIFF tienen un valor NoData — un valor centinela que indica que un píxel no tiene datos válidos (píxeles oceánicos en un producto solo de tierra, nubes enmascaradas, píxeles fuera del límite de la imagen). Valores NoData comunes: 0, -9999, -32768, 65535, NaN (para bandas float).
Python: rasterio
import rasterio
from rasterio.warp import calculate_default_transform, reproject, Resampling
import numpy as np
# Leer un GeoTIFF
with rasterio.open('sentinel2_rgb.tif') as src:
print(f"CRS: {src.crs}")
print(f"Transformación: {src.transform}")
print(f"Extensión: {src.bounds}")
print(f"Forma: {src.height} × {src.width} píxeles")
print(f"Bandas: {src.count}")
print(f"Tipo: {src.dtypes}")
print(f"NoData: {src.nodata}")
print(f"Resolución: {src.res} metros/píxel")
# Leer todas las bandas como array NumPy (bandas, alto, ancho)
datos = src.read() # forma: (bandas, alto, ancho)
# Leer solo banda 4 (NIR)
nir = src.read(4) # forma: (alto, ancho)
# Leer una ventana espacial (filas 100-200, cols 200-300)
ventana = rasterio.windows.Window(200, 100, 100, 100)
subconjunto = src.read(window=ventana)
# Obtener coordenadas geográficas para un píxel
lon, lat = src.xy(row=100, col=200)
print(f"Píxel (200,100) en ({lon:.4f}°, {lat:.4f}°)")
# Calcular NDVI (Índice de Vegetación de Diferencia Normalizada)
with rasterio.open('landsat8.tif') as src:
rojo = src.read(4).astype('float32') # Banda 4: Rojo
nir = src.read(5).astype('float32') # Banda 5: NIR
nodata = src.nodata
if nodata is not None:
mascara = (rojo == nodata) | (nir == nodata)
rojo[mascara] = np.nan
nir[mascara] = np.nan
ndvi = (nir - rojo) / (nir + rojo) # rango: -1 a +1
# Escribir resultado NDVI
perfil = src.profile.copy()
perfil.update(count=1, dtype='float32', nodata=-9999)
ndvi[np.isnan(ndvi)] = -9999
with rasterio.open('ndvi.tif', 'w', **perfil) as dst:
dst.write(ndvi, 1)
# Reproyectar a coordenadas geográficas WGS84
with rasterio.open('entrada_utm.tif') as src:
crs_destino = rasterio.crs.CRS.from_epsg(4326) # WGS84
transformacion, ancho, alto = calculate_default_transform(
src.crs, crs_destino, src.width, src.height, *src.bounds
)
perfil = src.profile.copy()
perfil.update(crs=crs_destino, transform=transformacion, width=ancho, height=alto)
with rasterio.open('salida_wgs84.tif', 'w', **perfil) as dst:
for i in range(1, src.count + 1):
reproject(
source=rasterio.band(src, i),
destination=rasterio.band(dst, i),
src_transform=src.transform,
src_crs=src.crs,
dst_transform=transformacion,
dst_crs=crs_destino,
resampling=Resampling.bilinear,
)
Cloud Optimized GeoTIFF (COG)
Un Cloud Optimized GeoTIFF (COG) reorganiza el diseño interno de un archivo GeoTIFF para que pueda accederse eficientemente a través de HTTP usando solicitudes de rango de bytes. El GeoTIFF estándar requiere descargar el archivo completo para leer un área pequeña; un COG almacena datos en teselas espaciales (típicamente 512×512 píxeles) con pirámides de vistas generales precomputadas a resoluciones inferiores.
import subprocess
# Convertir a COG
subprocess.run([
"gdal_translate",
"-of", "COG",
"-co", "COMPRESS=DEFLATE",
"-co", "OVERVIEW_LEVEL=AUTO",
"-co", "BLOCKSIZE=512",
"entrada.tif", "salida_cog.tif"
])
# Acceder a un COG remotamente sin descargarlo
with rasterio.open("https://ejemplo-bucket.s3.amazonaws.com/escena.tif") as src:
# Solo descarga las teselas necesarias para esta ventana
subconjunto = src.read(window=rasterio.windows.from_bounds(
left=-3.8, bottom=40.3, right=-3.6, top=40.5,
transform=src.transform
))
Modelos Digitales de Elevación (MDEs)
GeoTIFF es el formato estándar para los MDEs — rasters donde el valor de cada píxel es una elevación en metros:
import rasterio
import numpy as np
with rasterio.open('mde_10m.tif') as src:
elevacion = src.read(1).astype('float32')
nodata = src.nodata
if nodata:
elevacion[elevacion == nodata] = np.nan
print(f"Elevación mínima: {np.nanmin(elevacion):.1f} m")
print(f"Elevación máxima: {np.nanmax(elevacion):.1f} m")
print(f"Elevación media: {np.nanmean(elevacion):.1f} m")
# Calcular pendiente
res_x, res_y = src.res
dy, dx = np.gradient(elevacion, res_y, res_x)
pendiente_grados = np.degrees(np.arctan(np.sqrt(dx**2 + dy**2)))
print(f"Pendiente máxima: {np.nanmax(pendiente_grados):.1f} grados")
# Contar píxeles por encima de 1000 m
sobre_1000 = np.sum(elevacion > 1000)
area_km2 = sobre_1000 * (res_x * res_y) / 1e6
print(f"Área sobre 1000 m: {area_km2:.1f} km²")
GeoTIFF vs Otros Formatos Raster
| Característica | GeoTIFF | JPEG 2000 | NetCDF | PNG |
|---|---|---|---|---|
| Metadatos geoespaciales | ✅ Nativos | ✅ Sí | ✅ Sí | ❌ No |
| Compresión sin pérdida | ✅ LZW/DEFLATE | ✅ Sí | ✅ Sí | ✅ Sí |
| Compresión con pérdida | ✅ JPEG | ✅ Sí | ❌ No | ❌ No |
| Bandas multiespectrales | ✅ Ilimitadas | ✅ Sí | ✅ Sí | ❌ 4 máx |
| Datos float/int16 | ✅ Sí | ✅ Sí | ✅ Sí | ❌ Solo uint8 |
| Nativo en la nube | ✅ Variante COG | ⚠️ JPIP | ❌ Deficiente | ❌ No |
| Formato abierto | ✅ Estándar OGC | ✅ ISO 15444 | ✅ Abierto | ✅ Abierto |
Conclusión
El éxito de GeoTIFF deriva de tres factores: se construye sobre el contenedor TIFF universal (excelente soporte de herramientas), sus etiquetas de metadatos geoespaciales son una extensión limpia que degrada graciosamente en herramientas no geoespaciales, y la evolución del Cloud Optimized GeoTIFF lo hace práctico para flujos de trabajo nativos en la nube a escala planetaria. Comprender el modelo de transformación afín de GeoTIFF, la incrustación de CRS, la estructura de bandas, el manejo de NoData y el diseño COG es fundamental para cualquier trabajo en teledetección, análisis de imágenes satelitales, modelado del terreno o pipelines de datos geoespaciales.
Conversiones relacionadas
Lo más habitual al trabajar con imágenes son estas direcciones de conversión: