diff --git a/sen2chain/cloud_mask.py b/sen2chain/cloud_mask.py index 490c305913c19fc2025d3f9c559d156c731fa8a6..437cb115a50caa0108b915999c81160c5a700e5d 100755 --- a/sen2chain/cloud_mask.py +++ b/sen2chain/cloud_mask.py @@ -8,16 +8,18 @@ import logging import pathlib from pathlib import Path import pyproj +import otbApplication import shapely.geometry from shapely.geometry import box, Polygon, MultiPolygon, GeometryCollection, mapping, shape from shapely.ops import transform import rasterio from rasterio.features import shapes, rasterize import numpy as np +from typing import Mapping, Sequence, AbstractSet, List, Set, Dict, Tuple, Optional, Union import gdal -import otbApplication +import time +import os # type annotations -from typing import Mapping, Sequence, AbstractSet, List, Set, Dict, Tuple, Optional, Union from .config import Config @@ -254,14 +256,10 @@ def create_cloud_mask_v2(cloud_mask: Union[str, pathlib.PosixPath], out_path="./ LOGGER.info("Creating cloud-mask_v2") out_temp_path = Path(Config().get("temp_path")) - # LOGGER.info(out_temp_path) - # LOGGER.info(str(out_temp_path)) - # ATTENTION, si jamais des traitements en parallèle: les fichiers - # temporaires auront le même nom => conflits - # à utiliser uniquement pour des tests - out_bin = str(out_temp_path / "temp_bin.tif") - out_erode = str(out_temp_path / "temp_erode.tif") - out_dilate = str(out_temp_path / "temp_dilate.tif") + out_mil_epoch = str(int(time.time()*1000)) + out_bin = str(out_temp_path / Path("tmp_bin_" + out_mil_epoch + ".tif")) + out_erode = str(out_temp_path / Path("tmp_erode_" + out_mil_epoch + ".tif")) + out_dilate = str(out_temp_path / Path("tmp_dilate" + out_mil_epoch + ".tif")) # Seuillage du masque nuage Sen2Cor CLD_seuil = 25 @@ -271,7 +269,6 @@ def create_cloud_mask_v2(cloud_mask: Union[str, pathlib.PosixPath], out_path="./ BandMath.SetParameterOutputImagePixelType("out", 0) BandMath.SetParameterString("exp", "im1b1 > " + str(CLD_seuil)) BandMath.ExecuteAndWriteOutput() - LOGGER.info("bin fini") # Erosion BinaryMorphologicalOperation = otbApplication.Registry.CreateApplication("BinaryMorphologicalOperation") @@ -283,9 +280,7 @@ def create_cloud_mask_v2(cloud_mask: Union[str, pathlib.PosixPath], out_path="./ BinaryMorphologicalOperation.SetParameterInt("structype.ball.yradius", 1) BinaryMorphologicalOperation.SetParameterFloat("filter.erode.foreval", 1) BinaryMorphologicalOperation.SetParameterString("filter","erode") - LOGGER.info("erosion debut") BinaryMorphologicalOperation.ExecuteAndWriteOutput() - LOGGER.info("erosion fini") # Dilatation BinaryMorphologicalOperation = otbApplication.Registry.CreateApplication("BinaryMorphologicalOperation") @@ -298,16 +293,15 @@ def create_cloud_mask_v2(cloud_mask: Union[str, pathlib.PosixPath], out_path="./ BinaryMorphologicalOperation.SetParameterFloat("filter.dilate.foreval", 1) BinaryMorphologicalOperation.SetParameterString("filter","dilate") BinaryMorphologicalOperation.ExecuteAndWriteOutput() - LOGGER.info("dilatation fini") - # Enregistrement JP2000 + # Save to JP2000 src_ds = gdal.Open(out_dilate) driver = gdal.GetDriverByName("JP2OpenJPEG") dst_ds = driver.CreateCopy(str(out_path), src_ds, options=["CODEC=JP2", "QUALITY=100", "REVERSIBLE=YES", "YCBCR420=NO"]) dst_ds = None src_ds = None - #os.remove(out_bin) - #os.remove(out_erode) - #os.remove(out_dilate) + os.remove(out_bin) + os.remove(out_erode) + os.remove(out_dilate) diff --git a/sen2chain/colormap.py b/sen2chain/colormap.py index b0660b43ffd2472221ed1affe5e8ef5db56ddf1d..917f0351247e48cc37ceaa965999ea32b59a585a 100644 --- a/sen2chain/colormap.py +++ b/sen2chain/colormap.py @@ -107,7 +107,7 @@ def create_colormap(raster: Union[str, pathlib.PosixPath], :param clouds_color: color of the clouds (black or white). :param out_resolution: output resolution. """ - LOGGER.info("creating colormap") +# LOGGER.info("creating colormap") # cloud mask if not Path(str(cloud_mask)).is_file(): @@ -117,12 +117,16 @@ def create_colormap(raster: Union[str, pathlib.PosixPath], # clouds color if clouds_color.lower() == "black": - lut_dict[0] = (0, 0, 0) + cld_val = 0 + #lut_dict[0] = (0, 0, 0) elif clouds_color.lower() == "white": - lut_dict[0] = (255, 255, 255) + cld_val = 255 + #lut_dict[0] = (255, 255, 255) else: LOGGER.warning('Available clouds colors: "black" or "white" \ \nApplying default: black') + lut_dict[0] = (0, 0, 0) + lut_dict[255] = (255, 255, 255) # check out_crs if out_crs: @@ -131,34 +135,32 @@ def create_colormap(raster: Union[str, pathlib.PosixPath], except Exception as e: out_crs = None print("Invalid CRS: {}\nUsing source raster CRS".format(e)) - + with rasterio.open(str(raster)) as src: raster_band = src.read(1) profile = src.profile - + if cloud_mask: with rasterio.open(str(cloud_mask)) as cld_src: clouds_band = cld_src.read(1) - # resample cloud_mask to raster grid cld_reproj = np.empty(raster_band.shape, dtype=np.uint8) reproject(source=clouds_band, destination=cld_reproj, - src_transform=cld_src.affine, + src_transform=cld_src.transform, src_crs=cld_src.crs, - dst_transform=src.affine, + dst_transform=src.transform, dst_crs=src.crs, resampling=Resampling.nearest) - # clouds - raster_band = np.where(cld_reproj == 0, raster_band, 32767) + raster_band = np.where(cld_reproj == 0, raster_band, 16383) #band_mask_borders = np.where(raster_band != 32767, raster_band, -10000) - cmap = np.where(raster_band == 32767, -10000, raster_band) + cmap = np.where(raster_band == 16383, -10000, raster_band) cmap = (128 * (cmap/10000 + 1) * ((cmap+10000) > 0)).astype(np.uint8) - + cmap = np.where(cld_reproj == 1, cld_val, cmap) # compute default transform, width and height to fit the out resolution dst_transform, dst_width, dst_height = calculate_default_transform( src.crs, @@ -170,12 +172,13 @@ def create_colormap(raster: Union[str, pathlib.PosixPath], out_crs = src.crs if not out_crs else out_crs profile.update(nodata=None, + driver="Gtiff", + #compress="DEFLATE", dtype=np.uint8, transform=dst_transform, width=dst_width, height=dst_height, crs=out_crs) - # write colormap to out_path with rasterio.open(str(out_path), "w", **profile) as dst: dst.write(cmap, 1) diff --git a/sen2chain/indices.py b/sen2chain/indices.py index 07c97e4b20b4aae803b67221a746b769eba52d27..d1142066f275bf2fef2a39775910029db863c755 100644 --- a/sen2chain/indices.py +++ b/sen2chain/indices.py @@ -9,9 +9,10 @@ from pathlib import Path from abc import ABCMeta, abstractmethod, abstractproperty from matplotlib import cm from typing import List +import os from .indices_functions import (create_raw_ndvi, create_raw_ndwi, - create_masked_indice) + create_masked_indice, ndvi_tiff_2_jp2) from .colormap import matplotlib_colormap_to_rgb, create_colormap logger = logging.getLogger(__name__) @@ -55,7 +56,8 @@ class Ndvi(Indice): """ name = "NDVI" filename_template = "{product_identifier}_NDVI{ext}" - ext = ".tif" + ext = ".jp2" + ext_raw = ".tif" colormap = cm.RdYlGn # _colors_table_path = functions_data_path = Path("../share/data/RdYlGn.lut") @@ -69,35 +71,40 @@ class Ndvi(Indice): self.out_path = None # filenames - self.indice_stem = self.filename_template.format(product_identifier=self.l2a_product.identifier, - ext="") + self.indice_stem = self.filename_template.format(product_identifier=self.l2a_product.identifier, ext="") self.indice_filename = self.indice_stem + self.ext - + self.indice_raw = self.indice_stem + self.ext_raw def process_indice(self, out_path, reprocess=False, nodata_clouds=False, quicklook=False): """ process """ self.out_path = out_path - if (out_path / self.indice_filename).exists() and not reprocess: logger.info("{} already exists".format(self.indice_filename)) else: create_raw_ndvi(nir_path=self.l2a_product.b08_10m, vir_path=self.l2a_product.b04_10m, + out_path=(out_path / self.indice_raw)) + ndvi_tiff_2_jp2(img_path=(out_path / self.indice_raw), out_path=(out_path / self.indice_filename)) - if nodata_clouds: - if self.l2a_product.user_cloud_mask is None: raise ValueError("Cloud mask does not exist") masked_indice_filename = self.indice_stem + "_MASKED" + self.ext - - if (self.out_path / masked_indice_filename).exists() and not reprocess: + masked_indice_raw = self.indice_stem + "_MASKED" + self.ext_raw + + if (out_path / masked_indice_filename).exists() and not reprocess: logger.info("{} already exists".format(masked_indice_filename)) else: - create_masked_indice(raw_indice_path=(self.out_path / self.indice_filename), + if (out_path / self.indice_raw).exists(): + ndvi_name = (out_path / self.indice_raw) + else: + ndvi_name = (out_path / self.indice_filename) + create_masked_indice(indice_path=ndvi_name, cloud_mask_path=self.l2a_product.user_cloud_mask, - out_path=(self.out_path / masked_indice_filename)) - + out_path=(out_path / masked_indice_raw)) + ndvi_tiff_2_jp2(img_path=(out_path / masked_indice_raw), + out_path=(out_path / masked_indice_filename)) + os.remove(str(out_path / masked_indice_raw)) if quicklook: cmap = matplotlib_colormap_to_rgb(self.colormap, revers=False) @@ -106,13 +113,15 @@ class Ndvi(Indice): if (self.out_path / quicklook_filename).exists() and not reprocess: logger.info("{} already exists".format(quicklook_filename)) else: + logger.info("creating quicklook") create_colormap(raster=(self.out_path / self.indice_filename), cloud_mask=self.l2a_product.user_cloud_mask, # lut_dict=get_colormap(self._colors_table_path.absolute()), - lut_dict=cmap, + lut_dict=cmap, clouds_color="white", out_path=(self.out_path / quicklook_filename)) + class NdwiGao(Indice): """ NDWI(Gao) = (NIR-SWIR) / (NIR+SWIR) diff --git a/sen2chain/indices_functions.py b/sen2chain/indices_functions.py index 3d2cae595ce0da92e6d5f320778f4f5d3059cb1b..52da49f8bffdf2adeec6ee4785b1e9e0ba2f5daa 100644 --- a/sen2chain/indices_functions.py +++ b/sen2chain/indices_functions.py @@ -7,10 +7,13 @@ This module contains functions to compute radiometric indices. import logging import pathlib from pathlib import Path +import otbApplication import rasterio from rasterio.warp import reproject, Resampling import numpy as np from typing import Union +from .config import Config +import gdal LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -26,30 +29,26 @@ def create_raw_ndvi(nir_path: Union[str, pathlib.PosixPath], :param vir_path: path to the VIR raster. :param out_path: path to the output raster. """ - LOGGER.info("creating raw NDVI (int16)") + LOGGER.info("creating raw NDVI (tiff - int16)") with rasterio.open(str(nir_path)) as nir_src, \ rasterio.open(str(vir_path)) as vir_src: - nir_profile = nir_src.profile - # vir_profile = vir_src.profile - np.seterr(divide='ignore', invalid='ignore') # ignore warnings when dividing by zero nir = nir_src.read(1).astype(np.float32) vir = vir_src.read(1).astype(np.float32) ndvi = ((nir - vir) / (nir + vir)*10000).astype(np.int16) - ndvi_masked = np.where(nir != 0, ndvi, 32767) nir_profile.update(driver="Gtiff", - compress="DEFLATE", - tiled=False, - dtype=np.int16, - nodata=32767, - transform=nir_src.affine) - - with rasterio.open(str(out_path), "w", **nir_profile) as dst: - dst.write(ndvi_masked, 1) - + compress="DEFLATE", + tiled=False, + dtype=np.int16, + nodata=32767, + transform=nir_src.transform) + nir_profile.pop('tiled', None) + with rasterio.Env(GDAL_CACHEMAX=512) as env: + with rasterio.open(str(out_path), "w", **nir_profile) as dst: + dst.write(ndvi_masked, 1) return Path(str(out_path)).absolute @@ -102,14 +101,13 @@ def create_raw_ndwi(nir_path: Union[str, pathlib.PosixPath], return Path(str(out_path)).absolute - -def create_masked_indice(raw_indice_path: Union[str, pathlib.PosixPath], +def create_masked_indice(indice_path: Union[str, pathlib.PosixPath], cloud_mask_path: Union[str, pathlib.PosixPath], out_path: Union[str, pathlib.PosixPath]="./masked_indice.tif") -> pathlib.PosixPath: """ Masks an indice raster with a cloud mask. - :param raw_indice_path: path to the NDVI raster. + :param indice_path: path to the NDVI raster. :param cloud_mask_path: path to the cloud mask raster. :param out_path: path to the output raster. """ @@ -117,30 +115,51 @@ def create_masked_indice(raw_indice_path: Union[str, pathlib.PosixPath], LOGGER.info("Cloud-masking indice (int16)") - with rasterio.open(str(raw_indice_path)) as indice_src, \ + with rasterio.open(str(indice_path)) as indice_src, \ rasterio.open(str(cloud_mask_path)) as cld_src: profile = indice_src.profile raw_indice = indice_src.read(1) cld = cld_src.read(1) - # repoject cloud_mask to ndvi resolution cld_reproj = np.empty(raw_indice.shape, dtype=np.uint8) reproject(source=cld, destination=cld_reproj, - src_transform=cld_src.affine, + src_transform=cld_src.transform, src_crs=cld_src.crs, - dst_transform=indice_src.affine, + dst_transform=indice_src.transform, dst_crs=indice_src.crs, resampling=Resampling.nearest) +# indice_borders_mask = np.where(raw_indice > 0, raw_indice, 32767) + indice_cloud_mask = np.where(cld_reproj == 0, raw_indice, 32767) - indice_borders_mask = np.where(raw_indice > 0, raw_indice, 32767) - indice_cloud_mask = np.where(cld_reproj == 0, indice_borders_mask, 32767) + profile.update(driver="Gtiff", + compress="DEFLATE", + tiled=False, + dtype=np.int16, + nodata=32767, + transform=indice_src.transform) +# profile.pop('tiled', None) - profile.update(nodata=32767, - transform=indice_src.affine) + with rasterio.Env(GDAL_CACHEMAX=512) as env: + with rasterio.open(str(out_path), "w", **profile) as dst: + dst.write(indice_cloud_mask, 1) + + return str(Path(str(out_path)).absolute) - with rasterio.open(str(out_path), "w", **profile) as dst: - dst.write(indice_cloud_mask, 1) +def ndvi_tiff_2_jp2(img_path: Union[str, pathlib.PosixPath], + out_path: Union[str, pathlib.PosixPath]="./ndvi_2_jp2.jp2") -> pathlib.PosixPath: + """ + Convert a NDVI file from TIF to JP2. + :param nir_path: path to the NIR raster. + :param vir_path: path to the VIR raster. + :param out_path: path to the output raster. + """ + LOGGER.info("converting raw NDWI to JPEG2000") + src_ds = gdal.Open(str(img_path)) + driver = gdal.GetDriverByName("JP2OpenJPEG") + dst_ds = driver.CreateCopy(str(out_path), src_ds, options=['NBITS=15', 'CODEC=JP2', 'QUALITY=20']) + dst_ds = None + src_ds = None return str(Path(str(out_path)).absolute) diff --git a/sen2chain/products.py b/sen2chain/products.py index 6516d71b41c12407d4c99ebb7fd1f1a12a783ef7..bc76b5fa7876b94ae6e28eb20340676b3c525724 100755 --- a/sen2chain/products.py +++ b/sen2chain/products.py @@ -345,7 +345,7 @@ class L2aProduct(Product): self.indices_path = self._indices_library_path # user cloud mask - self.user_cloud_mask = self.path.parent / (self.identifier + "_CLOUD_MASK.tif") + self.user_cloud_mask = self.path.parent / (self.identifier + "_CLOUD_MASK.jp2") if not self.user_cloud_mask.exists(): self.user_cloud_mask = None @@ -427,7 +427,8 @@ class L2aProduct(Product): raise ValueError("Indice not defined") if out_path is None: - indice_path = self.indices_path / indice.upper() / self.tile + #logger.info(self.identifier) + indice_path = self.indices_path / indice.upper() / self.tile / self.identifier else: indice_path = Path(out_path) / (self.identifier + "_INDICES") / indice.upper() indice_path.mkdir(parents=True, exist_ok=True) diff --git a/sen2chain/tiles.py b/sen2chain/tiles.py index e8235faa44b840d806bd317a35185c896ae87e36..5b604e3a8e9f3f2c249d4fd9f2fc5714fd5eadb3 100644 --- a/sen2chain/tiles.py +++ b/sen2chain/tiles.py @@ -202,7 +202,11 @@ class Tile: self._products["indices"][indice] = IndicesList() indice_template = IndicesCollection.get_indice_cls(indice.upper()).filename_template indice_ext = IndicesCollection.get_indice_cls(indice.upper()).ext - for f in path.glob("*{}".format(indice_ext)): + file_patterns = [indice_ext, 'QUICKLOOK.tif'] + files_selected = [] + for p in file_patterns: + files_selected.extend(path.glob("*/*{}".format(p))) + for f in files_selected: try: indice_pattern = re.sub("{.*?}", "", indice_template) remove_pattern = "{}.*".format(indice_pattern)