#30DayMapChallenge 2026 · Día 07 · 10 minute map

0

El Día 07 del #30DayMapChallenge pedía un mapa de luces nocturnas hecho a toda prisa, y yo quería que Venezuela se viera desde el espacio, de noche. Lo armé con tres piezas: un script corto de Google Earth Engine que baja la radiancia VIIRS, ArcGIS Pro para pintarla y componer el layout, y una IA conectada a Pro por MCP que ejecutó el trabajo repetitivo. Al final hice una animación en la que las luces se encienden de la más intensa a la más tenue. Te cuento cada paso y dejo el código.


mapa-dia-07


En esta guía aprenderás
  • A descargar la radiancia nocturna VIIRS de un país con Earth Engine, ya reescalada para mapas.
  • A darle una paleta propia al raster en ArcGIS Pro y a armar un layout vertical con arcpy.
  • A convertir el mapa en una animación con música original, sin derechos de autor.

El resultado

Un mapa vertical 4:5, de fondo casi negro, donde cada píxel de unos 464 m de lado es una luz medida por el satélite. La escala de color va de negro a azul navy, azul cielo, dorado y blanco. Tiene título, ciudades, leyenda, fuentes y sistema de referencia (EPSG:4326, WGS 84).


Mapa-luces-nocturnas-Venezuela
Figura 1. Venezuela de noche, radiancia VIIRS (mediana de 2023) en ArcGIS Pro. Fuente: NOAA / NASA VIIRS vía Google Earth Engine; elaboración DALPGIS.

Puedes ver el video final en YouTube: Venezuela de noche, animación.


Quién hizo qué

Un MCP (Model Context Protocol) es el puente que deja a una IA usar las herramientas de un programa. Con el MCP de ArcGIS Pro, la IA puede leer mi proyecto abierto, ejecutar geoprocesos y scripts de arcpy y exportar layouts. Así quedó el reparto:

TareaLa IA por MCPYo
Elegir tema y datoPropuso opcionesElegí las luces nocturnas y el país
Script de Earth EngineLo escribióLo corrí en el Code Editor y exporté el raster
Mapa y paleta en ProAplicó la paleta y creó el mapaRevisé el color
LayoutLo construyó con arcpy y exportó vistas previasMoví algunas etiquetas a mano
Animación y músicaLas generó con códigoRevisé el resultado


Paso a paso

1. Descarga la luz nocturna con Earth Engine

Uso la colección mensual NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG, que corrige la luz parásita, y los límites de países de FAO GAUL 2015. Saco la mediana de los doce meses para quitar destellos puntuales, y la paso a escala logarítmica de 0 a 255. Con el logaritmo se ven a la vez una ciudad grande y un pueblo pequeño.

var year = 2023;                                  // año a resumir
var pais = ee.FeatureCollection('FAO/GAUL/2015/level0')
  .filter(ee.Filter.eq('ADM0_NAME', 'Venezuela'));
var aoi = pais.geometry().buffer(30000, 1000).bounds(1000);   // país + 30 km de margen

var viirs = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate(year + '-01-01', (year + 1) + '-01-01')
  .filterBounds(aoi)
  .select('avg_rad');
print('Imágenes mensuales usadas:', viirs.size());     // si sale 0, baja el año

var radiancia = viirs.median().clip(aoi);              // mediana anual

// log10 reescalado: 0,1 nW/cm²/sr o menos -> 0 ; 100 o más -> 255
var luz8 = radiancia.max(0.1).log10()
  .add(1).divide(3).multiply(255)
  .clamp(0, 255).round().uint8().rename('LUZ_' + year);

Export.image.toDrive({
  image: luz8, description: 'D07_LUZ_VEN_' + year,
  folder: 'D07_LUCES_VENEZUELA', fileNamePrefix: 'D07_LUZ_VEN_' + year,
  region: aoi, crs: 'EPSG:4326', scale: 463.83, maxPixels: 1e9
});

En el Code Editor abre la pestaña Tasks y pulsa RUN. El GeoTIFF llega a tu Google Drive.

💡 Tip DALPGIS: si viirs.size() imprime 0, ese año aún no está en la colección. Baja el valor de year. Yo usé 2023.

2. Crea la paleta de marca

ArcGIS Pro acepta un archivo .clr con 256 líneas (índice, rojo, verde, azul). Lo genero interpolando entre cinco colores:

# Genera d07_marca.clr: 256 colores interpolados con la paleta DALPGIS
stops = {0: (0, 0, 0), 64: (7, 34, 95), 128: (91, 143, 214),
         191: (229, 192, 11), 255: (255, 255, 255)}
claves = sorted(stops)
with open("d07_marca.clr", "w") as f:
    for i in range(256):
        a = max(k for k in claves if k <= i)
        b = min(k for k in claves if k >= i)
        t = 0 if a == b else (i - a) / (b - a)
        rgb = [round(stops[a][c] + (stops[b][c] - stops[a][c]) * t) for c in range(3)]
        f.write("%d %d %d %d\n" % (i, *rgb))


3. Prepara el mapa en ArcGIS Pro

Aquí entra el MCP. Mi proyecto tiene una caja de herramientas Python mínima (un .pyt) con una herramienta que ejecuta un script .py dentro del proyecto abierto, con la variable aprx ya disponible. La IA escribe el script, lo guardo en mi carpeta y lo lanza con run_gp_tool. El script copia el raster, le pone la paleta y crea el mapa:

import arcpy, os
base = r"D:\PROY_RETO_MAPAS\DIA_07"
arcpy.env.overwriteOutput = True

# copia del raster con la paleta (el original no se toca)
src = os.path.join(base, "D07_LUZ_VEN_2023.tif")
dst = os.path.join(base, "D07_LUZ_VEN_2023_marca.tif")
arcpy.management.CopyRaster(src, dst, pixel_type="8_BIT_UNSIGNED")
arcpy.management.AddColormap(dst, "#", os.path.join(base, "d07_marca.clr"))

# mapa nuevo en coordenadas geográficas
m = aprx.createMap("D07_Noche", "MAP")
m.spatialReference = arcpy.SpatialReference(4326)
r = m.addDataFromPath(dst)
r.name = "Luz_nocturna_2023"

# contorno de estados: sin relleno y dorado casi transparente
e = m.addDataFromPath(r"RUTA\A\TU\CAPA_DE_ESTADOS")
e.name = "Estados"
s = e.symbology
s.updateRenderer("SimpleRenderer")
s.renderer.symbol.color = {"RGB": [0, 0, 0, 0]}
s.renderer.symbol.outlineColor = {"RGB": [229, 192, 11, 40]}
s.renderer.symbol.outlineWidth = 0.4
e.symbology = s
aprx.save()
Ojo: me pasó que, al copiar de nuevo un script con el mismo nombre, Pro siguió ejecutando la versión vieja. Guarda cada versión con un nombre nuevo (d07_layout_v1.py, v2) y revisa el registro antes de exportar.


4. Arma el layout con arcpy

La clave es que el marco del mapa tenga la misma proporción que el raster. Así puedo convertir longitud y latitud a posición de página con una regla de tres y colocar cada etiqueta con precisión. Este es el esqueleto:

import arcpy
lyt = aprx.createLayout(12.0, 15.0, "INCH", "D07_Noche_Venezuela")     # 4:5

def clr(rgb, t):
    return '<CLR red="%d" green="%d" blue="%d">%s</CLR>' % (rgb[0], rgb[1], rgb[2], t)

def poly(x0, y0, x1, y1):
    return arcpy.Polygon(arcpy.Array([arcpy.Point(x0, y0), arcpy.Point(x0, y1),
        arcpy.Point(x1, y1), arcpy.Point(x1, y0), arcpy.Point(x0, y0)]))

def text(name, x0, ytop, w, h, txt, size, rgb, style="Regular"):
    aprx.createTextElement(lyt, poly(x0, ytop - h, x0 + w, ytop), "POLYGON",
                           clr(rgb, txt), size, "Montserrat", style, name=name)

# marco del mapa con la MISMA proporción del raster (así las coordenadas calzan)
RW, RE, RS, RN = -73.6498076, -59.5290112, 0.3791657, 12.7624667
MX0, MX1, MY1 = 0.5, 11.5, 12.85
MY0 = MY1 - (MX1 - MX0) * (RN - RS) / (RE - RW)
mf = lyt.createMapFrame(arcpy.Extent(MX0, MY0, MX1, MY1), m, "D07_Mapa")
mf.camera.setExtent(arcpy.Extent(RW, RS, RE, RN))

def pg(lon, lat):             # lon/lat -> posición en la página (pulgadas)
    return (MX0 + (lon - RW) / (RE - RW) * (MX1 - MX0),
            MY0 + (lat - RS) / (RN - RS) * (MY1 - MY0))

text("D07_Titulo", 0.5, 14.3, 11.2, 0.9, "VENEZUELA DE NOCHE", 52, (255, 255, 255), "Bold")

# una ciudad = un anillo + una etiqueta, posicionados con pg()
x, y = pg(-71.62, 10.64)      # Maracaibo
aprx.createPredefinedGraphicElement(lyt, poly(x - .045, y - .045, x + .045, y + .045),
                                    "ELLIPSE", name="D07_Pt_Maracaibo")
text("D07_Lb_Maracaibo", x - 2.3, y + 0.30, 2.2, 0.25, "Maracaibo", 10, (255, 255, 255), "Bold")

Repetí ese patrón (anillo y etiqueta) para Maracaibo, Caracas, Valencia–Maracay, Barquisimeto, Puerto La Cruz, Ciudad Guayana y Puerto Ayacucho. También puse una llamada dorada en el norte de Monagas, la leyenda como una barra de 64 rectángulos con la paleta y el bloque de fuentes y CRS.


Detalle-norte-de-Venezuela
Figura 2. Detalle del norte del país. Se distingue el corredor de ciudades de la costa y el resplandor de Monagas. Fuente: NOAA / NASA VIIRS; elaboración DALPGIS.


5. Revisa, ajusta y exporta

La IA exportó una vista previa pequeña con export_layout, yo la miré y corregimos lo que chocaba: un texto sobre otro, una etiqueta tapada por el brillo. Luego moví unas etiquetas a mano en Pro y exporté el PNG a 200 ppp y el PDF.


6. Convierte el mapa en una animación

ArcPy no anima, así que renderizo los fotogramas fuera de Pro con el mismo raster y la misma paleta. En cada fotograma muestro solo los píxeles por encima de un umbral que baja con el tiempo: primero se encienden los más brillantes y al final los tenues. Después aparece el layout completo.

import numpy as np
from PIL import Image

mapa = Image.open("D07_Venezuela_de_noche.png").convert("RGB")      # export de ArcGIS Pro
W, H = mapa.size
s = W / 12.0                                                         # píxeles por pulgada
x0, x1, y0, y1 = int(0.5 * s), int(11.5 * s), int(2.15 * s), int(11.80 * s)

paleta = np.array([[int(v) for v in l.split()[1:4]]
                   for l in open("d07_marca.clr")], dtype=np.uint8)
raster = np.array(Image.open("D07_LUZ_VEN_2023.tif"))                # uint8 0-255
v = np.array(Image.fromarray(raster).resize((x1 - x0, y1 - y0), Image.BILINEAR))

fondo = np.array((3, 9, 26), dtype=np.uint8)
base = np.array(mapa).copy()
base[y0:y1, x0:x1] = fondo                                           # sin mapa: título, leyenda, pie

fps, n = 30, 0
def guardar(a):
    global n
    Image.fromarray(a).resize((1080, 1350), Image.LANCZOS).save("fr/f%04d.png" % n)
    n += 1

for i in range(4 * fps):                                             # 4 s: se encienden las luces
    t = (i + 1) / (4 * fps)
    t = t * t * (3 - 2 * t)                                          # entra y sale suave
    umbral = 255 - (255 - 45) * t                                    # de las más intensas a las tenues
    fr = base.copy()
    fr[y0:y1, x0:x1] = np.where((v >= umbral)[..., None], paleta[v], fondo)
    guardar(fr)

ultimo, completo = fr.copy(), np.array(mapa)
for i in range(fps // 2):
    guardar(ultimo)                                                  # pausa
for i in range(fps):                                                 # 1 s: aparece el layout final
    a = (i + 1) / fps
    guardar((ultimo * (1 - a) + completo * a).astype(np.uint8))
for i in range(3 * fps):
    guardar(completo)
# ffmpeg -framerate 30 -i fr/f%04d.png -c:v libx264 -pix_fmt yuv420p -crf 18 animacion.mp4

fotogramas-animación
Figura 3. Cuatro momentos de la animación. Fuente: NOAA / NASA VIIRS; elaboración DALPGIS.


7. Música original sin derechos de autor

Para no depender de pistas de terceros, generé la música con código: un colchón de acordes suaves y una campanita por cada luz que se enciende. Como la compuse yo, no tiene derechos de autor.

import numpy as np, wave
sr, D = 44100, 8.5
t = np.arange(int(sr * D)) / sr
L, R = np.zeros_like(t), np.zeros_like(t)
rng = np.random.default_rng(7)
midi = lambda m: 440 * 2 ** ((m - 69) / 12)

def sumar(sig, pan):
    global L, R
    L += sig * (1 - pan); R += sig * pan

# colchón: acordes de Am9 y Fmaj7 con dos voces ligeramente desafinadas
def colchon(notas, a, b):
    for m in notas:
        for det, pan in ((-0.07, 0.3), (0.07, 0.7)):
            f = midi(m) * 2 ** (det / 12)
            s = sum(np.sin(2 * np.pi * f * k * t + k) / k ** 1.6 for k in range(1, 7))
            sumar(s * np.clip((t - a) / 1.6, 0, 1) * np.clip((b - t) / 1.6, 0, 1) * 0.05, pan)

colchon([45, 52, 57, 60, 64], 0, 4.8)
colchon([41, 48, 53, 57, 64], 3.2, D)

# campanitas: una por "luz" que se enciende, cada vez más seguidas
escala = [69, 72, 74, 76, 79, 81, 84, 88]
e = 0.3
while e < 4.6:
    f = midi(escala[min(7, int(rng.random() * 3 + e / 4.6 * 5))])
    d = t - e
    env = np.where(d > 0, np.exp(-d * 3.5), 0) * np.clip(d * 200, 0, 1)
    sumar((np.sin(2 * np.pi * f * t) + 0.4 * np.sin(2 * np.pi * f * 2.01 * t)) * env * 0.05, rng.random())
    e += max(0.12, 0.7 * (1 - e / 4.6) ** 1.5 + 0.1)

for ch in (L, R):                                    # eco difuso + fade in/out
    for dl, g in ((0.073, .45), (0.131, .35), (0.197, .28), (0.283, .2)):
        k = int(dl * sr); ch[k:] += ch[:-k].copy() * g
fade = np.clip(t / 0.8, 0, 1) * np.clip((D - t) / 1.5, 0, 1)
st = np.stack([L * fade, R * fade], 1)
st = (st / abs(st).max() * 0.8 * 32767).astype(np.int16)
with wave.open("musica.wav", "wb") as w:
    w.setnchannels(2); w.setsampwidth(2); w.setframerate(sr); w.writeframes(st.tobytes())

Para unirla al video: ffmpeg -i animacion.mp4 -i musica.wav -map 0:v -map 1:a -c:v copy -c:a aac -shortest salida.mp4


Qué se ve en el mapa

  • Un corredor de luz en el norte. Desde Maracaibo hasta Puerto La Cruz, la costa y el eje de Valencia, Maracay y Caracas concentran la mayor parte del brillo.
  • El sur, casi a oscuras. Amazonas y el sur de Bolívar casi no emiten luz; apenas se distingue Puerto Ayacucho.
  • Un resplandor intenso en el norte de Monagas. Es el punto más brillante del mapa y coincide con una zona petrolera. No lo atribuyo a una sola causa sin verificarla; podría incluir instalaciones con quema de gas.
  • Vecinos visibles. Cúcuta, Boa Vista y las luces de Trinidad y Tobago aparecen en el borde del mapa.


Límites y precauciones

  • La escala es logarítmica. Sirve para ver dónde hay luz, no para comparar cuánta. El brillo se satura en 100 nW/cm²/sr.
  • Luz no es población ni economía. VIIRS no capta bien algunas luces (por ejemplo, ciertos LED de luz azul) y sí registra quemas, llamas de gas e incluso barcos. Una zona oscura no está necesariamente deshabitada.
  • Es una mediana de 2023. Esconde apagones o cambios dentro del año.
  • Proyección. El mapa está en EPSG:4326 (grados). No lo uses para medir áreas ni distancias; reproyecta antes.
  • Límites estatales. Vienen de mi capa de división estadal y se muestran con transparencia solo como referencia.
  • Resolución. Cada píxel mide unos 464 m; no se ven calles ni edificios.


Dónde usar este método

El mismo flujo (descargar, dar paleta, componer layout y animar) sirve para series de mapas con un estilo común, como este reto. Con luces nocturnas puedes comparar años, estudiar la expansión urbana o preparar un mapa de portada. Si quieres ver cómo usé el mismo MCP en otros días, lee el Día 03 y el Día 05. La documentación del conjunto de datos está en el catálogo de Earth Engine, y la de arcpy.mp en la ayuda de ArcGIS Pro.

El lunes 9/11/2026 llega el Atlas Petrolero

Suscríbete a GeoHacks Insight en LinkedIn y recibe cada mapa del reto con su proceso.

Suscribirme en LinkedIn Entrar a la comunidad

Preguntas frecuentes

¿Cómo hago un mapa de luces nocturnas en ArcGIS Pro?

Descarga la radiancia VIIRS (por ejemplo desde Earth Engine), ábrela en Pro, aplica una paleta con un archivo .clr y arma el layout. La escala logarítmica ayuda a ver a la vez ciudades grandes y pueblos pequeños.

¿Qué es el MCP de ArcGIS Pro?

Es un puente que deja a una IA usar ArcGIS Pro: leer el proyecto, ejecutar geoprocesos y scripts de arcpy y exportar layouts. Tú decides qué se hace y revisas cada resultado.

¿Por qué usar escala logarítmica?

Porque la radiancia varía en varios órdenes de magnitud. Sin logaritmo, solo se verían las ciudades más grandes y todo lo demás quedaría negro.

¿Puedo animar un mapa en ArcGIS Pro?

ArcPy no ofrece animación de mapas ráster como esta. Yo renderizo los fotogramas con Python, con el mismo raster y paleta, y los uno con ffmpeg.

¿Los puntos brillantes son siempre ciudades?

No. VIIRS también capta carreteras, instalaciones industriales, quema de gas, incendios y barcos. Interpreta cada zona con otra fuente antes de concluir algo.


Entradas que pueden interesarte

Sin comentarios

Posts Populares

¡Bienvenido al Curso Básico de ArcGIS Pro Desde Cero!

¡Bienvenido al Curso Básico de ArcGIS Pro Desde Cero!

Hola, soy Daniel León, ingeniero civil y experto en Siste…

NDVI con ArcGIS: Guía completa para el monitoreo de cultivos

NDVI con ArcGIS: Guía completa para el monitoreo de cultivos

¿Te gustaría saber qué es el NDVI y cómo puedes usarlo con…

ArcGIS Pro: ¿Cuánto cuesta y dónde adquirirlo?

ArcGIS Pro: ¿Cuánto cuesta y dónde adquirirlo?

ArcGIS Pro es una potente herramienta de software diseñada…

ArcGIS Pro: Asignar simbología a rasters con archivo .CLR paso a paso (Colormap)

ArcGIS Pro: Asignar simbología a rasters con archivo .CLR paso a paso (Colormap)

1) Verifica que tu raster sea compatible con .clr Los archi…