def __init__(self, scene_id): """ class constructor for the inversion class Parameters ---------- scene_id : String ID of the scene to be inverted -> links to the metadata stored in the OBIA4RTM database Returns ------- None """ self.scene_id = scene_id # get a logger self.__logger = get_logger() self.__sensor = None self.__scene_id = None self.acquisition_time, self.acquisition_date = None, None # angles self.__tts, self.__tto, self.__psi = None, None, None # setup the DB connection self.conn, self.cursor = connect_db.connect_db() self.__logger.info('Connected to PostgreSQL engine sucessfully!') # determine the directory the configuration files are located obia4rtm_dir = os.path.dirname(OBIA4RTM.__file__) fname = obia4rtm_dir + os.sep + 'OBIA4RTM_HOME' with open(fname, 'r') as data: self.__directory = data.readline()
def __init__(self): """ class constructor and basic setup of processing environment """ # find the directory the 6S binary has been installed to # this is a sub-directory of the OBIA4RTM_HOME with open(os.path.dirname(OBIA4RTM.__file__) + os.sep + 'OBIA4RTM_HOME', 'r') as data: obia4rtm_dir = data.readline() self.sixS_install_dir = obia4rtm_dir + os.sep + 'sixS'+ os.sep + \ 'src' + os.sep + '6SV1.1' # make sure that 6S is installed if not os.path.isdir(self.sixS_install_dir): print("Error: 6S is not installed on your computer or cannot be found!\n"\ "Expected installation location: '{}'\n"\ "You might want to run OBIA4RTM.S2_PreProcessor.install_6S "\ "for installing 6S".format(self.sixS_install_dir)) # add this directory of 6S binary to system path temporally (does not work properly) # sys.path.append(sixS_install_dir) # get a logger for recording sucess and error messages self.__logger = get_logger() self.__logger.info('Setting up Google EE environment for Sentinel-2 '\ 'atmospheric correction using the 6S algorithm') try: ee.Initialize() except EEException: self.__logger.error('No (valid) Earth-Engine credentials provided!', exc_info=True) close_logger(self.__logger) sys.exit(-1) # for storing the metadata self.info = None # for the image object self.S2 = None # for the solar zenith angle and the scene timestamp self.solar_z = None self.scene_date = None
def get_mean_refl_ee(shp_file, img, acqui_date, scene_id, table_name): """ calculates mean reflectance per object in image. Uses GEE-Python bindings for reading the shape and Sentinel-2 imagery data. Parameters ---------- shp_file : String file-path to ESRI shapefile with the image object boundaries img : ee.image.Image GEE imagery containing the atmospherically collected Sentinel-2 data acqui_date : String acquisition date of the imagery (used for linking to LUT and metadata) scene_id : String ID of the Sentinel-2 scene table_name : String Name of the table the object reflectance values should be written to Returns ------- None """ # open the database connection to OBIA4RTM's backend conn, cursor = connect_db() # get a logger logger = get_logger() # in case it isn't done yet: ee.Initialize() # iterate over the shapefile to get the metadata # Shapefile handling driver = ogr.GetDriverByName('ESRI Shapefile') shpfile = driver.Open(shp_file) # check if shapefile exists and could be opened if shpfile is None: raise TypeError( "The provided File '{}' is invalid or blocked!".format(shp_file)) layer = shpfile.GetLayer(0) num_objects = layer.GetFeatureCount() logger.info( "{0} image objects will be processed. This might take a while...". format(num_objects)) # loop over single features # get geometry of features and their ID as well as mean reflectane per band # before that check the raster metadata from GEE img_epsg = img.select('B2').projection().crs().getInfo() img_epsg = int(img_epsg.split(':')[1]) # check with the epsg of the shapefile ref = layer.GetSpatialRef() if ref is None: logger.warning('The layer has no projection info! Assume it is the same'\ 'as for the imagery - but check results!') shp_epsg = img_epsg # asuming that the imagery is projected in UTM as it should # the UTM-Zone is stored in the last two digits utm = int(str(shp_epsg)[3::]) else: code = ref.GetAuthorityCode(None) shp_epsg = int(code) utm = ref.GetUTMZone() if img_epsg != shp_epsg: logger.error('The projection of the imagery does not match the projection '\ 'of the shapefile you provided!'\ 'EPSG-Code of the Image: EPSG:{0}; '\ 'EPSG-Code of the Shapefile: EPSG:{1}'.format( img_epsg, shp_epsg)) close_logger(logger) sys.exit( 'An error occured while execute get_mean_refl. Check logfile!') # determine the min area of an object (determined by S2 spatial resolution) # use the "standard" resolution of 20 meters # an object must be twice times larger min_area = 20 * 60 * 2 # for requesting the landuse information luc_field = 'LU' + acqui_date.replace('-', '') # start iterating over features # Get geometry and extent of feature for ii in range(num_objects): feature = layer.GetFeature(ii) # extract the geometry geom = feature.GetGeometryRef() # get a well-know text representation -> required by PostGIS wkt = geom.ExportToWkt() # get the ID # f_id = feature.GetFID() # depraceted f_id = feature.GetField('id') # get the land cover code luc = feature.GetField(luc_field) # convert to integer coding if luc is provided as text try: luc = int(luc) except ValueError: luc = luc.upper() query = "SELECT landuse FROM public.s2_landuse WHERE landuse_semantic = "\ "'{0}';".format( luc) cursor.execute(query) res = cursor.fetchall() luc = int(res[0][0]) # end try-except # get the area of the feature and check if it fits the image # resolution -> if the object is to small skip it area = geom.Area() # m2 # the area must be at least 2.5 times larger than the coarsest # possible spatial resolution of Sentinel-2 (60 by 60 meters) if area < min_area: logger.warning('The object {0} was too small compared to the '\ 'spatial resolution of Sentinel-2! '\ 'Object area (m2): {1}; Minimum area required (m2): '\ '{2} -> skipping'.format( f_id, area, min_area)) continue # export the coordinates of the geometry temporarily to JSON dictionary # for communicating with GEE geom_json = ast.literal_eval(geom.ExportToJson()) # get the geometry type # allowed values: Polygon and Multipolygon geom_type = geom_json.get('type') # get the coordinates geom_coords = geom_json.get('coordinates')[0] # must be converted to lon, lat for GEE geo_coords = [] for geom_coord in geom_coords: easting = geom_coord[0] northing = geom_coord[1] # call transform method lon, lat, alt = transform_utm_to_wgs84(easting, northing, utm) geo_coord = [] geo_coord.append(lon) geo_coord.append(lat) geo_coords.append(geo_coord) if geom_type not in ['Polygon', 'Multipolygon']: logger.warning('Object with ID {} is not of type Polygon or '\ 'Multipolygon -> skipping'.format(f_id)) continue # construct a GEE geometry # TODO -> test what happens for Multipolygon! geom_gee = ee.geometry.Geometry.Polygon(geo_coords) # use the image reduce function to get the mean reflectance values # for each of the nine bands used in GEE meanDictionary = img.reduceRegion(reducer=ee.Reducer.mean(), geometry=geom_gee) # extract the computed mean values for the particular image # only use those bands required for OBIA4RTM # multiply with 100 to get % surface reflectance values multiplier = 100 # surround with try-except in case only blackfill was found for a object try: B2 = meanDictionary.get('B2').getInfo() * multiplier B3 = meanDictionary.get('B3').getInfo() * multiplier B4 = meanDictionary.get('B4').getInfo() * multiplier B5 = meanDictionary.get('B5').getInfo() * multiplier B6 = meanDictionary.get('B6').getInfo() * multiplier B7 = meanDictionary.get('B7').getInfo() * multiplier B8A = meanDictionary.get('B8A').getInfo() * multiplier B11 = meanDictionary.get('B11').getInfo() * multiplier B12 = meanDictionary.get('B12').getInfo() * multiplier except TypeError: logger.info( 'No spectral information found for Object with ID {}'.format( f_id)) continue # check cloud and shadow mask # the cloud and shadow masks are binary # if the average is zero everything is OK (=no clouds, no shadows) cm = meanDictionary.get('CloudMask').getInfo() sm = meanDictionary.get('ShadowMask').getInfo() # if the shadow and/ or the cloud mask is not zero on average # -> skip the object as it is cloud covered or affected by # cloud shadows if cm > 0: logger.info( 'Object with ID {} is coverd by clouds -> skipping'.format( f_id)) continue if sm > 0: logger.info( 'Object with ID {} is coverd by cloud shadows -> skipping'. format(f_id)) continue # also make sure that the object really contains reflectance values # checking the first band should be sufficient if B2 is None: logger.info( 'Object with ID {} contains only NaN values -> skipping'. format(f_id)) continue # otherwise insert the data into the PostgreSQL database try: query = "INSERT INTO {0} (object_id, acquisition_date, landuse, object_geom, "\ "b2, b3, b4, b5, b6, b7, b8a, b11, b12, scene_id) VALUES ( " \ "{1}, '{2}', {3}, ST_Multi(ST_GeometryFromText('{4}', {5})), " \ "{6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, '{15}') "\ " ON CONFLICT (object_id, scene_id) DO NOTHING;".format( table_name, f_id, acqui_date, luc, wkt, img_epsg, np.round(B2, 4), np.round(B3, 4), np.round(B4, 4), np.round(B5, 4), np.round(B6, 4), np.round(B7, 4), np.round(B8A, 4), np.round(B11, 4), np.round(B12, 4), scene_id ) except ValueError: logger.error("Invalid string syntax encountered when attempting"\ " to generate INSERT for field {0} on '{1}'".format( f_id, acqui_date)) continue # catch errors for single objects accordingly and continue with next # object to avoid interrupts of whole workflow try: cursor.execute(query) conn.commit() except (DatabaseError, ProgrammingError): logger.error( "Could not insert image object with ID {0} into table '{1}'". format(f_id, table_name), exc_info=True) conn.rollback() continue #endfor # close the GDAL-bindings to the files shpfile = None layer = None # close database connection close_db_connection(conn, cursor) # close the logger close_logger(logger)
def create_schema(): """ this function is used to generate a new schema in the OBIA4RTM database. In case the schema already exists, nothing will happen. The schema to be created is taken from the obia4rtm_backend.cfg file Parameters ---------- None Returns ------- status : integer zero if everything was OK """ status = 0 # connect to OBIA4RTM database con, cursor = connect_db() # open a logger logger = get_logger() logger.info('Trying to setup a new schema for the OBIA4RTM database') # read in the obia4rtm_backend information to get the name of the schema # therefore the obia4rtm_backend.cfg file must be read install_dir = os.path.dirname(OBIA4RTM.__file__) home_pointer = install_dir + os.sep + 'OBIA4RTM_HOME' if not os.path.isfile(home_pointer): logger.error('Cannot determine OBIA4RTM Home directory!') close_logger(logger) sys.exit(-1) with open(home_pointer, "r") as data: obia4rtm_home = data.read() backend_cfg = obia4rtm_home + os.sep + 'obia4rtm_backend.cfg' if not os.path.isfile(backend_cfg): logger.error( 'Cannot read obia4rtm_backend.cfg from {}!'.format(obia4rtm_home)) close_logger(logger) sys.exit(sys_exit_message) # now, the cfg information can be read in using the configParser class parser = ConfigParser() try: parser.read(backend_cfg) except MissingSectionHeaderError: logger.error( 'The obia4rtm_backend.cfg does not fulfil the formal requirements!', exc_info=True) close_logger(logger) sys.exit(-1) # no get the name of the schema schema = parser.get('schema-setting', 'schema_obia4rtm') try: assert schema is not None and schema != '' except AssertionError: logger.error( 'The version of your obia4rtm_backend.cfg file seems to be corrupt!', exc_info=True) close_logger(logger) sys.exit(sys_exit_message) # if the schema name is OK, the schema can be created # if the schema already exists in the current database, nothing will happen sql = 'CREATE SCHEMA IF NOT EXISTS {};'.format(schema) cursor.execute(sql) con.commit() # enable PostGIS and HSTORE extension # enable the PostGIS extension # in case it fails it is most likely because the extension was almost # enabled as it should sql = "CREATE EXTENSION PostGIS;" try: cursor.execute(sql) con.commit() except (ProgrammingError, DatabaseError): logger.info("PostGIS already enabled!") con.rollback() pass # enable the HSTORE extension sql = "CREATE EXTENSION HSTORE;" try: cursor.execute(sql) con.commit() except (ProgrammingError, DatabaseError): logger.error("HSTORE already enabled!") con.rollback() pass logger.info( "Successfully created schema '{}' in current OBIA4RTM database!". format(schema)) # after that the schema-specific tables are created that are required # in OBIA4RTM sql_home = install_dir + os.sep + 'SQL' + os.sep + 'Tables' # the tables 's2_inversion_results, s2_lookuptable, s2_objects and s2_inversion_mapping # must be created within the schema # check if the tables already exist before trying to create them sql_scripts = [ 's2_lookuptable.sql', 's2_inversion_results.sql', 's2_objects.sql', 'inversion_mapping.sql' ] # go through the config file to get the table-names table_names = [] table_names.append(parser.get('schema-setting', 'table_lookuptabe')) table_names.append(parser.get('schema-setting', 'table_inv_results')) table_names.append(parser.get('schema-setting', 'table_object_spectra')) table_names.append(parser.get('schema-setting', 'table_inv_mapping')) # the parser can be cleared now as all information is read parser.clear() # iterate through the 4 scripts to create the tables given they not exist for index in range(len(sql_scripts)): sql_script = sql_home + os.sep + sql_scripts[index] table_name = table_names[index] # check if the table already exists exists = check_if_exists(schema, table_name, cursor) # if already exists table log a warning and continue with the next table if exists: logger.warning( "Table '{0}' already exists in schema '{1}' - skipping".format( table_name, schema)) continue # else create the table # get the corresponding sql-statment and try to execute it sql_statement = create_sql_statement(sql_script, schema, table_name, logger) try: cursor.execute(sql_statement) con.commit() except (DatabaseError, ProgrammingError): logger.error("Creating table '{0}' in schema '{1}' failed!".format( table_name, schema), exc_info=True) close_logger(logger) sys.exit(sys_exit_message) # log success logger.info("Successfully created table '{0}' in schema '{1}'".format( table_name, schema)) # create the RMSE function required for inverting the spectra fun_home = install_dir + os.sep + 'SQL' + os.sep + 'Queries_Functions' rmse_fun = fun_home + os.sep + 'rmse_function.sql' sql_statement = create_function_statement(rmse_fun, logger) try: cursor.execute(sql_statement) con.commit() except (DatabaseError, ProgrammingError): logger.error("Creating function '{0}' failed!".format(rmse_fun), exc_info=True) close_logger(logger) sys.exit(sys_exit_message) # after iterating, the db connection and the logger can be close close_db_connection(con, cursor) close_logger(logger) return status
def update_luc_table(landcover_table, landcover_cfg=None): """ updates the land-cover/ land use table in OBIA4RTM that is required for performing land-cover class specific vegetation parameter retrieval Make sure that the classes in the config file match the land cover classes provided for the image objects and used for generating the lookup-table. Otherwise bad things might happen. NOTE: in case land cover classes that are about to be inserted are already stored in the table, they will be overwritten! Parameters ---------- landcover_table : String name of the table with the land cover information (<schema.table>) landcover_cfg : String file-path to land cover configurations file Returns ------- None """ # open the logger logger = get_logger() # if no other file is specified the default file from the OBIA4RTM # directory in the user profile will be used (landcover.cfg) if landcover_cfg is None: # determine the directory the configuration files are located obia4rtm_dir = os.path.dirname(OBIA4RTM.__file__) fname = obia4rtm_dir + os.sep + 'OBIA4RTM_HOME' with open(fname, 'r') as data: directory = data.readline() landcover_cfg = directory + os.sep + 'landcover.cfg' # check if specified file exists if not os.path.isfile(landcover_cfg): logger.error('The specified landcover.cfg cannot be found!', exc_info=True) close_logger(logger) sys.exit('Error during inserting landcover information. Check log!') # connect database con, cursor = connect_db() # read the landcover information luc_classes = get_landcover_classes(landcover_cfg) # now read in the actual data n_classes = len(luc_classes) # number of land cover classes try: assert n_classes >= 1 except AssertionError: logger.error('Error: >=1 land cover class must be provided!', exc_info=True) close_logger(logger) sys.exit('Error while reading the landcover.cfg file. Check log.') # now, iterate through the lines of the cfg files and insert it into # the Postgres database logger.info("Try to insert values into table '{0}' from landcover.cfg "\ "file ({1})".format( landcover_table, landcover_cfg)) for luc_class in luc_classes: # the first item of the tuple must be an integer value # the second one a string try: luc_code = int(luc_class[0]) except ValueError: logger.error('Landcover.cfg file seems to be corrupt. '\ 'Excepted integer for land cover code!', exc_info=True) close_logger(logger) sys.exit('Error during inserting landcover.cfg. Check log!') try: luc_desc = luc_class[1] except ValueError: logger.error('Landcover.cfg file seems to be corrupt. '\ 'Excepted string for land cover description!', exc_info=True) close_logger(logger) sys.exit('Error during inserting landcover.cfg. Check log!') # insert into database # ON CONFLICT -> old values will be replaced sql = "INSERT INTO {0} (landuse, landuse_semantic) VALUES ({1},'{2}')"\ " ON CONFLICT (landuse) DO UPDATE SET landuse = {1},"\ " landuse_semantic = '{2}';".format( landcover_table, luc_code, luc_desc) cursor.execute(sql) con.commit() # close the logger and database connection afterwards logger.info("Updated land cover information in table '{}'".format( landcover_table)) close_logger(logger) close_db_connection(con, cursor)
def call_sen2core(sentinel_data_dir, zipped, resolution, path_sen2core): """ calls Sen2Core and runs it on a downloaded Sentinel 1C dataset to convert it to Level 2A. The output spatial resolution must be provided (10, 20 or 60 meters) NOTE: If you have already L2 imagery then only run the gdal_merge wrapper and to not use this function Parameters ---------- sentinel_data_dir : String path to the directory that contains the Level-1C data. In case the data is zipped (default when downloaded from Copernicus) specify the file-path of the zip zipped : Boolean specifies if the directory with the Sat data is zipped resolution : Integer spatial resolution of the atmospherically corrected imagery possible value: 10, 20, 60 meters path_sen2core : String directory containing Sen2Core software (top-most level; e.g. /home/user/Sen2Core/). Must be the same directory as specified during the Sen2Core installation process using the --target option Returns ------- sentinel_data_dir_l2 """ # check inputs if zipped: if not os.path.isfile(sentinel_data_dir): print("Error: '{}' does not exist!".format(sentinel_data_dir)) sys.exit(-1) else: if not os.path.isdir(sentinel_data_dir): print("Error: '{}' does not exist!".format(sentinel_data_dir)) sys.exit(-1) # check specified spatial resolution of the result allowed_res = [10, 20, 60] # m if resolution not in allowed_res: print("Error: The specified spatial resolution of {0} is not allowed!\n"\ "Must be one of {1}!".format(resolution, allowed_res)) sys.exit(-1) # check if Sen2Core is installed and working runs, cmd = check_sen2core_installation(path_sen2core) if not runs: print('Error: Sen2Core seems not to work properly!') sys.exit(-1) # after that, enable logging logger = get_logger() logger.info('Setting up Sen2Core processing environment for processing '\ '{}'.format(sentinel_data_dir)) # if the data is still zipped unzip if zipped: # extract the parent directory parent_dir = os.path.dirname(sentinel_data_dir) # unzip zip_file = ZipFile(sentinel_data_dir) zip_file.extractall(parent_dir) zip_file.close() # check if everything is OK # new name of sentinel_data_dir is now without ending .zip sentinel_data_dir = sentinel_data_dir.replace('.zip', '') # should be a directory if not os.path.isdir(sentinel_data_dir): logger.error('Unpacking of Sentinel-2 data failed!') close_logger(logger) sys.exit(error_message) logger.info("Successfully extracted zipped data to {}".format( sentinel_data_dir)) # endif # now the data is ready for Sen2Core # create the command to run Sen2Core cmd = cmd.replace(' --help', '') cmd = cmd + ' ' + sentinel_data_dir + ' --resolution=' + str(resolution) # call Sen2Core # try to execute the command logger.info('Starting Sen2Core processing') out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) # read the communication of the process res = out.communicate() stdout = res[0].decode() stderr = res[1].decode() # write output to file # determine the filename sub_pos = [m.start() for m in re.finditer('_', sentinel_data_dir)] fname = sentinel_data_dir[0:sub_pos[2]] # parent directory parent_dir = os.path.dirname(sentinel_data_dir) # save output of Sen2Core sen2core_stderr = parent_dir + os.sep + 'Sen2Core_' + os.path.basename( fname) + '.stdout' with open(sen2core_stderr, 'a') as out: out.writelines(stdout) sen2core_stdout = parent_dir + os.sep + 'Sen2Core_' + os.path.basename( fname) + '.stderr' with open(sen2core_stdout, 'a') as out: if stderr == '': stderr = 'Sen2Core terminated without errors' out.writelines(stderr) # determine the dir-name of the processed data fname = fname.replace('MSIL1C', 'MSIL2A') # get list of all directories list_dirs = os.listdir(parent_dir) # directory of the L2 data sentinel_data_dir_l2 = '' for directory in list_dirs: sub_pos = [m.start() for m in re.finditer('_', directory)] fname_act = parent_dir + os.sep + directory[0:sub_pos[2]] if fname_act == fname: sentinel_data_dir_l2 = parent_dir + os.sep + directory break if sentinel_data_dir_l2 == '': logger.error( 'Seems as if Sen2Core failed. Check {} for more info.'.format( sen2core_stdout)) close_logger(logger) sys.exit(error_message) logger.info("Success - Processed data is in '{0}. See also {1}'".format( sentinel_data_dir_l2, sen2core_stdout)) # delete the L1C directory in case the data was available as zip to # save disk storage if zipped: shutil.rmtree(sentinel_data_dir, ignore_errors=True) close_logger(logger) return sentinel_data_dir_l2
def call_gdal_merge(sentinel_data_dir_l2, resolution, storage_dir=None): """ calls the gdal_merge.py script to make an image layer stack and prepare the imagery for usage in OBIA4RTM. Outputs a GeoTiff with the nine Sentinel-2 bands used in OBIA4RTM and the SCL band that contains the preclassification information. You can use this function also if you L2 data Parameters ---------- sentinel_data_dir_l2 : String path of the directory containing the output of sen2core in L2 level resolution : Integer spatial resolution of the atmospherically corrected imagery possible value: 10, 20, 60 meters storage_dir : String path to the directory the layer stack should be moved to. If None, the layer stack will remain the sentinel_data_dir_l2 in the img folder Returns ------- fname_stack : String file-path to the stacked imagery metadata_xml : String file-path to the metadata xml file """ # enable logging logger = get_logger() # check inputs if not os.path.isdir(sentinel_data_dir_l2): logger.error( "Error: '{}' does not exist!".format(sentinel_data_dir_l2)) close_logger(logger) sys.exit(error_message) # resolution allowed_res = [10, 20, 60] # m if resolution not in allowed_res: logger.error("Error: The specified spatial resolution of {0} is not allowed! "\ "Must be one of {1}!".format(resolution, allowed_res)) sys.exit(error_message) # storage directory if storage_dir is not None: if not os.path.isdir(storage_dir): try: os.mkdir(storage_dir) except PermissionError: logger.error("Could not create directory '{}'".format( sentinel_data_dir_l2), exc_info=True) logger.info('Formatting sen2core output for OBIA4RTM') # change to the sentinel_data_dir_l2 to avoid endless path names # jump directly into the 'Granule directory' sentinel_data_dir_l2 = sentinel_data_dir_l2 + os.sep + 'GRANULE' os.chdir(sentinel_data_dir_l2) # path to the image bands next_subdir = os.listdir()[0] # now the full path can be constructed sentinel_data_dir_l2 += os.sep + next_subdir os.chdir(sentinel_data_dir_l2) # check if the MTD_TL.xml metadata file can be found if not os.path.isfile('MTD_TL.xml'): logger.warning('No metadata-xml file could be found!') metadata_xml = '' else: metadata_xml = sentinel_data_dir_l2 + os.sep + 'MTD_TL.xml' sentinel_data_dir_l2 += os.sep + 'IMG_DATA' + os.sep + 'R' + str( resolution) + 'm' try: os.chdir(sentinel_data_dir_l2) except FileNotFoundError: logger.error('Could not find {}'.format(sentinel_data_dir_l2), exc_info=True) close_logger(logger) sys.exit(error_message) # get the jp2 files containing the single bands and the SCL information band_files = os.listdir() band_names = [ 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B8A', 'B11', 'B12', 'SCL' ] stack_files = [] # loop over the filenames to find the important ones for band_file in band_files: for band_name in band_names: if band_file.find(band_name) != -1: stack_files.append(band_file) break # now the list must be sorted to bring the bands in the correct order stack_files.sort() # check if 10 bands were found try: assert len(stack_files) == 10 except AssertionError: logger.error('Expected 10 bands got {}'.format(len(stack_files)), exc_info=True) close_logger(logger) sys.exit(error_message) # convert list to string stack_files = str(stack_files).replace('[', '').replace(']', '').replace(',', ' ') # make name of stacked layer file sub_pos = [m.start() for m in re.finditer('_', band_files[0])] prefix = band_files[0][0:sub_pos[1]] fname_stack = prefix + '_merged.tiff' # now everything is ready for running the gdal_merge.py script # output is GTiff and not Jpeg2000 as there are still some driver problems cmd = 'gdal_merge.py -of GTiff -separate ' + stack_files + ' -o ' + fname_stack logger.info('Running {}'.format(cmd)) out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) res = out.communicate() stderr = res[1].decode() # check if the command worked if stderr != '': logger.error('gdal_merge failed: {}'.format(stderr)) close_logger(logger) sys.exit(error_message) logger.info( 'Successfully stacked Sentinel-2 bands ({})'.format(fname_stack)) # give the full path fname_stack_short = fname_stack fname_stack = sentinel_data_dir_l2 + os.sep + fname_stack # in case an alternative directory was specified move the metadate file and # the stacked layer file to this directory if storage_dir is not None: # rename the metadata-file and copy it to the storage drive copy = shutil.copy2(metadata_xml, storage_dir + os.sep + prefix + '_MTD_TL.xml') try: assert copy != '' assert copy is not None except AssertionError: logger.error('Copying of metafile failed!', exc_info=True) close_logger(logger) sys.exit(error_message) # move the stacked image os.rename(fname_stack, storage_dir + os.sep + fname_stack_short) logger.info("Moved the imagery and the metadata xml to '{}'".format( storage_dir)) # close the logger close_logger(logger) # return the file-paths of the imagery and the metadata return fname_stack, metadata_xml
def get_mean_refl(shp_file, raster_file, acqui_date, scene_id, table_name): """ calculates mean reflectance per object in image. Uses GDAL-Python bindings for reading the shape and raster data. Parameters ---------- shp_file : String file-path to ESRI shapefile with the image object boundaries raster_file : String file-path to raster containing Sentinel-2 imagery as GeoTiff it is assumed that clouds/ shadows etc have already been masked out and these pixels are set to the according NoData value acqui_date : String acquisition date of the imagery (used for linking to LUT and metadata) scene_id : String ID of the Sentinel-2 scene table_name : String Name of the table the object reflectance values should be written to Returns ------- None """ # open the database connection to OBIA4RTM's backend conn, cursor = connect_db() # get a logger logger = get_logger() # iterate over the shapefile to get the metadata # Shapefile handling driver = ogr.GetDriverByName('ESRI Shapefile') shpfile = driver.Open(shp_file) layer = shpfile.GetLayer(0) num_objects = layer.GetFeatureCount() logger.info("{0} image objects will be processed".format(num_objects)) # loop over single features # get geometry of features and their ID as well as mean reflectane per band # open raster data value raster = gdal.Open(raster_file) # Get image raster georeference info transform = raster.GetGeoTransform() xOrigin = transform[0] yOrigin = transform[3] pixelWidth = transform[1] pixelHeight = transform[5] # extract the epsg-code proj = osr.SpatialReference(wkt=raster.GetProjection()) epsg = int(proj.GetAttrValue('AUTHORITY', 1)) # check with the epsg of the shapefile ref = layer.GetSpatialRef() if ref is None: logger.warning('The layer has no projection info! Assume it is the same'\ 'as for the imagery - but check results!') shp_epsg = epsg else: code = ref.GetAuthorityCode(None) shp_epsg = int(code) # check if the raster and the shapefile epsg match if epsg != shp_epsg: logger.error('The projection of the imagery does not match the projection '\ 'of the shapefile you provided!'\ 'EPSG-Code of the Image: EPSG:{0}; '\ 'EPSG-Code of the Shapefile: EPSG:{1}'.format( epsg, shp_epsg)) close_logger(logger) sys.exit( 'An error occured while execute get_mean_refl. Check logfile!') # check the image raster num_bands = 10 # Sentinel-2 bands: B2, B3, B4, B5, B6, B7, B8A, B11, B12 + SLC if (raster.RasterCount != num_bands): logger.error( "The number of bands you provided does not match the image file!") close_logger(logger) sys.exit(-1) # determine the min area of an object (determined by S2 spatial resolution) # use the "standard" resolution of 20 meters # an object must be twice times larger min_area = 20 * 20 * 2 # 20 by 20 meters times two as the minimum size constraint # for requesting the landuse information luc_field = 'LU' + acqui_date.replace('-', '') # Get geometry and extent of feature for ii in range(num_objects): feature = layer.GetFeature(ii) # extract the geometry geom = feature.GetGeometryRef() # get well-known-text of feature geomtry wkt = geom.ExportToWkt() # extract feature ID f_id = feature.GetFID() # get the area of the current feature area = geom.Area() # m2 # the area must be at least 2.5 times larger than the coarsest # possible spatial resolution of Sentinel-2 (60 by 60 meters) if area < min_area: logger.warning('The object {0} was too small compared to the '\ 'spatial resolution of Sentinel-2! '\ 'Object area (m2): {1}; Minimum area required (m2): '\ '{2} -> skipping'.format( f_id, area, min_area)) continue luc = feature.GetField(luc_field) # convert to integer coding if luc is provided as text try: luc = int(luc) except ValueError: luc = luc.upper() query = "SELECT landuse FROM s2_landuse WHERE landuse_semantic = '{0}';".format( luc) cursor.execute(query) res = cursor.fetchall() luc = int(res[0][0]) # end try-except # check for feature type -> could be either POLYGON or MULTIPOLYGON if (geom.GetGeometryName() == 'MULTIPOLYGON'): count = 0 pointsX = [] pointsY = [] for polygon in geom: geomInner = geom.GetGeometryRef(count) ring = geomInner.GetGeometryRef(0) numpoints = ring.GetPointCount() for p in range(numpoints): lon, lat, z = ring.GetPoint(p) pointsX.append(lon) pointsY.append(lat) count += 1 elif (geom.GetGeometryName() == 'POLYGON'): ring = geom.GetGeometryRef(0) numpoints = ring.GetPointCount() pointsX = [] pointsY = [] values = [] for p in range(numpoints): lon, lat, val = ring.GetPoint(p) pointsX.append(lon) pointsY.append(lat) values.append(val) else: sys.exit( "ERROR: Geometry needs to be either Polygon or Multipolygon") #endif #get exact extent of feature for masking xmin = min(pointsX) xmax = max(pointsX) ymin = min(pointsY) ymax = max(pointsY) # Specify offset and rows and columns to read # -> thus, only a part of the array must be read # -> calculate the offset in rows in cols to go the specific part of the S2-raster xoff = int((xmin - xOrigin) / pixelWidth) yoff = int((yOrigin - ymax) / pixelWidth) xcount = int((xmax - xmin) / pixelWidth) + 1 ycount = int((ymax - ymin) / pixelWidth) + 1 # temporary raster for masking the actual feature target_ds = gdal.GetDriverByName('MEM').Create('', xcount, ycount, 1, gdal.GDT_Byte) target_ds.SetGeoTransform(( xmin, pixelWidth, 0, ymax, 0, pixelHeight, )) # Rasterize zone polygon to raster gdal.RasterizeLayer(target_ds, [1], layer, burn_values=[1]) # the mask to be used for the calculation of the stats bandmask = target_ds.GetRasterBand(1) datamask = bandmask.ReadAsArray(0, 0, xcount, ycount).astype(np.float) # Rasterize zone polygon to raster -> thus data is only read at the location of the #actual feature gdal.RasterizeLayer(target_ds, [1], layer, burn_values=[1]) # Read image raster as array meanValues = [] # iterator variable for looping over Sentinel-2 bands index = 1 # in case the object is cloud covered or of affected by cirrus skip_flag = False # iterate over the bands for ii in range(raster.RasterCount): banddataraster = raster.GetRasterBand(index) # read image data at the specific extent covering the actual feature dataraster = banddataraster.ReadAsArray(xoff, yoff, xcount, ycount).astype(np.float) # Mask zone of raster zoneraster = np.ma.masked_array(dataraster, np.logical_not(datamask)) # apply conversion factor of 0.01 to get the correct reflectance # values for ProSAIL if ii < raster.RasterCount - 1: mean = np.nanmean(zoneraster) * 0.01 meanValues.append(mean) # treat the SCL band with the pre-class info differently else: counts = np.bincount(zoneraster) # get the most frequent value argmax = np.argmax(counts) # in case the value is greater than 4 (vegetation) skip the object if argmax > 4.: skip_flag = True # increment index index += 1 #endfor # in case the skip flag was set -> skip if skip_flag: logger.info('The object is not vegetated -> skipping!') continue # check if the results are not nan -> if there are nans skip the object # as the ProSAIL model inversion cannot deal with missing values if any(np.isnan(meanValues)): logger.warning('The object with ID {} contains NaNs -> skipping!') continue # insert the mean reflectane and the object geometry into DB query = "INSERT INTO {0} (object_id, acquisition_date, landuse object_geom, "\ "b2, b3, b4, b5, b6, b7, b8a, b11, b12, scene_id) VALUES ( " \ "{1}, '{2}', {3}, ST_Multi(ST_GeometryFromText('{4}', {5})), " \ "{6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, '{15}'}) "\ " ON CONFLICT (object_id, scene_id) DO NOTHING;".format( table_name, f_id, acqui_date, luc, wkt, epsg, np.round(meanValues[0], 4), np.round(meanValues[1], 4), np.round(meanValues[2], 4), np.round(meanValues[3], 4), np.round(meanValues[4], 4), np.round(meanValues[5], 4), np.round(meanValues[6], 4), np.round(meanValues[7], 4), np.round(meanValues[8], 4), scene_id ) # catch errors for single objects accordingly and continue with next # object to avoid interrupts of whole workflow try: cursor.execute(query) conn.commit() except (DatabaseError, ProgrammingError): logger.error( "Could not insert image object with ID {0} into table '{1}'". format(f_id, table_name), exc_info=True) conn.rollback() continue # endfor # close the GDAL-bindings to the files raster = None shpfile = None layer = None # close database connection close_db_connection(conn, cursor)