diff --git a/TimeSeries/landsat_planetary.py b/TimeSeries/landsat_planetary.py index aa2db6a897a9ce51c8313ce629efd461e37b1c44..881d6cf34a33d4b7ef22e1c99db266e5298b4ab8 100644 --- a/TimeSeries/landsat_planetary.py +++ b/TimeSeries/landsat_planetary.py @@ -1,3 +1,4 @@ +from TimeSeries.s2theia import * from Common.geotools import get_query_bbox from pystac_client import Client import planetary_computer as PC @@ -52,4 +53,133 @@ def fetch(shp, dt, out_fld, auth=None, band_list=None): with rasterio.open(ofn, "w", **out_meta) as dest: dest.write(out_img) prg.update() - prg.close() \ No newline at end of file + prg.close() + + +class Landsat89PlanetaryTilePipeline(S2TheiaTilePipeline): + # --- BEGIN SENSOR PROTOTYPE --- + + NAME = 'LANDSAT89-PLANETARY' + REF_TYPE = otb.ImagePixelType_uint16 + MSK_TYPE = otb.ImagePixelType_uint8 + PTRN_dir = 'LC0*_L2SP*' + PTRN_ref = '_B' + B2_name = 'B1' + PTRN_30m = ['*_SR_B1.TIF', '*_SR_B2.TIF', '*_SR_B3.TIF', + '*_SR_B4.TIF', '*_SR_B5.TIF', '*_SR_B6.TIF', + '*_SR_B7.TIF'] + PTRN_msk = ['*_QA_PIXEL.TIF'] + PTRN_ful = PTRN_30m + MASK_exp = 'im1b1!=21824 && im1b1!=21888 && im1b1!=30048' + FEAT_exp = { + 'B1': 'im1b1', + 'B2': 'im1b2', + 'B3': 'im1b3', + 'B4': 'im1b4', + 'B5': 'im1b5', + 'B6': 'im1b6', + 'B7': 'im1b7', + 'NDVI': '(im1b5-im1b4)/(im1b5+im1b4+1e-6)', + 'NDWI': '(im1b3-im1b5)/(im1b3+im1b5+1e-6)', + 'BRI': 'sqrt(' + '+'.join(['im1b%d*im1b%d' % (i, i) for i in range(1, 8)]) + ')', + 'MNDWI': '(im1b3-im1b7)/(im1b3+im1b7+1e-6)', + 'SWNDVI': '(im1b7-im1b5)/(im1b7+im1b5+1e-6)', + } + NDT = 0 + + @classmethod + def _check(cls,x): + return os.path.isdir(x) and os.path.basename(x).startswith(cls.PTRN_dir.split('*')[0]) and 'L2SP' in os.path.basename(x) + + @classmethod + def _img_id(cls,x): + if cls._check(x): + return os.path.basename(x) + else: + return None + + @classmethod + def _img_date(cls,x): + if cls._check(x): + return os.path.basename(x).split('_')[3] + else: + return None + + @classmethod + def _tile_id(cls,x): + if cls._check(x): + return os.path.basename(x).split('_')[2] + else: + return None + + def _process_mask(self, msks): + msk_pipe = otb.Registry.CreateApplication('BandMath') + msk_pipe.AddImageToParameterInputImageList('il', msks[0].GetParameterOutputImage('out')) + msk_pipe.SetParameterString('exp', self.MASK_exp) + msk_pipe.Execute() + return msk_pipe + + # ---- END SENSOR PROTOTYPE ---- + + def preprocess(self): + proc_idx = self.out_idx.copy() + self.out_idx = [] + num_of_band_inputs = len(self.PTRN_ful) + num_of_mask_inputs = len(self.PTRN_msk) + for t in proc_idx[::num_of_band_inputs+num_of_mask_inputs]: + idx_to_stack = [] + cct = otb.Registry.CreateApplication('ConcatenateImages') + [cct.AddImageToParameterInputImageList('il', self.pipe[j].GetParameterOutputImage('out')) for j in range(t, t+num_of_band_inputs)] + cct.Execute() + fn = self.files[t].replace(self.B2_name, 'STACK') + ty = self.REF_TYPE + self.append(cct, fn, ty, 'out', is_output=True) + + msks = self.pipe[t+num_of_band_inputs:t+num_of_band_inputs+num_of_mask_inputs] + msk_pipe = self._process_mask(msks) + fn = self.files[t].replace(self.B2_name, 'BINARY_MASK') + ty = self.MSK_TYPE + self.append(msk_pipe, fn, ty, 'out', is_output=True) + +class Landsat7PlanetaryTilePipeline(Landsat89PlanetaryTilePipeline): + NAME = 'LANDSAT7-PLANETARY' + PTRN_dir = 'LE07_L2SP*' + PTRN_30m = ['*_SR_B1.TIF', '*_SR_B2.TIF', '*_SR_B3.TIF', + '*_SR_B4.TIF', '*_SR_B5.TIF', '*_SR_B7.TIF'] + PTRN_ful = PTRN_30m + MASK_exp = 'im1b1!=5440 && im1b1!=5504 && im1b1!=13664' + FEAT_exp = { + 'B1': 'im1b1', + 'B2': 'im1b2', + 'B3': 'im1b3', + 'B4': 'im1b4', + 'B5': 'im1b5', + 'B7': 'im1b6', + 'NDVI': '(im1b4-im1b3)/(im1b4+im1b3+1e-6)', + 'NDWI': '(im1b2-im1b4)/(im1b2+im1b4+1e-6)', + 'BRI': 'sqrt(' + '+'.join(['im1b%d*im1b%d' % (i, i) for i in range(1, 7)]) + ')', + 'MNDWI': '(im1b2-im1b6)/(im1b2+im1b6+1e-6)', + 'SWNDVI': '(im1b6-im1b4)/(im1b6+im1b4+1e-6)', + } + +class Landsat45PlanetaryTilePipeline(Landsat7PlanetaryTilePipeline): + NAME = 'LANDSAT45-PLANETARY' + PTRN_dir = 'LT0*_L2SP*' + +class Landsat89PlanetaryPipeline(S2TheiaPipeline): + + S2TilePipeline = Landsat89PlanetaryTilePipeline + _check = S2TilePipeline._check + _tile_id = S2TilePipeline._tile_id + +class Landsat7PlanetaryPipeline(S2TheiaPipeline): + + S2TilePipeline = Landsat7PlanetaryTilePipeline + _check = S2TilePipeline._check + _tile_id = S2TilePipeline._tile_id + +class Landsat45PlanetaryPipeline(S2TheiaPipeline): + + S2TilePipeline = Landsat45PlanetaryTilePipeline + _check = S2TilePipeline._check + _tile_id = S2TilePipeline._tile_id diff --git a/TimeSeries/s2theia.py b/TimeSeries/s2theia.py index b42992cbcda0afb50b79ba6ec1cfbaa4de15a60b..0fec83dd5c6391cce096312653acb8321a8ae788 100644 --- a/TimeSeries/s2theia.py +++ b/TimeSeries/s2theia.py @@ -163,9 +163,9 @@ class S2TheiaTilePipeline: def _check_roi(cls, x, roi, min_surf=0.0, temp_fld='/tmp'): if cls._check(x): if os.path.isdir(x): - bnd = glob.glob(os.path.join(x, cls.PTRN_20m[0]))[0] + bnd = glob.glob(os.path.join(x, cls.PTRN_ful[0]))[0] elif os.path.splitext(x)[-1] == '.zip': - idf = cls.PTRN_20m[0].replace('*', '') + idf = cls.PTRN_ful[0].replace('*', '') arch = zipfile.ZipFile(x) fid = [name for name in arch.namelist() if idf in name] fle = arch.read(fid[0]) @@ -340,7 +340,7 @@ class S2TheiaTilePipeline: i += 1 to_merge.append(curr) - T = 10 + len(self.PTRN_msk) + T = len(self.PTRN_ful) + len(self.PTRN_msk) new_dates = [] proc_idx = self.out_idx.copy() self.out_idx = [] @@ -351,18 +351,18 @@ class S2TheiaTilePipeline: idx = T*l[0]+k self.append(self.pipe[idx], self.files[idx], self.types[idx], 'out', is_output=True) else: - for k in range(10): + for k in range(len(self.PTRN_ful)): mos = otb.Registry.CreateApplication('Mosaic') for i in l: mos.AddImageToParameterInputImageList('il', self.pipe[T*i+k].GetParameterOutputImage(self.out_p[T*i+k])) mos.SetParameterInt('nodata', self.NDT) mos.Execute() self.append(mos, self.files[T*i+k], self.types[T*i+k], 'out', is_output=True) - for k in range(10,T): + for k in range(len(self.PTRN_ful),T): bm = otb.Registry.CreateApplication('BandMath') for i in l: bm.AddImageToParameterInputImageList('il', self.pipe[T*i+k].GetParameterOutputImage(self.out_p[T*i+k])) - bm.SetParameterString('exp', self.MERG_msk[k-10] + '({})'.format(','.join(['im{}b1'.format(w+1) for w in range(len(l))]))) + bm.SetParameterString('exp', self.MERG_msk[k-len(self.PTRN_ful)] + '({})'.format(','.join(['im{}b1'.format(w+1) for w in range(len(l))]))) bm.Execute() self.append(bm, self.files[T * i + k], self.types[T * i + k], 'out', is_output=True) new_dates.append(self.input_dates[l[-1]]) @@ -385,7 +385,7 @@ class S2TheiaTilePipeline: return cc def set_input_epsg(self): - f, _ = self.get_file(self.image_list[0], self.PTRN_20m[0]) + f, _ = self.get_file(self.image_list[0], self.PTRN_ful[0]) ds = gdal.Open(f) self.input_epsg = osr.SpatialReference(wkt=ds.GetProjection()).GetAuthorityCode('PROJCS') ds = None @@ -417,10 +417,10 @@ class S2TheiaTilePipeline: proc_idx = self.out_idx.copy() self.out_idx = [] num_of_mask_inputs = len(self.PTRN_msk) - for t in proc_idx[::10+num_of_mask_inputs]: + for t in proc_idx[::len(self.PTRN_ful)+num_of_mask_inputs]: idx_to_stack = [] - for k in range(t,t+10): - if k % (10 + num_of_mask_inputs) in [3, 4, 5, 7, 8, 9]: + for k in range(t,t+len(self.PTRN_ful)): + if k % (len(self.PTRN_ful) + num_of_mask_inputs) in [3, 4, 5, 7, 8, 9]: si = otb.Registry.CreateApplication('Superimpose') si.SetParameterInputImage('inm', self.pipe[k].GetParameterOutputImage('out')) si.SetParameterInputImage('inr', self.pipe[t].GetParameterOutputImage('out')) @@ -442,7 +442,7 @@ class S2TheiaTilePipeline: ty = self.REF_TYPE self.append(cct, fn, ty, 'out', is_output=True) - msks = self.pipe[t+10:t+10+num_of_mask_inputs] + msks = self.pipe[t+len(self.PTRN_ful):t+len(self.PTRN_ful)+num_of_mask_inputs] msk_pipe = self._process_mask(msks) for app in msk_pipe[:-1]: self.append(app) @@ -554,8 +554,8 @@ class S2TheiaTilePipeline: proc_idx = self.out_idx.copy() self.out_idx = [] img = [self.pipe[t] for t in range(self.pipe_start+match_band, - self.pipe_start+(10+len(self.PTRN_msk))*len(self.input_dates), - 10+len(self.PTRN_msk))] + self.pipe_start+(len(self.PTRN_ful)+len(self.PTRN_msk))*len(self.input_dates), + len(self.PTRN_ful)+len(self.PTRN_msk))] msk = [self.pipe[t] for t in proc_idx[1::2]] RX, RY = img[0].GetImageSpacing(out_param) ref_idx = None @@ -662,7 +662,7 @@ class S2TheiaTilePipeline: gf = otb.Registry.CreateApplication('ImageTimeSeriesGapFilling') gf.SetParameterInputImage('in', self.pipe[-2].GetParameterOutputImage('out')) gf.SetParameterInputImage('mask', self.pipe[-1].GetParameterOutputImage('out')) - gf.SetParameterInt('comp', 10) + gf.SetParameterInt('comp', len(self.PTRN_ful)) gf.SetParameterString('it', 'linear') gf.SetParameterString('id', os.path.join(self.folder, self.tile_id + '_indates.txt')) gf.SetParameterString('od', output_dates) @@ -670,7 +670,7 @@ class S2TheiaTilePipeline: gf.Execute() gf_out = gf else: - gf_fn = self.temp_fld + os.sep + 'SENTINEL2_' + self.tile_id + '_GAPFILLED_FULL.tif' + gf_fn = self.temp_fld + os.sep + '{}_'.format(self.NAME) + self.tile_id + '_GAPFILLED_FULL.tif' if not os.path.exists(gf_fn): gf.SetParameterString("out", gf_fn) gf.ExecuteAndWriteOutput() @@ -679,14 +679,14 @@ class S2TheiaTilePipeline: t = 1 for d in od: - ch_list = ['Channel%d' % i for i in range(t,t+10)] - t += 10 + ch_list = ['Channel%d' % i for i in range(t,t+len(self.PTRN_ful))] + t += len(self.PTRN_ful) er = otb.Registry.CreateApplication('ExtractROI') er.SetParameterInputImage('in', gf_out.GetParameterOutputImage('out')) er.UpdateParameters() er.SetParameterStringList('cl', ch_list) er.Execute() - dn = 'SENTINEL2_' + self.tile_id + '_GAPFILL_' + d + dn = '{}_'.format(self.NAME) + self.tile_id + '_GAPFILL_' + d fn = self.tile_id + os.sep + dn + os.sep + dn + '_STACK.tif' ty = self.REF_TYPE self.append(er, fn, ty, 'out', is_output=True) @@ -726,7 +726,7 @@ class S2TheiaTilePipeline: i += 1 bm.SetParameterString('exp', '{' + ';'.join(expr) + '}') bm.Execute() - fn = self.tile_id + os.sep + 'SENTINEL2_' + self.tile_id + '_GAPFILL_' + feat + '.tif' + fn = self.tile_id + os.sep + '{}_'.format(self.NAME) + self.tile_id + '_GAPFILL_' + feat + '.tif' ty = otb.ImagePixelType_float self.append(bm, fn, ty, 'out', is_output=True) @@ -892,7 +892,7 @@ class S2TheiaPipeline: out_mos = [] vrtopt = gdal.BuildVRTOptions() for i in range(n_dates): - fn = out_fld + os.sep + 'SENTINEL2_MOSAIC_GAPFILL_' + self.tiles[0].output_dates[i] + '_' + stack_name + '.vrt' + fn = out_fld + os.sep + '{}_MOSAIC_GAPFILL_'.format(self.tiles[0].NAME) + self.tiles[0].output_dates[i] + '_' + stack_name + '.vrt' to_mosaic = [x[i] for x in out] gdal.BuildVRT(fn, to_mosaic, options=vrtopt) out_mos.append(fn) @@ -916,7 +916,7 @@ class S2TheiaPipeline: out_mos = [] vrtopt = gdal.BuildVRTOptions() for i in range(len(feat_list)): - fn = out_fld + os.sep + 'SENTINEL2_MOSAIC_GAPFILL_' + feat_list[i] + '.vrt' + fn = out_fld + os.sep + '{}_MOSAIC_GAPFILL_'.format(self.tiles[0].NAME) + feat_list[i] + '.vrt' to_mosaic = [x[i] for x in out] gdal.BuildVRT(fn, to_mosaic, options=vrtopt) out_mos.append(fn) diff --git a/Workflows/basic.py b/Workflows/basic.py index 81fd68242d69630a1669b254e406b8b85cb66dda..6d439b86e5fb4db50f5c107735d8daacc6f221b6 100644 --- a/Workflows/basic.py +++ b/Workflows/basic.py @@ -51,6 +51,16 @@ def process_timeseries(oroot, d, ts_lst_pkl): os.makedirs(ots, exist_ok=True) ts_lst.append(preprocess_planet(ts['path'], ots)) + elif ts['type'].startswith('landsat'): + print('[MORINGA-INFO] : Preprocessing {}'.format(ts['type'])) + ots = os.path.join(oroot, 'timeseries/' + ts['type']) + os.makedirs(ots, exist_ok=True) + ts_lst.append(preprocess_landsat(ts['path'], + ots, + roi=d['roi'], + output_dates_file=ts['output_dates_file'], + which=ts['type'])) + else: raise ValueError('TimeSeries type not yet supported.') with open(ts_lst_pkl, 'wb') as ts_save: @@ -84,7 +94,9 @@ def train_valid_workflow(seg, ts_lst_pkl, d, m_file): reference_data=d['ref_db']['path'], ref_class_field=d['ref_db']['fields']) - obc.gen_k_folds(5, class_field=d['ref_db']['fields'][-1],augment=d['training']['augment_if_missing']) + obc.gen_k_folds(5, class_field=d['ref_db']['fields'][-1], + augment=d['training']['augment_if_missing'], + min_samples_per_class=d['training']['augment_if_missing']) if 'export_training_base' in d['training'].keys() and d['training']['export_training_base'] is True: obc.save_training_base('{}/_side/training_base.pkl'.format(os.path.join(d['output_path'], d['chain_name']))) diff --git a/Workflows/operations.py b/Workflows/operations.py index 3619f5f40da921dac5c82706c47afafd491e35f5..a80026c680a3d2d9620876e50f9d95c59332055e 100644 --- a/Workflows/operations.py +++ b/Workflows/operations.py @@ -55,6 +55,25 @@ def preprocess_s2(in_fld, out_fld, output_dates_file=None, roi=None, return 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) +def preprocess_landsat(in_fld, out_fld, output_dates_file=None, roi=None, which='landsat89'): + LandsatProcessor = None + if which == 'landsat89': + LandsatProcessor = landsat_planetary.Landsat89PlanetaryPipeline + elif which == 'landsat7': + LandsatProcessor = landsat_planetary.Landsat7PlanetaryPipeline + elif which == 'landsat45': + LandsatProcessor = landsat_planetary.Landsat45PlanetaryPipeline + else: + raise ValueError("Unknown Landsat mission.") + + ls = LandsatProcessor(in_fld, temp_fld=out_fld, roi=roi) + if output_dates_file is not None: + ls.set_output_dates_by_file(output_dates_file) + else: + raise ValueError("Please provide path to a text file containing output dates.") + + return ls.extract_feature_set(out_fld, store_gapfill=True, mosaicking='vrt') + def preprocess_planet(in_fld, out_fld): pl = planet_mosaics.PlanetMosaicPipeline(in_fld) pl.compute_features()