diff --git a/Common/geotools.py b/Common/geotools.py new file mode 100644 index 0000000000000000000000000000000000000000..646c2d2db27795f091f71e784baf0a9defcf8526 --- /dev/null +++ b/Common/geotools.py @@ -0,0 +1,23 @@ +from osgeo import gdal +from osgeo import ogr +from pyproj import Transformer as T + +def get_query_bbox(shp, query_srs=4326, return_all=False): + ds = ogr.Open(shp) + ly = ds.GetLayer(0) + xt = ly.GetExtent() + shp_srs = int(ly.GetSpatialRef().GetAuthorityCode(None)) + + i_bbox = [xt[0], xt[2], xt[1], xt[3]] + if shp_srs == query_srs: + bbox = i_bbox + else: + tr = T.from_crs(shp_srs, query_srs, always_xy=True) + lon_min, lat_min = tr.transform(xt[0], xt[2]) + lon_max, lat_max = tr.transform(xt[1], xt[3]) + bbox = [lon_min, lat_min, lon_max, lat_max] + + if return_all: + return bbox, i_bbox, shp_srs + else: + return bbox \ No newline at end of file diff --git a/TimeSeries/s1base.py b/TimeSeries/s1base.py index d0cbd0977f20ecc56b809f40338e67e3eecc813f..923b2f0d8eeb741340599eb3ba5d74ed2cd3b0af 100644 --- a/TimeSeries/s1base.py +++ b/TimeSeries/s1base.py @@ -7,13 +7,34 @@ import uuid import xml.etree.ElementTree as ET import shutil from itertools import groupby +from Common.geotools import get_query_bbox +from eodag import EODataAccessGateway + +def fetch(shp, dt, output_fld, credentials): + bbox = get_query_bbox(shp) + dag = EODataAccessGateway(user_conf_file_path=credentials) + dag.set_preferred_provider("peps") + search_criteria = { + "productType": "S1_SAR_GRD", + "start": dt.split("/")[0], + "end": dt.split("/")[1], + "geom": {"lonmin": bbox[0], "latmin": bbox[1], "lonmax": bbox[2], "latmax": bbox[3]} + } + res = dag.search_all(**search_criteria) + res.filter_property(sensorMode='IW') + if len(res) > 0: + os.makedirs(output_fld, exist_ok=True) + dag.download_all(res, outputs_prefix=output_fld, extract=True) + + return S1GRDPipeline(output_fld) + class S1GRDPipeline: # --- BEGIN SENSOR PROTOTYPE --- NAME = 'S1-IW-GRD' VAL_TYPE = otb.ImagePixelType_int16 TMP_TYPE = otb.ImagePixelType_float - PTRN_dir = 'S1*_IW_GRD*.SAFE' + PTRN_dir = 'S1*_IW_GRD*/S1*_IW_GRD*.SAFE' PTRN_ref = '-iw-grd-' VH_name = 'vh' PTRN = ['measurement/s1*-iw-grd-vh-*.tiff', 'measurement/s1*-iw-grd-vv-*.tiff'] diff --git a/TimeSeries/s1planetary.py b/TimeSeries/s1planetary.py index e104c9303c0fcb4567c183525a81c6e229b21ae8..28e04c2685f4fd36684e5ef2a3a85b013e3eb0eb 100644 --- a/TimeSeries/s1planetary.py +++ b/TimeSeries/s1planetary.py @@ -1,9 +1,7 @@ import os.path - from TimeSeries.s1base import * import planetary_computer as PC from pystac_client import Client -from osgeo import ogr from pyproj import Transformer as T from shapely.geometry import Polygon import rasterio @@ -11,24 +9,12 @@ import rasterio.mask import tqdm import otbApplication as otb import json +from Common.geotools import get_query_bbox def fetch(shp, dt, output_fld, api_key, direction=None): os.environ['PC_SDK_SUBSCRIPTION_KEY'] = api_key - ds = ogr.Open(shp) - ly = ds.GetLayer(0) - xt = ly.GetExtent() - shp_srs = int(ly.GetSpatialRef().GetAuthorityCode(None)) - qry_srs = 4326 - - i_bbox = [xt[0], xt[2], xt[1], xt[3]] - if shp_srs == qry_srs: - bbox = i_bbox - else: - tr = T.from_crs(shp_srs, qry_srs, always_xy=True) - lon_min, lat_min = tr.transform(xt[0], xt[2]) - lon_max, lat_max = tr.transform(xt[1], xt[3]) - bbox = [lon_min, lat_min, lon_max, lat_max] + bbox, i_bbox, shp_srs = get_query_bbox(shp, return_all=True) api = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1', modifier=PC.sign_inplace) res = api.search(collections="sentinel-1-rtc", bbox=bbox, datetime=dt) diff --git a/TimeSeries/s2planetary.py b/TimeSeries/s2planetary.py index a0de40366d7655da645181a4762821b2376245fa..928ce5399ea04c0120d91aec304639f6924d6e33 100644 --- a/TimeSeries/s2planetary.py +++ b/TimeSeries/s2planetary.py @@ -1,31 +1,16 @@ -import warnings - from TimeSeries.s2sen2cor import * import planetary_computer as PC from pystac_client import Client import rasterio import rasterio.mask -from osgeo import ogr +from Common.geotools import get_query_bbox from pyproj import Transformer as T from shapely.geometry import Polygon import tqdm def fetch(shp, dt, output_fld, band_list=None): - ds = ogr.Open(shp) - ly = ds.GetLayer(0) - xt = ly.GetExtent() - shp_srs = int(ly.GetSpatialRef().GetAuthorityCode(None)) - qry_srs = 4326 - - i_bbox = [xt[0], xt[2], xt[1], xt[3]] - if shp_srs == qry_srs: - bbox = i_bbox - else: - tr = T.from_crs(shp_srs,qry_srs,always_xy=True) - lon_min, lat_min = tr.transform(xt[0], xt[2]) - lon_max, lat_max = tr.transform(xt[1], xt[3]) - bbox = [lon_min, lat_min, lon_max, lat_max] + bbox, i_bbox, shp_srs = get_query_bbox(shp, return_all=True) api = Client.open('https://planetarycomputer.microsoft.com/api/stac/v1', modifier=PC.sign_inplace) res = api.search(collections="sentinel-2-l2a", bbox=bbox, datetime=dt) diff --git a/TimeSeries/s2theia.py b/TimeSeries/s2theia.py index 39593e2c02b22f55eae467783853a3f5bbdeecc6..bd9ff9f0b2035064dbc3eb296f1fb2426d5ecfce 100644 --- a/TimeSeries/s2theia.py +++ b/TimeSeries/s2theia.py @@ -1,11 +1,7 @@ -import sys import warnings - -from osgeo import gdal,ogr +from osgeo import gdal import otbApplication as otb from theia_picker import TheiaCatalog -from pyproj import Transformer as T -import tqdm from Common.otb_numpy_proc import to_otb_pipeline import numpy as np @@ -17,25 +13,11 @@ from osgeo import osr import datetime import uuid import shutil - +from Common.geotools import get_query_bbox from Common.geometry import get_displacements_to_ref, get_clearest_central_image def fetch(shp, dt, output_fld, credentials): - - ds = ogr.Open(shp) - ly = ds.GetLayer(0) - xt = ly.GetExtent() - shp_srs = int(ly.GetSpatialRef().GetAuthorityCode(None)) - qry_srs = 4326 - - i_bbox = [xt[0], xt[2], xt[1], xt[3]] - if shp_srs == qry_srs: - bbox = i_bbox - else: - tr = T.from_crs(shp_srs,qry_srs,always_xy=True) - lon_min, lat_min = tr.transform(xt[0], xt[2]) - lon_max, lat_max = tr.transform(xt[1], xt[3]) - bbox = [lon_min, lat_min, lon_max, lat_max] + bbox = get_query_bbox(shp) theia = TheiaCatalog(credentials) features = theia.search( @@ -48,11 +30,8 @@ def fetch(shp, dt, output_fld, credentials): lst = ['FRE_B2','FRE_B3','FRE_B4','FRE_B5','FRE_B6','FRE_B7','FRE_B8','FRE_B8A', 'FRE_B11','FRE_B12','EDG_R1','SAT_R1','CLM_R1'] - #prg = tqdm.tqdm(total=len(features) * 13, desc="Fetching from Theia") for f in features: f.download_files(matching=lst, download_dir=output_fld) - #prg.update() - #prg.close() return S2TheiaPipeline(output_fld) diff --git a/docker/dockerfile b/docker/dockerfile index 38766075c4abfa6bab8bdef5beba6a8453ce4cad..47c533127a544f19c49781e5ee1ced33e1e492a4 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -115,7 +115,8 @@ RUN pip install gdal==3.4.2 \ planetary_computer \ theia_picker \ fpdf2 \ - planet + planet \ + eodag RUN mkdir moringav2 COPY --chown=ubuntu . /home/ubuntu/moringav2 \ No newline at end of file diff --git a/moringa.py b/moringa.py index f297185e99d2272e46458a4ebd8cde05ad7f1066..c372a65449a98a8c60ee7630d4b5c64d593636f7 100755 --- a/moringa.py +++ b/moringa.py @@ -3,7 +3,7 @@ import sys import argparse import OBIA.segmentation import VHR.vhrbase -from TimeSeries import s2theia, s2planetary +from TimeSeries import s2theia, s2planetary, s1base, s1planetary def run_segmentation(img, threshold, cw, sw , out_seg, n_first_iter, margin, roi, n_proc, memory, @@ -32,8 +32,8 @@ def preprocess_spot67(in_fld, out_fld, dem_fld, geoid_file, skip_ps, compress, sp.write_outputs(out_fld, compress=compress) return -def preprocess_s2(in_fld, out_fld, output_dates_file=None, roi=None, coregister_to=None, coregister_to_band=1, - coregister_using_band=3, provider='theia'): +def preprocess_s2(in_fld, out_fld, output_dates_file=None, roi=None, + align_to=None, align_to_band=3, align_using_band=3, provider='theia'): S2Processor = None if provider == 'theia': S2Processor = s2theia.S2TheiaPipeline @@ -50,23 +50,50 @@ def preprocess_s2(in_fld, out_fld, output_dates_file=None, roi=None, coregister_ else: raise ValueError("Please provide path to a text file containing output dates.") - s2.extract_feature_set(out_fld, store_gapfill=True, mosaicking='vrt') + align = align_to is not None + if (align_to == 'self'): + align_to = None + + s2.extract_feature_set(out_fld, store_gapfill=True, mosaicking='vrt', align=align, + align_to=align_to, align_to_band=align_to_band, align_using_band=align_using_band) + return + +def preprocess_s1(in_fld, roi, out_fld, dem_fld=None, geoid=None, direction=None, satellite=None, + skip_despeckle=False, provider='native'): + S1processor = None + if provider == 'native': + S1Processor = s1base.S1GRDPipeline + elif provider == 'planetary': + S1Processor = s1planetary.S1RTCPipeline + else: + raise ValueError("Unsupported/non-valid provider") + + s1 = S1Processor(in_fld, roi, out_fld, direction=direction, satellite=satellite) + if provider == 'native': + assert(os.path.exists(dem_fld) and os.path.exists(geoid)) + s1.calibrate() + s1.orthorectify(dem_fld, geoid) + s1.stitch() + if not skip_despeckle: + s1.multitemp_speckle_filter() + s1.compute_features() + s1.write_outputs(out_fld) return def main(args): parser = argparse.ArgumentParser(prog="moringa", add_help=False) subpar = parser.add_subparsers(dest="cmd") - prepr = subpar.add_parser("preprocess_s2", help="Performs Moringa preset time series preprocessing.", + prepr = subpar.add_parser("preprocess_s2", help="Performs Moringa preset time series preprocessing for Sentinel-2.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) prepr.add_argument("in_folder", type=str, help="Path to the folder containing (de-zipped) S2 images.") prepr.add_argument("out_folder", type=str, help="Path to the folder in which preprocessed stacks will be stored.") prepr.add_argument("--output_dates_file", type=str, default=None, help="Path to the text file containing output dates for temporal interpolation.") prepr.add_argument("--roi", type=str, default=None, help="Path to the ROI vector file.") - prepr.add_argument("--coregister_to", type=str, default=None, help="Path to a reference image to which the stacks must be coregistered.") - prepr.add_argument("--coregister_to_band", type=int, default=1, help="Band of reference image used for co-registration.") - prepr.add_argument("--coregister_using_band", type=int, default=3, help="Band of current stack used for co-registration.") - prepr.add_argument("--provider", type=str, default='theia', help="S2 image provider. Supported: 'theia', 'theial3a', 'sen2cor', 'planetary'") + prepr.add_argument("--align_to", type=str, default=None, help="Optional strategy for S2 spatial alignment (self/<date>/path to reference image).") + prepr.add_argument("--align_to_band", type=int, default=3, help="Band of reference image used for co-registration.") + prepr.add_argument("--align_using_band", type=int, default=3, help="Band of current stack used for co-registration.") + prepr.add_argument("--provider", type=str, default='theia', help="S2 image provider. Currently supported: 'theia', 'planetary'") segmt = subpar.add_parser("segment", help="Performs (large scale Baatz-Shape) segmentation of an input image.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -97,7 +124,18 @@ def main(args): vhrprep.add_argument("--skip_ps", help="Skip pansharpening step.", action='store_true') vhrprep.add_argument("--compress", help="Use lossless compression on outputs.", action='store_true') - + s1prepr = subpar.add_parser("preprocess_s1", help="Performs Moringa preset time series preprocessing for Sentinel-1.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + s1prepr.add_argument("in_folder", type=str, help="Path to the folder containing (de-zipped) S1 images.") + s1prepr.add_argument("roi", type=str, default=None, help="Path to an image whose geometry has to be fit (e.g. a S2 scene).") + s1prepr.add_argument("out_folder", type=str, help="Path to the folder in which preprocessed stacks will be stored.") + s1prepr.add_argument("--dem_fld", type=str, default=None, help="Path to the folder containing DEM covering the scene in WGS84 projection.") + s1prepr.add_argument("--geoid", type=str, default=None, help="Path to the geoid file.") + s1prepr.add_argument("--direction", type=str, default=None, help="Filter direction (ascending/descending)") + s1prepr.add_argument("--satellite", type=str, default=None, help="Filter satellite (s1a/s1b)") + s1prepr.add_argument("--skip_despeckle", help="Skip despeckling step", action='store_true') + s1prepr.add_argument("--provider", type=str, default='native', + help="S1 image provider. Currently supported: 'native' (e.g. esa/peps), 'planetary'") if len(args) == 1: parser.print_help() @@ -107,7 +145,8 @@ def main(args): if arg.cmd == "preprocess_s2": preprocess_s2(arg.in_folder, arg.out_folder, output_dates_file=arg.output_dates_file, roi=arg.roi, - coregister_to=arg.coregister_to, provider=arg.provider) + align_to=arg.align_to, align_to_band=arg.align_to_band, align_using_band=arg.align_using_band, + provider=arg.provider) if arg.cmd == "segment": run_segmentation(arg.img, arg.threshold, arg.cw, arg.sw, arg.outimg, arg.n_first_iter, arg.tile_margin, @@ -117,6 +156,10 @@ def main(args): preprocess_spot67(arg.fld, arg.out_fld, arg.dem_fld, arg.geoid, arg.skip_ps, arg.compress, arg.clip, arg.align_to, arg.align_to_band, arg.align_using_band) + if arg.cmd == "preprocess_s1": + preprocess_s1(arg.fld, arg.roi, arg.out_fld, arg.dem_fld, arg.geoid, arg.direction, arg.satellite, + arg.skip_despeckle, arg.provider) + return 0 if __name__ == "__main__":