Diego García Díaz
GIS & Remote Sensing. Python & Statistics. Surf & MTB :)
Protocolo automático para el tratamiento de imágenes Landsat y la generación de productos derivados
I meeting proyecto GOYAS (2/12/2024)
Protocolo automático para el tratamiento de imágenes Landsat y la generación de productos derivados
Javier Bustamante Díaz
Responsable científico
(Investigador Científico)
Ricardo Díaz-Delgado Hernández
Dr. Biología
Isabel Afán Asencio
Responsable Técnico
Dra. Biología
David Aragonés Borrego
Responsable Técnico
Ingeniero Forestal
Diego García Díaz
Geógrafo
Pedro Jesús Gómez Giráldez
Ingeniero de Montes
Gabriela Patricia Romero Olivos
Geógrafa
1- Productos
(y metadatos)
2 - Scripts
3 - Geoportal
Flood Mask
Turbidez
Profundidad
Hidroperiodo.
¿Qué podemos aportar al proyecto?
2 - Scripts de procesado
2 - Scripts
3 - Geoportal
class Landsat:
"""Clase para trabajar con las Landsat de la nueva Collection 2 Level 2 del USGS"""
def __init__(self, ruta_escena):
"""Inicializa un objeto Landsat basado en el path de la escena.
Args:
ruta_escena (str): Ruta al directorio de la escena Landsat.
"""
# Definimos los paths de entrada
self.ruta_escena = ruta_escena
self.escena = os.path.split(self.ruta_escena)[1]
self.ori = os.path.split(self.ruta_escena)[0]
self.base = os.path.split(self.ori)[0]
self.data = os.path.join(self.base, 'data')
self.geo = os.path.join(self.base, 'geo')
self.rad = os.path.join(self.base, 'rad')
self.nor = os.path.join(self.base, 'nor')
self.pro = os.path.join(self.base, 'pro')
# Definimos un grupo de variables de la escena
data_escena = self.escena.split('_')
self.sat = "L" + data_escena[0][-1]
sensores = {'L4': 'TM', 'L5': 'TM', 'L7': 'ETM+', 'L8': 'OLI', 'L9': 'OLI'}
self.sensor = sensores[self.sat]
self.nprocesado = data_escena[1]
self.path = data_escena[2][:3]
self.row = data_escena[2][-3:]
self.escena_date = data_escena[3]
self.escena_procesado_date = data_escena[4]
self.collection = data_escena[5]
self.tier = data_escena[6]
# Mascara de nuebes. Hay que hacerlo así porque sabe dios por qué ^!·/&"! los valores no son los mismos en OLI que en ETM+ y TM
if self.sensor == 'OLI':
self.cloud_mask_values = [21824, 21952]
else:
self.cloud_mask_values = [5440, 5504]
#Definimos nombre last
if self.sensor == 'ETM+':
self.last_name = (self.escena_date + self.sat + self.sensor[:-1] + self.path + '_' + self.row[1:]).lower()
else:
self.last_name = (self.escena_date + self.sat + self.sensor + self.path + '_' + self.row[1:]).lower()
# Definimos paths de salida
self.pro_escena = os.path.join(self.pro, self.last_name)
os.makedirs(self.pro_escena, exist_ok=True)
self.geo_escena = os.path.join(self.geo, self.last_name)
os.makedirs(self.geo_escena, exist_ok=True)
self.rad_escena = os.path.join(self.rad, self.last_name)
os.makedirs(self.rad_escena, exist_ok=True)
self.nor_escena = os.path.join(self.nor, self.last_name)
os.makedirs(self.nor_escena, exist_ok=True)
self.pro_escena = os.path.join(self.pro, self.last_name)
os.makedirs(self.pro_escena, exist_ok=True)
# Definimos máscaras a utilizar
self.equilibrado = os.path.join(self.data, 'Equilibrada.tif')
self.noequilibrado = os.path.join(self.data, 'NoEquilibrada.tif')
self.parametrosnor = {}
self.iter = 1
#CReamos un diccionario a partir del MTL
self.mtl = {}
for i in os.listdir(self.ruta_escena):
if i.endswith('MTL.txt'):
mtl = os.path.join(self.ruta_escena, i)
f = open(mtl, 'r')
for line in f.readlines():
if "=" in line:
l = line.split("=")
self.mtl[l[0].strip()] = l[1].strip()
#Creamos las variables con la franja del espectro de cada banda
if self.sat in ['L8', 'L9']:
for i in os.listdir(self.ruta_escena):
if i.endswith('.TIF'):
banda = os.path.splitext(i)[0].split('_')[-1]
if banda == 'B2':
self.blue = os.path.join(self.ruta_escena, i)
elif banda == 'B3':
self.green = os.path.join(self.ruta_escena, i)
elif banda == 'B4':
self.red = os.path.join(self.ruta_escena, i)
elif banda == 'B5':
self.nir = os.path.join(self.ruta_escena, i)
elif banda == 'B6':
self.swir1 = os.path.join(self.ruta_escena, i)
elif banda == 'B7':
self.swir2 = os.path.join(self.ruta_escena, i)
elif i.endswith('QA_PIXEL.TIF'):
self.qa = os.path.join(self.ruta_escena, i)
print(self.qa)
else:
continue
elif self.sat in ['L7', 'L5']:
for i in os.listdir(self.ruta_escena):
if i.endswith('.TIF'):
banda = os.path.splitext(i)[0].split('_')[-1]
if banda == 'B1':
self.blue = os.path.join(self.ruta_escena, i)
elif banda == 'B2':
self.green = os.path.join(self.ruta_escena, i)
elif banda == 'B3':
self.red = os.path.join(self.ruta_escena, i)
elif banda == 'B4':
self.nir = os.path.join(self.ruta_escena, i)
elif banda == 'B5':
self.swir1 = os.path.join(self.ruta_escena, i)
elif banda == 'B7':
self.swir2 = os.path.join(self.ruta_escena, i)
elif i.endswith('QA_PIXEL.TIF'):
self.qa = os.path.join(self.ruta_escena, i)
print(self.qa)
else:
continue
else:
print('No encuentro ninguna escena Landsat')
# Descargamos el quicklook de la escena
url_base = 'https://landsatlook.usgs.gov/gen-browse?size=rrb&type=refl&product_id={}'.format(self.mtl['LANDSAT_PRODUCT_ID'].strip('""'))
self.qk_name = os.path.join(self.ruta_escena, self.escena + '_Quicklook.jpeg')
if not os.path.exists(self.qk_name):
qk = open(self.qk_name, 'wb')
qk_open = urlopen(url_base)
urlimg = qk_open.read()
qk.write(urlimg)
qk.close()
print('QuicKlook descargado')
else:
print('El Quicklook ya estaba previamente descargado')
#print(url_base)
print('Landsat iniciada con éxito')
self.pn_cover = None
#Creamos el json para instarlo en la base de datos MongoDB
self.newesc = {'_id': self.last_name,
'usgs_id': self.mtl['LANDSAT_SCENE_ID'][1:-1],
'tier_id': self.mtl['LANDSAT_PRODUCT_ID'][1:-1],
'lpgs': self.mtl['PROCESSING_SOFTWARE_VERSION'][1:-1],
'category': self.mtl['COLLECTION_CATEGORY'][1:-1],
'Clouds': {'cloud_scene': float(self.mtl['CLOUD_COVER']),
'land cloud cover': float(self.mtl['CLOUD_COVER_LAND'])},
'Info': {'Tecnico': 'LAST-EBD Auto',
'Iniciada': datetime.now(), 'Pasos': {'rad': '', 'nor': ''}}}
try:
db.insert_one(self.newesc)
except Exception as e:
db.update_one({'_id': self.last_name}, {'$set':{'Info.Iniciada': datetime.now()}}, upsert=True)
#print("Unexpected error:", type(e)) #se Podria dar un error por clave unica, por eso en
#ese caso, lo que hacemos es actualizar la fecha en la que tratamos la imagen
print('Landsat instanciada y subida a la base de datos')
¿Qué podemos aportar al proyecto?
3 - Geoportal:
presente y
futuro
3 - Geoportal
http://venus.ebd.csic.es/imgs/
¿Qué podemos aportar al proyecto?
Máquina Virtual
Características
Software
download.py
protocolov2.py
productos.py
hidroperiodo.py
utils.py
run_download.sh
run_hydroperiod.sh
GitHub Repository
Código
Carpetas
/ori
/geo
/rad
/nor
/pro
/hyd
/data
/mongo
Busqueda y descarga
Hillshade
MDT
Geo & Rad
Productos
Normalización
Escena
Landsat
Pias
PIAs Ref
FMASK
Data
No
GeoServer
ICTS
LAST
{"_id": "20240815l9oli202_34",
"usgs_id": "LC92020342024228LGN01",
"tier_id": "LC09_L1TP_202034_20240815_20240815_02_T1",
"lpgs": "LPGS_16.4.0",
"category": "T1",
"Clouds": {"cloud_scene": 0.06,
"land cloud cover": 0.06,
"cloud_PN": 0 },
"Info": {
"Tecnico": "LAST-EBD Auto",
"Iniciada": {"$date": {"$numberLong": "1724690285816"}},
"Pasos": {"rad": "",
"nor": {"Normalize": "True",
"Nor-Values": {
"blue":
{"Parametros":
{"slope": 1.020172035444719,
"intercept": -0.00711237939135306,
"std": 0.016095327213406563,
"r": 0.98906989518987,
"N": 54886,
"iter": 1},
"Tipo_Area":
{"Mar": 49347,
"Embalses": 793,
"Pinar": 1564,
"Urbano-1": 575,
"Urbano-2": 944,
"Aeropuertos": 547,
"Arena": 664,
"Pastizales": 266,
"Mineria": 186}},
"Productos":
["NDVI", "NDWI", "MNDWI",
{"Flood": {
"El Rincon del Pescador": 72.72,
"Marismillas": 0.99,
"Caracoles": 0,
"FAO": 14.94,
"Marisma Occidental": 1.26,
"Marisma Oriental": 6.57,
"Entremuros": 115.29} },
"Turbidity",
"Depth" ]}
{"_id": "hidroperiodo_2023-2024",
"escenas": [
{"escena_id": "20240409l9oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 19896.300000000003},
{"escena_id": "20240503l8oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 11161.53},
{"escena_id": "20240511l9oli202_34",
"nubes_marismas": 8.31,
"ha_inundacion": 8354.789999999999},
{"escena_id": "20240527l9oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 3754.53},
{"escena_id": "20240604l8oli202_34",
"nubes_marismas": 0.06,
"ha_inundacion": 2661.84},
{"escena_id": "20240714l9oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 249.93},
{"escena_id": "20240722l8oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 215.1},
{"escena_id": "20240807l8oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 209.43},
{"escena_id": "20240815l9oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 211.76999999999998},
{"escena_id": "20231008l8oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 305.91},
{"escena_id": "20231024l8oli202_34",
"nubes_marismas": 7.69,
"ha_inundacion": 1131.8400000000001},
{"escena_id": "20231117l9oli202_34",
"nubes_marismas": 0,
"ha_inundacion": 408.51},
{"escena_id": "20231125l8oli202_34",
"nubes_marismas": 0.03,
"ha_inundacion": 421.83},
{"escena_id": "20231219l9oli202_34",
"nubes_marismas": 4.76,
"ha_inundacion": 589.5899999999999},
{"escena_id": "20231227l8oli202_34",
"nubes_marismas": 0.63,
"ha_inundacion": 512.8199999999999},
{"escena_id": "20240112l8oli202_34",
"nubes_marismas": 0.09,
"ha_inundacion": 696.69},
{"escena_id": "20240120l9oli202_34",
"nubes_marismas": 0.01,
"ha_inundacion": 1138.59},
{"escena_id": "20240221l9oli202_34",
"nubes_marismas": 7.73,
"ha_inundacion": 4288.32},
{"escena_id": "20240229l8oli202_34",
"nubes_marismas": 0.01,
"ha_inundacion": 3901.3200000000006},
{"escena_id": "20240316l8oli202_34",
"nubes_marismas": 6.05,
"ha_inundacion": 4150.98}]}
Landsat
Hidroperiodo
dtm
fmask_escena
hillshade_escena
ndvi_escena
ndwi_escena
mndwi_escena
cob_veg
ndvi_p10
ndvi_mean
fmask
mndwi
ndwi
swir1
Criterios:
Swir1 <= 0.12
slope > 8 (**embalses NDWI_scene & MNDWI_scene**)
hillshade < p30
ndvip10 > 0.3 & ndvimean > 0.5
cobveg > 75
ndvi_scene > 0.6 & dtm > 2.5
fmask_scene clouds & shadows
sum(reclass((fmask_scene, ndwi_scene, mndwi_scene)) >= 2
Valid days
Hydroperiod
nº días inundado / nº días válidos * 365
ciclo / media(84-24)
Gracias por su atención
By Diego García Díaz
GIS & Remote Sensing. Python & Statistics. Surf & MTB :)