def merge_overlapping_images(metadata, inputs): """ Merge simultaneous overlapping images that cover the area of interest. When the area of interest is located at the boundary between 2 images, there will be overlap between the 2 images and both will be downloaded from Google Earth Engine. This function merges the 2 images, so that the area of interest is covered by only 1 image. KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded inputs: dict with the following keys 'sitename': str name of the site 'polygon': list polygon containing the lon/lat coordinates to be extracted, longitudes in the first column and latitudes in the second column, there are 5 pairs of lat/lon with the fifth point equal to the first point: ``` polygon = [[[151.3, -33.7],[151.4, -33.7],[151.4, -33.8],[151.3, -33.8], [151.3, -33.7]]] ``` 'dates': list of str list that contains 2 strings with the initial and final dates in format 'yyyy-mm-dd': ``` dates = ['1987-01-01', '2018-01-01'] ``` 'sat_list': list of str list that contains the names of the satellite missions to include: ``` sat_list = ['L5', 'L7', 'L8', 'S2'] ``` 'filepath_data': str filepath to the directory where the images are downloaded Returns: ----------- metadata_updated: dict updated metadata """ # only for Sentinel-2 at this stage (not sure if this is needed for Landsat images) sat = 'S2' filepath = os.path.join(inputs['filepath'], inputs['sitename']) filenames = metadata[sat]['filenames'] # find the pairs of images that are within 5 minutes of each other time_delta = 5 * 60 # 5 minutes in seconds dates = metadata[sat]['dates'].copy() pairs = [] for i, date in enumerate(metadata[sat]['dates']): # dummy value so it does not match it again dates[i] = pytz.utc.localize(datetime(1, 1, 1) + timedelta(days=i + 1)) # calculate time difference time_diff = np.array( [np.abs((date - _).total_seconds()) for _ in dates]) # find the matching times and add to pairs list boolvec = time_diff <= time_delta if np.sum(boolvec) == 0: continue else: idx_dup = np.where(boolvec)[0][0] pairs.append([i, idx_dup]) # because they could be triplicates in S2 images, adjust the pairs for consecutive merges for i in range(1, len(pairs)): if pairs[i - 1][1] == pairs[i][0]: pairs[i][0] = pairs[i - 1][0] # check also for quadruplicates and remove them pair_first = [_[0] for _ in pairs] idx_remove_pair = [] for idx in np.unique(pair_first): # quadruplicate if trying to merge 3 times the same image with a successive image if sum(pair_first == idx) == 3: # remove the last image: 3 .tif files + the .txt file idx_last = [pairs[_] for _ in np.where(pair_first == idx)[0]][-1][-1] fn_im = [ os.path.join(filepath, 'S2', '10m', filenames[idx_last]), os.path.join(filepath, 'S2', '20m', filenames[idx_last].replace('10m', '20m')), os.path.join(filepath, 'S2', '60m', filenames[idx_last].replace('10m', '60m')), os.path.join( filepath, 'S2', 'meta', filenames[idx_last].replace('_10m', '').replace('.tif', '.txt')) ] for k in range(4): os.chmod(fn_im[k], 0o777) os.remove(fn_im[k]) # store the index of the pair to remove it outside the loop idx_remove_pair.append(np.where(pair_first == idx)[0][-1]) # remove quadruplicates from list of pairs pairs = [i for j, i in enumerate(pairs) if j not in idx_remove_pair] # for each pair of image, first check if one image completely contains the other # in that case keep the larger image. Otherwise merge the two images. for i, pair in enumerate(pairs): # get filenames of all the files corresponding to the each image in the pair fn_im = [] for index in range(len(pair)): fn_im.append([ os.path.join(filepath, 'S2', '10m', filenames[pair[index]]), os.path.join(filepath, 'S2', '20m', filenames[pair[index]].replace('10m', '20m')), os.path.join(filepath, 'S2', '60m', filenames[pair[index]].replace('10m', '60m')), os.path.join( filepath, 'S2', 'meta', filenames[pair[index]].replace('_10m', '').replace('.tif', '.txt')) ]) # get polygon for first image polygon0 = SDS_tools.get_image_bounds(fn_im[0][0]) im_epsg0 = metadata[sat]['epsg'][pair[0]] # get polygon for second image polygon1 = SDS_tools.get_image_bounds(fn_im[1][0]) im_epsg1 = metadata[sat]['epsg'][pair[1]] # check if epsg are the same if not im_epsg0 == im_epsg1: print( 'WARNING: there was an error as two S2 images do not have the same epsg,' + ' please open an issue on Github at https://github.com/kvos/CoastSat/issues' + ' and include your script so we can find out what happened.') break # check if one image contains the other one if polygon0.contains(polygon1): # if polygon0 contains polygon1, remove files for polygon1 for k in range(4): # remove the 3 .tif files + the .txt file os.chmod(fn_im[1][k], 0o777) os.remove(fn_im[1][k]) # print('removed 1') continue elif polygon1.contains(polygon0): # if polygon1 contains polygon0, remove image0 for k in range(4): # remove the 3 .tif files + the .txt file os.chmod(fn_im[0][k], 0o777) os.remove(fn_im[0][k]) # print('removed 0') # adjust the order in case of triplicates if i + 1 < len(pairs): if pairs[i + 1][0] == pair[0]: pairs[i + 1][0] = pairs[i][1] continue # otherwise merge the two images after masking the nodata values else: for index in range(len(pair)): # read image im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = SDS_preprocess.preprocess_single( fn_im[index], sat, False) # in Sentinel2 images close to the edge of the image there are some artefacts, # that are squares with constant pixel intensities. They need to be masked in the # raster (GEOTIFF). It can be done using the image standard deviation, which # indicates values close to 0 for the artefacts. if len(im_ms) > 0: # calculate image std for the first 10m band im_std = SDS_tools.image_std(im_ms[:, :, 0], 1) # convert to binary im_binary = np.logical_or(im_std < 1e-6, np.isnan(im_std)) # dilate to fill the edges (which have high std) mask10 = morphology.dilation(im_binary, morphology.square(3)) # mask the 10m .tif file (add no_data where mask is True) SDS_tools.mask_raster(fn_im[index][0], mask10) # now calculate the mask for the 20m band (SWIR1) # for the older version of the ee api calculate the image std again if int(ee.__version__[-3:]) <= 201: # calculate std to create another mask for the 20m band (SWIR1) im_std = SDS_tools.image_std(im_extra, 1) im_binary = np.logical_or(im_std < 1e-6, np.isnan(im_std)) mask20 = morphology.dilation(im_binary, morphology.square(3)) # for the newer versions just resample the mask for the 10m bands else: # create mask for the 20m band (SWIR1) by resampling the 10m one mask20 = ndimage.zoom(mask10, zoom=1 / 2, order=0) mask20 = transform.resize(mask20, im_extra.shape, mode='constant', order=0, preserve_range=True) mask20 = mask20.astype(bool) # mask the 20m .tif file (im_extra) SDS_tools.mask_raster(fn_im[index][1], mask20) # create a mask for the 60m QA band by resampling the 20m one mask60 = ndimage.zoom(mask20, zoom=1 / 3, order=0) mask60 = transform.resize(mask60, im_QA.shape, mode='constant', order=0, preserve_range=True) mask60 = mask60.astype(bool) # mask the 60m .tif file (im_QA) SDS_tools.mask_raster(fn_im[index][2], mask60) # make a figure for quality control/debugging # im_RGB = SDS_preprocess.rescale_image_intensity(im_ms[:,:,[2,1,0]], cloud_mask, 99.9) # fig,ax= plt.subplots(2,3,tight_layout=True) # ax[0,0].imshow(im_RGB) # ax[0,0].set_title('RGB original') # ax[1,0].imshow(mask10) # ax[1,0].set_title('Mask 10m') # ax[0,1].imshow(mask20) # ax[0,1].set_title('Mask 20m') # ax[1,1].imshow(mask60) # ax[1,1].set_title('Mask 60 m') # ax[0,2].imshow(im_QA) # ax[0,2].set_title('Im QA') # ax[1,2].imshow(im_nodata) # ax[1,2].set_title('Im nodata') else: continue # once all the pairs of .tif files have been masked with no_data, merge the using gdal_merge fn_merged = os.path.join(filepath, 'merged.tif') for k in range(3): # merge masked bands gdal_merge.main( ['', '-o', fn_merged, '-n', '0', fn_im[0][k], fn_im[1][k]]) # remove old files os.chmod(fn_im[0][k], 0o777) os.remove(fn_im[0][k]) os.chmod(fn_im[1][k], 0o777) os.remove(fn_im[1][k]) # rename new file fn_new = fn_im[0][k].split('.')[0] + '_merged.tif' os.chmod(fn_merged, 0o777) os.rename(fn_merged, fn_new) # open both metadata files metadict0 = dict([]) with open(fn_im[0][3], 'r') as f: metadict0['filename'] = f.readline().split('\t')[1].replace( '\n', '') metadict0['acc_georef'] = float( f.readline().split('\t')[1].replace('\n', '')) metadict0['epsg'] = int(f.readline().split('\t')[1].replace( '\n', '')) metadict1 = dict([]) with open(fn_im[1][3], 'r') as f: metadict1['filename'] = f.readline().split('\t')[1].replace( '\n', '') metadict1['acc_georef'] = float( f.readline().split('\t')[1].replace('\n', '')) metadict1['epsg'] = int(f.readline().split('\t')[1].replace( '\n', '')) # check if both images have the same georef accuracy if np.any( np.array([ metadict0['acc_georef'], metadict1['acc_georef'] ]) == -1): metadict0['georef'] = -1 # add new name metadict0['filename'] = metadict0['filename'].split( '.')[0] + '_merged.tif' # remove the old metadata.txt files os.chmod(fn_im[0][3], 0o777) os.remove(fn_im[0][3]) os.chmod(fn_im[1][3], 0o777) os.remove(fn_im[1][3]) # rewrite the .txt file with a new metadata file fn_new = fn_im[0][3].split('.')[0] + '_merged.txt' with open(fn_new, 'w') as f: for key in metadict0.keys(): f.write('%s\t%s\n' % (key, metadict0[key])) # update filenames list (in case there are triplicates) filenames[pair[0]] = metadict0['filename'] print( '%d out of %d Sentinel-2 images were merged (overlapping or duplicate)' % (len(pairs), len(filenames))) # update the metadata dict metadata_updated = get_metadata(inputs) return metadata_updated
def extract_shorelines(metadata, settings): """ Extracts shorelines from satellite images. KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded settings: dict contains the following fields: sitename: str String containig the name of the site cloud_mask_issue: boolean True if there is an issue with the cloud mask and sand pixels are being masked on the images buffer_size: int size of the buffer (m) around the sandy beach over which the pixels are considered in the thresholding algorithm min_beach_area: int minimum allowable object area (in metres^2) for the class 'sand' cloud_thresh: float value between 0 and 1 defining the maximum percentage of cloud cover allowed in the images output_epsg: int output spatial reference system as EPSG code check_detection: boolean True to show each invidual detection and let the user validate the mapped shoreline Returns: ----------- output: dict contains the extracted shorelines and corresponding dates. """ sitename = settings['inputs']['sitename'] filepath_data = settings['inputs']['filepath'] # initialise output structure output = dict([]) # create a subfolder to store the .jpg images showing the detection filepath_jpg = os.path.join(filepath_data, sitename, 'jpg_files', 'detection') if not os.path.exists(filepath_jpg): os.makedirs(filepath_jpg) # close all open figures plt.close('all') print('Mapping shorelines:') # loop through satellite list for satname in metadata.keys(): # get images filepath = SDS_tools.get_filepath(settings['inputs'], satname) filenames = metadata[satname]['filenames'] # initialise the output variables output_timestamp = [ ] # datetime at which the image was acquired (UTC time) output_shoreline = [] # vector of shoreline points output_filename = [ ] # filename of the images from which the shorelines where derived output_cloudcover = [] # cloud cover of the images output_geoaccuracy = [] # georeferencing accuracy of the images output_idxkeep = [ ] # index that were kept during the analysis (cloudy images are skipped) # load classifiers and if satname in ['L5', 'L7', 'L8']: pixel_size = 15 if settings['dark_sand']: clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_Landsat_dark.pkl')) else: clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_Landsat.pkl')) elif satname == 'S2': pixel_size = 10 clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_S2.pkl')) # convert settings['min_beach_area'] and settings['buffer_size'] from metres to pixels buffer_size_pixels = np.ceil(settings['buffer_size'] / pixel_size) min_beach_area_pixels = np.ceil(settings['min_beach_area'] / pixel_size**2) # loop through the images for i in range(len(filenames)): print('\r%s: %d%%' % (satname, int(((i + 1) / len(filenames)) * 100)), end='') # get image filename fn = SDS_tools.get_filenames(filenames[i], filepath, satname) # preprocess image (cloud mask + pansharpening/downsampling) im_ms, georef, cloud_mask, im_extra, imQA = SDS_preprocess.preprocess_single( fn, satname, settings['cloud_mask_issue']) # get image spatial reference system (epsg code) from metadata dict image_epsg = metadata[satname]['epsg'][i] # calculate cloud cover cloud_cover = np.divide( sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0] * cloud_mask.shape[1])) # skip image if cloud cover is above threshold if cloud_cover > settings['cloud_thresh']: continue # classify image in 4 classes (sand, whitewater, water, other) with NN classifier im_classif, im_labels = classify_image_NN(im_ms, im_extra, cloud_mask, min_beach_area_pixels, clf) # calculate a buffer around the reference shoreline (if any has been digitised) im_ref_buffer = create_shoreline_buffer(cloud_mask.shape, georef, image_epsg, pixel_size, settings) # there are two options to extract to map the contours: # if there are pixels in the 'sand' class --> use find_wl_contours2 (enhanced) # otherwise use find_wl_contours2 (traditional) try: # use try/except structure for long runs if sum(sum(im_labels[:, :, 0])) == 0: # compute MNDWI image (SWIR-G) im_mndwi = SDS_tools.nd_index(im_ms[:, :, 4], im_ms[:, :, 1], cloud_mask) # find water contours on MNDWI grayscale image contours_mwi = find_wl_contours1(im_mndwi, cloud_mask, im_ref_buffer) else: # use classification to refine threshold and extract the sand/water interface contours_wi, contours_mwi = find_wl_contours2( im_ms, im_labels, cloud_mask, buffer_size_pixels, im_ref_buffer) except: print('Could not map shoreline for this image: ' + filenames[i]) continue # process water contours into shorelines shoreline = process_shoreline(contours_mwi, georef, image_epsg, settings) # visualise the mapped shorelines, there are two options: # if settings['check_detection'] = True, shows the detection to the user for accept/reject # if settings['save_figure'] = True, saves a figure for each mapped shoreline if settings['check_detection'] or settings['save_figure']: date = filenames[i][:19] skip_image = show_detection(im_ms, cloud_mask, im_labels, shoreline, image_epsg, georef, settings, date, satname) # if the user decides to skip the image, continue and do not save the mapped shoreline if skip_image: continue # append to output variables output_timestamp.append(metadata[satname]['dates'][i]) output_shoreline.append(shoreline) output_filename.append(filenames[i]) output_cloudcover.append(cloud_cover) output_geoaccuracy.append(metadata[satname]['acc_georef'][i]) output_idxkeep.append(i) # create dictionnary of output output[satname] = { 'dates': output_timestamp, 'shorelines': output_shoreline, 'filename': output_filename, 'cloud_cover': output_cloudcover, 'geoaccuracy': output_geoaccuracy, 'idx': output_idxkeep } print('') # Close figure window if still open if plt.get_fignums(): plt.close() # change the format to have one list sorted by date with all the shorelines (easier to use) output = SDS_tools.merge_output(output) # save outputput structure as output.pkl filepath = os.path.join(filepath_data, sitename) with open(os.path.join(filepath, sitename + '_output.pkl'), 'wb') as f: pickle.dump(output, f) # save output into a gdb.GeoDataFrame gdf = SDS_tools.output_to_gdf(output) # set projection gdf.crs = {'init': 'epsg:' + str(settings['output_epsg'])} # save as geojson gdf.to_file(os.path.join(filepath, sitename + '_output.geojson'), driver='GeoJSON', encoding='utf-8') return output
} # [OPTIONAL] preprocess images (cloud masking, pansharpening/down-sampling) SDS_preprocess.save_jpg(metadata, settings) # [OPTIONAL] create a reference shoreline (helps to identify outliers and false detections) settings['reference_shoreline'] = SDS_preprocess.get_reference_sl( metadata, settings) # set the max distance (in meters) allowed from the reference shoreline for a detected shoreline to be valid settings['max_dist_ref'] = 100 # extract shorelines from all images (also saves output.pkl and shorelines.kml) output = SDS_shoreline.extract_shorelines(metadata, settings) # remove duplicates (images taken on the same date by the same satellite) output = SDS_tools.remove_duplicates(output) # remove inaccurate georeferencing (set threshold to 10 m) output = SDS_tools.remove_inaccurate_georef(output, 10) # plot the mapped shorelines fig = plt.figure(figsize=[15, 8], tight_layout=True) plt.axis('equal') plt.xlabel('Eastings') plt.ylabel('Northings') plt.grid(linestyle=':', color='0.5') for i in range(len(output['shorelines'])): sl = output['shorelines'][i] date = output['dates'][i] plt.plot(sl[:, 0], sl[:, 1], '.', label=date.strftime('%d-%m-%Y')) plt.legend()
def adjust_detection(im_ms, cloud_mask, im_labels, im_ref_buffer, image_epsg, georef, settings, date, satname, buffer_size_pixels): """ Advanced version of show detection where the user can adjust the detected shorelines with a slide bar. KV WRL 2020 Arguments: ----------- im_ms: np.array RGB + downsampled NIR and SWIR cloud_mask: np.array 2D cloud mask with True where cloud pixels are im_labels: np.array 3D image containing a boolean image for each class in the order (sand, swash, water) im_ref_buffer: np.array Binary image containing a buffer around the reference shoreline image_epsg: int spatial reference system of the image from which the contours were extracted georef: np.array vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale] date: string date at which the image was taken satname: string indicates the satname (L5,L7,L8 or S2) buffer_size_pixels: int buffer_size converted to number of pixels settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) 'output_epsg': int output spatial reference system as EPSG code 'save_figure': bool if True, saves a -jpg file for each mapped shoreline Returns: ----------- skip_image: boolean True if the user wants to skip the image, False otherwise shoreline: np.array array of points with the X and Y coordinates of the shoreline t_mndwi: float value of the MNDWI threshold used to map the shoreline """ sitename = settings['inputs']['sitename'] filepath_data = settings['inputs']['filepath'] # subfolder where the .jpg file is stored if the user accepts the shoreline detection filepath = os.path.join(filepath_data, sitename, 'jpg_files', 'detection') # format date date_str = datetime.strptime(date,'%Y-%m-%d-%H-%M-%S').strftime('%Y-%m-%d %H:%M:%S') im_RGB = SDS_preprocess.rescale_image_intensity(im_ms[:,:,[2,1,0]], cloud_mask, 99.9) # compute classified image im_class = np.copy(im_RGB) cmap = cm.get_cmap('tab20c') colorpalette = cmap(np.arange(0,13,1)) colours = np.zeros((3,4)) colours[0,:] = colorpalette[5] colours[1,:] = np.array([204/255,1,1,1]) colours[2,:] = np.array([0,91/255,1,1]) for k in range(0,im_labels.shape[2]): im_class[im_labels[:,:,k],0] = colours[k,0] im_class[im_labels[:,:,k],1] = colours[k,1] im_class[im_labels[:,:,k],2] = colours[k,2] # compute MNDWI grayscale image im_mndwi = SDS_tools.nd_index(im_ms[:,:,4], im_ms[:,:,1], cloud_mask) # buffer MNDWI using reference shoreline im_mndwi_buffer = np.copy(im_mndwi) im_mndwi_buffer[~im_ref_buffer] = np.nan # get MNDWI pixel intensity in each class (for histogram plot) int_sand = im_mndwi[im_labels[:,:,0]] int_ww = im_mndwi[im_labels[:,:,1]] int_water = im_mndwi[im_labels[:,:,2]] labels_other = np.logical_and(np.logical_and(~im_labels[:,:,0],~im_labels[:,:,1]),~im_labels[:,:,2]) int_other = im_mndwi[labels_other] # create figure if plt.get_fignums(): # if it exists, open the figure fig = plt.gcf() ax1 = fig.axes[0] ax2 = fig.axes[1] ax3 = fig.axes[2] ax4 = fig.axes[3] else: # else create a new figure fig = plt.figure() fig.set_size_inches([18, 9]) mng = plt.get_current_fig_manager() mng.window.showMaximized() gs = gridspec.GridSpec(2, 3, height_ratios=[4,1]) gs.update(bottom=0.05, top=0.95, left=0.03, right=0.97) ax1 = fig.add_subplot(gs[0,0]) ax2 = fig.add_subplot(gs[0,1], sharex=ax1, sharey=ax1) ax3 = fig.add_subplot(gs[0,2], sharex=ax1, sharey=ax1) ax4 = fig.add_subplot(gs[1,:]) ########################################################################## # to do: rotate image if too wide ########################################################################## # change the color of nans to either black (0.0) or white (1.0) or somewhere in between nan_color = 1.0 im_RGB = np.where(np.isnan(im_RGB), nan_color, im_RGB) im_class = np.where(np.isnan(im_class), 1.0, im_class) # plot image 1 (RGB) ax1.imshow(im_RGB) ax1.axis('off') ax1.set_title('%s - %s'%(sitename, satname), fontsize=12) # plot image 2 (classification) ax2.imshow(im_class) ax2.axis('off') orange_patch = mpatches.Patch(color=colours[0,:], label='sand') white_patch = mpatches.Patch(color=colours[1,:], label='whitewater') blue_patch = mpatches.Patch(color=colours[2,:], label='water') black_line = mlines.Line2D([],[],color='k',linestyle='-', label='shoreline') ax2.legend(handles=[orange_patch,white_patch,blue_patch, black_line], bbox_to_anchor=(1.1, 0.5), fontsize=10) ax2.set_title(date_str, fontsize=12) # plot image 3 (MNDWI) ax3.imshow(im_mndwi, cmap='bwr') ax3.axis('off') ax3.set_title('MNDWI', fontsize=12) # plot histogram of MNDWI values binwidth = 0.01 ax4.set_facecolor('0.75') ax4.yaxis.grid(color='w', linestyle='--', linewidth=0.5) ax4.set(ylabel='PDF',yticklabels=[], xlim=[-1,1]) if len(int_sand) > 0 and sum(~np.isnan(int_sand)) > 0: bins = np.arange(np.nanmin(int_sand), np.nanmax(int_sand) + binwidth, binwidth) ax4.hist(int_sand, bins=bins, density=True, color=colours[0,:], label='sand') if len(int_ww) > 0 and sum(~np.isnan(int_ww)) > 0: bins = np.arange(np.nanmin(int_ww), np.nanmax(int_ww) + binwidth, binwidth) ax4.hist(int_ww, bins=bins, density=True, color=colours[1,:], label='whitewater', alpha=0.75) if len(int_water) > 0 and sum(~np.isnan(int_water)) > 0: bins = np.arange(np.nanmin(int_water), np.nanmax(int_water) + binwidth, binwidth) ax4.hist(int_water, bins=bins, density=True, color=colours[2,:], label='water', alpha=0.75) if len(int_other) > 0 and sum(~np.isnan(int_other)) > 0: bins = np.arange(np.nanmin(int_other), np.nanmax(int_other) + binwidth, binwidth) ax4.hist(int_other, bins=bins, density=True, color='C4', label='other', alpha=0.5) # automatically map the shoreline based on the classifier if enough sand pixels try: if sum(sum(im_labels[:,:,0])) > 10: # use classification to refine threshold and extract the sand/water interface contours_mndwi, t_mndwi = find_wl_contours2(im_ms, im_labels, cloud_mask, buffer_size_pixels, im_ref_buffer) else: # find water contours on MNDWI grayscale image contours_mndwi, t_mndwi = find_wl_contours1(im_mndwi, cloud_mask, im_ref_buffer) except: print('Could not map shoreline so image was skipped') # clear axes and return skip_image=True, so that image is skipped above for ax in fig.axes: ax.clear() return True,[],[] # process the water contours into a shoreline shoreline = process_shoreline(contours_mndwi, cloud_mask, georef, image_epsg, settings) # convert shoreline to pixels if len(shoreline) > 0: sl_pix = SDS_tools.convert_world2pix(SDS_tools.convert_epsg(shoreline, settings['output_epsg'], image_epsg)[:,[0,1]], georef) else: sl_pix = np.array([[np.nan, np.nan],[np.nan, np.nan]]) # plot the shoreline on the images sl_plot1 = ax1.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) sl_plot2 = ax2.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) sl_plot3 = ax3.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) t_line = ax4.axvline(x=t_mndwi,ls='--', c='k', lw=1.5, label='threshold') ax4.legend(loc=1) plt.draw() # to update the plot # adjust the threshold manually by letting the user change the threshold ax4.set_title('Click on the plot below to change the location of the threhsold and adjust the shoreline detection. When finished, press <Enter>') while True: # let the user click on the threshold plot pt = ginput(n=1, show_clicks=True, timeout=-1) # if a point was clicked if len(pt) > 0: # if user clicked somewhere wrong and value is not between -1 and 1 if np.abs(pt[0][0]) >= 1: continue # update the threshold value t_mndwi = pt[0][0] # update the plot t_line.set_xdata([t_mndwi,t_mndwi]) # map contours with new threshold contours = measure.find_contours(im_mndwi_buffer, t_mndwi) # remove contours that contain NaNs (due to cloud pixels in the contour) contours = process_contours(contours) # process the water contours into a shoreline shoreline = process_shoreline(contours, cloud_mask, georef, image_epsg, settings) # convert shoreline to pixels if len(shoreline) > 0: sl_pix = SDS_tools.convert_world2pix(SDS_tools.convert_epsg(shoreline, settings['output_epsg'], image_epsg)[:,[0,1]], georef) else: sl_pix = np.array([[np.nan, np.nan],[np.nan, np.nan]]) # update the plotted shorelines sl_plot1[0].set_data([sl_pix[:,0], sl_pix[:,1]]) sl_plot2[0].set_data([sl_pix[:,0], sl_pix[:,1]]) sl_plot3[0].set_data([sl_pix[:,0], sl_pix[:,1]]) fig.canvas.draw_idle() else: ax4.set_title('MNDWI pixel intensities and threshold') break # let user manually accept/reject the image skip_image = False # set a key event to accept/reject the detections (see https://stackoverflow.com/a/15033071) # this variable needs to be immuatable so we can access it after the keypress event key_event = {} def press(event): # store what key was pressed in the dictionary key_event['pressed'] = event.key # let the user press a key, right arrow to keep the image, left arrow to skip it # to break the loop the user can press 'escape' while True: btn_keep = plt.text(1.1, 0.9, 'keep ⇨', size=12, ha="right", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) btn_skip = plt.text(-0.1, 0.9, '⇦ skip', size=12, ha="left", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) btn_esc = plt.text(0.5, 0, '<esc> to quit', size=12, ha="center", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) plt.draw() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() # after button is pressed, remove the buttons btn_skip.remove() btn_keep.remove() btn_esc.remove() # keep/skip image according to the pressed key, 'escape' to break the loop if key_event.get('pressed') == 'right': skip_image = False break elif key_event.get('pressed') == 'left': skip_image = True break elif key_event.get('pressed') == 'escape': plt.close() raise StopIteration('User cancelled checking shoreline detection') else: plt.waitforbuttonpress() # if save_figure is True, save a .jpg under /jpg_files/detection if settings['save_figure'] and not skip_image: fig.savefig(os.path.join(filepath, date + '_' + satname + '.jpg'), dpi=150) # don't close the figure window, but remove all axes and settings, ready for next plot for ax in fig.axes: ax.clear() return skip_image, shoreline, t_mndwi
def draw_transects(output, settings): """ Draw shore-normal transects interactively on top of the mapped shorelines KV WRL 2018 Arguments: ----------- output: dict contains the extracted shorelines and corresponding metadata settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) Returns: ----------- transects: dict contains the X and Y coordinates of all the transects drawn. Also saves the coordinates as a .geojson as well as a .jpg figure showing the location of the transects. """ sitename = settings['inputs']['sitename'] filepath = os.path.join(settings['inputs']['filepath'], sitename) # plot the mapped shorelines fig1 = plt.figure() ax1 = fig1.add_subplot(111) ax1.axis('equal') ax1.set_xlabel('Eastings [m]') ax1.set_ylabel('Northings [m]') ax1.grid(linestyle=':', color='0.5') for i in range(len(output['shorelines'])): sl = output['shorelines'][i] date = output['dates'][i] ax1.plot(sl[:, 0], sl[:, 1], '.', markersize=3, label=date.strftime('%d-%m-%Y')) # ax1.legend() fig1.set_tight_layout(True) mng = plt.get_current_fig_manager() mng.window.showMaximized() ax1.set_title( 'Click two points to define each transect (first point is the ' + 'origin of the transect and is landwards, second point seawards).\n' + 'When all transects have been defined, click on <ENTER>', fontsize=16) # initialise transects dict transects = dict([]) counter = 0 # loop until user breaks it by click <enter> while 1: # let user click two points pts = ginput(n=2, timeout=-1) if len(pts) > 0: origin = pts[0] # if user presses <enter>, no points are selected else: # save figure as .jpg fig1.gca().set_title('Transect locations', fontsize=16) fig1.savefig(os.path.join(filepath, 'jpg_files', sitename + '_transect_locations.jpg'), dpi=200) plt.title('Transect coordinates saved as ' + sitename + '_transects.geojson') plt.draw() # wait 2 seconds for user to visualise the transects that are saved ginput(n=1, timeout=2, show_clicks=True) plt.close(fig1) # break the loop break # add selectect points to the transect dict counter = counter + 1 transect = np.array([pts[0], pts[1]]) # alternative of making the transect the origin, orientation and length # temp = np.array(pts[1]) - np.array(origin) # phi = np.arctan2(temp[1], temp[0]) # orientation = -(phi*180/np.pi - 90) # length = np.linalg.norm(temp) # transect = create_transect(origin, orientation, length) transects[str(counter)] = transect # plot the transects on the figure ax1.plot(transect[:, 0], transect[:, 1], 'b-', lw=2.5) ax1.plot(transect[0, 0], transect[0, 1], 'rx', markersize=10) ax1.text(transect[-1, 0], transect[-1, 1], str(counter), size=16, bbox=dict(boxstyle="square", ec='k', fc='w')) plt.draw() # save transects.geojson gdf = SDS_tools.transects_to_gdf(transects) # set projection gdf.crs = {'init': 'epsg:' + str(settings['output_epsg'])} # save as geojson gdf.to_file(os.path.join(filepath, sitename + '_transects.geojson'), driver='GeoJSON', encoding='utf-8') # print the location of the files print('Transect locations saved in ' + filepath) return transects
def extract_shorelines(metadata, settings): """ Main function to extract shorelines from satellite images KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) 'cloud_thresh': float value between 0 and 1 indicating the maximum cloud fraction in the cropped image that is accepted 'cloud_mask_issue': boolean True if there is an issue with the cloud mask and sand pixels are erroneously being masked on the images 'buffer_size': int size of the buffer (m) around the sandy pixels over which the pixels are considered in the thresholding algorithm 'min_beach_area': int minimum allowable object area (in metres^2) for the class 'sand', the area is converted to number of connected pixels 'min_length_sl': int minimum length (in metres) of shoreline contour to be valid 'sand_color': str default', 'dark' (for grey/black sand beaches) or 'bright' (for white sand beaches) 'output_epsg': int output spatial reference system as EPSG code 'check_detection': bool if True, lets user manually accept/reject the mapped shorelines 'save_figure': bool if True, saves a -jpg file for each mapped shoreline 'adjust_detection': bool if True, allows user to manually adjust the detected shoreline Returns: ----------- output: dict contains the extracted shorelines and corresponding dates + metadata """ sitename = settings['inputs']['sitename'] filepath_data = settings['inputs']['filepath'] filepath_models = os.path.join(os.getcwd(), 'classification', 'models') # initialise output structure output = dict([]) # create a subfolder to store the .jpg images showing the detection filepath_jpg = os.path.join(filepath_data, sitename, 'jpg_files', 'detection') if not os.path.exists(filepath_jpg): os.makedirs(filepath_jpg) # close all open figures plt.close('all') print('Mapping shorelines:') # loop through satellite list for satname in metadata.keys(): # get images filepath = SDS_tools.get_filepath(settings['inputs'],satname) filenames = metadata[satname]['filenames'] # initialise the output variables output_timestamp = [] # datetime at which the image was acquired (UTC time) output_shoreline = [] # vector of shoreline points output_filename = [] # filename of the images from which the shorelines where derived output_cloudcover = [] # cloud cover of the images output_geoaccuracy = []# georeferencing accuracy of the images output_idxkeep = [] # index that were kept during the analysis (cloudy images are skipped) output_t_mndwi = [] # MNDWI threshold used to map the shoreline # load classifiers (if sklearn version above 0.20, learn the new files) str_new = '' if not sklearn.__version__[:4] == '0.20': str_new = '_new' if satname in ['L5','L7','L8']: pixel_size = 15 if settings['sand_color'] == 'dark': clf = joblib.load(os.path.join(filepath_models, 'NN_4classes_Landsat_dark%s.pkl'%str_new)) elif settings['sand_color'] == 'bright': clf = joblib.load(os.path.join(filepath_models, 'NN_4classes_Landsat_bright%s.pkl'%str_new)) else: clf = joblib.load(os.path.join(filepath_models, 'NN_4classes_Landsat%s.pkl'%str_new)) elif satname == 'S2': pixel_size = 10 clf = joblib.load(os.path.join(filepath_models, 'NN_4classes_S2%s.pkl'%str_new)) # convert settings['min_beach_area'] and settings['buffer_size'] from metres to pixels buffer_size_pixels = np.ceil(settings['buffer_size']/pixel_size) min_beach_area_pixels = np.ceil(settings['min_beach_area']/pixel_size**2) # loop through the images for i in range(len(filenames)): print('\r%s: %d%%' % (satname,int(((i+1)/len(filenames))*100)), end='') # get image filename fn = SDS_tools.get_filenames(filenames[i],filepath, satname) # preprocess image (cloud mask + pansharpening/downsampling) im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = SDS_preprocess.preprocess_single(fn, satname, settings['cloud_mask_issue']) # get image spatial reference system (epsg code) from metadata dict image_epsg = metadata[satname]['epsg'][i] # compute cloud_cover percentage (with no data pixels) cloud_cover_combined = np.divide(sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0]*cloud_mask.shape[1])) if cloud_cover_combined > 0.99: # if 99% of cloudy pixels in image skip continue # remove no data pixels from the cloud mask # (for example L7 bands of no data should not be accounted for) cloud_mask_adv = np.logical_xor(cloud_mask, im_nodata) # compute updated cloud cover percentage (without no data pixels) cloud_cover = np.divide(sum(sum(cloud_mask_adv.astype(int))), (cloud_mask.shape[0]*cloud_mask.shape[1])) # skip image if cloud cover is above user-defined threshold if cloud_cover > settings['cloud_thresh']: continue # calculate a buffer around the reference shoreline (if any has been digitised) im_ref_buffer = create_shoreline_buffer(cloud_mask.shape, georef, image_epsg, pixel_size, settings) # classify image in 4 classes (sand, whitewater, water, other) with NN classifier im_classif, im_labels = classify_image_NN(im_ms, im_extra, cloud_mask, min_beach_area_pixels, clf) # if adjust_detection is True, let the user adjust the detected shoreline if settings['adjust_detection']: date = filenames[i][:19] skip_image, shoreline, t_mndwi = adjust_detection(im_ms, cloud_mask, im_labels, im_ref_buffer, image_epsg, georef, settings, date, satname, buffer_size_pixels) # if the user decides to skip the image, continue and do not save the mapped shoreline if skip_image: continue # otherwise map the contours automatically with one of the two following functions: # if there are pixels in the 'sand' class --> use find_wl_contours2 (enhanced) # otherwise use find_wl_contours2 (traditional) else: try: # use try/except structure for long runs if sum(sum(im_labels[:,:,0])) < 10 : # minimum number of sand pixels # compute MNDWI image (SWIR-G) im_mndwi = SDS_tools.nd_index(im_ms[:,:,4], im_ms[:,:,1], cloud_mask) # find water contours on MNDWI grayscale image contours_mwi, t_mndwi = find_wl_contours1(im_mndwi, cloud_mask, im_ref_buffer) else: # use classification to refine threshold and extract the sand/water interface contours_mwi, t_mndwi = find_wl_contours2(im_ms, im_labels, cloud_mask, buffer_size_pixels, im_ref_buffer) except: print('Could not map shoreline for this image: ' + filenames[i]) continue # process the water contours into a shoreline shoreline = process_shoreline(contours_mwi, cloud_mask, georef, image_epsg, settings) # visualise the mapped shorelines, there are two options: # if settings['check_detection'] = True, shows the detection to the user for accept/reject # if settings['save_figure'] = True, saves a figure for each mapped shoreline if settings['check_detection'] or settings['save_figure']: date = filenames[i][:19] if not settings['check_detection']: plt.ioff() # turning interactive plotting off skip_image = show_detection(im_ms, cloud_mask, im_labels, shoreline, image_epsg, georef, settings, date, satname) # if the user decides to skip the image, continue and do not save the mapped shoreline if skip_image: continue # append to output variables output_timestamp.append(metadata[satname]['dates'][i]) output_shoreline.append(shoreline) output_filename.append(filenames[i]) output_cloudcover.append(cloud_cover) output_geoaccuracy.append(metadata[satname]['acc_georef'][i]) output_idxkeep.append(i) output_t_mndwi.append(t_mndwi) # create dictionnary of output output[satname] = { 'dates': output_timestamp, 'shorelines': output_shoreline, 'filename': output_filename, 'cloud_cover': output_cloudcover, 'geoaccuracy': output_geoaccuracy, 'idx': output_idxkeep, 'MNDWI_threshold': output_t_mndwi, } print('') # close figure window if still open if plt.get_fignums(): plt.close() # change the format to have one list sorted by date with all the shorelines (easier to use) output = SDS_tools.merge_output(output) # save outputput structure as output.pkl filepath = os.path.join(filepath_data, sitename) with open(os.path.join(filepath, sitename + '_output.pkl'), 'wb') as f: pickle.dump(output, f) return output
# Kilian Vos WRL 2018 #%% 1. Initial settings # load modules import os import numpy as np import pickle import warnings warnings.filterwarnings("ignore") import matplotlib.pyplot as plt from coastsat import SDS_islands, SDS_download, SDS_preprocess, SDS_tools, SDS_transects # region of interest (longitude, latitude in WGS84), can be loaded from a .kml polygon polygon = SDS_tools.polygon_from_kml( os.path.join(os.getcwd(), 'example', 'EVA.kml')) # or enter the coordinates (first and last pair of coordinates are the same) # polygon = [[114.4249504953477, -21.9295184484435], # [114.4383556651795, -21.92949300318377], # [114.4388731500701, -21.91491228133647], # [114.4250081185656, -21.91495393621703], # [114.4249504953477, -21.9295184484435]] # date range dates = ['2019-01-01', '2019-02-01'] # satellite missions sat_list = ['S2'] # name of the site sitename = 'EVA'
default="default", ) o = parser.parse_args(sys.argv[1:]) slope_value = None if o.slope != "default": slope_value = float(o.slope) if o.site != "default" and o.start != "default" and o.end != "default": filepath_data = os.path.join(os.getcwd(), "data") sitename = o.site metadata = [] kml_polygon = os.path.join(filepath_data, sitename, sitename + ".kml") polygon = SDS_tools.polygon_from_kml(kml_polygon) dates = [o.start, o.end] sat_list = ["L5", "L7", "L8", "S2"] pts_sl = np.expand_dims(np.array([np.nan, np.nan]), axis=0) with open( os.path.join(filepath_data, sitename, sitename + "_shoreline.csv")) as csv_file: csv_reader = csv.reader(csv_file, delimiter=",") for row in csv_reader: item = np.array([float(row[0]), float(row[1])]) pts_sl = np.vstack((pts_sl, item)) pts_sl = np.delete(pts_sl, 0, axis=0) pts_world_interp = utils.get_interpolate_points(pts_sl)
def get_reference_sl(metadata, settings): """ Allows the user to manually digitize a reference shoreline that is used seed the shoreline detection algorithm. The reference shoreline helps to detect the outliers, making the shoreline detection more robust. KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) 'cloud_thresh': float value between 0 and 1 indicating the maximum cloud fraction in the cropped image that is accepted 'cloud_mask_issue': boolean True if there is an issue with the cloud mask and sand pixels are erroneously being masked on the images 'output_epsg': int output spatial reference system as EPSG code Returns: ----------- reference_shoreline: np.array coordinates of the reference shoreline that was manually digitized. This is also saved as a .pkl and .geojson file. """ sitename = settings['inputs']['sitename'] filepath_data = settings['inputs']['filepath'] pts_coords = [] # check if reference shoreline already exists in the corresponding folder filepath = os.path.join(filepath_data, sitename) filename = sitename + '_reference_shoreline.pkl' # if it exist, load it and return it if filename in os.listdir(filepath): print('Reference shoreline already exists and was loaded') with open(os.path.join(filepath, sitename + '_reference_shoreline.pkl'), 'rb') as f: refsl = pickle.load(f) return refsl # otherwise get the user to manually digitise a shoreline on S2, L8 or L5 images (no L7 because of scan line error) else: # first try to use S2 images (10m res for manually digitizing the reference shoreline) if 'S2' in metadata.keys(): satname = 'S2' filepath = SDS_tools.get_filepath(settings['inputs'],satname) filenames = metadata[satname]['filenames'] # if no S2 images, try L8 (15m res in the RGB with pansharpening) elif not 'S2' in metadata.keys() and 'L8' in metadata.keys(): satname = 'L8' filepath = SDS_tools.get_filepath(settings['inputs'],satname) filenames = metadata[satname]['filenames'] # if no S2 images and no L8, use L5 images (L7 images have black diagonal bands making it # hard to manually digitize a shoreline) elif not 'S2' in metadata.keys() and not 'L8' in metadata.keys() and 'L5' in metadata.keys(): satname = 'L5' filepath = SDS_tools.get_filepath(settings['inputs'],satname) filenames = metadata[satname]['filenames'] else: raise Exception('You cannot digitize the shoreline on L7 images (because of gaps in the images), add another L8, S2 or L5 to your dataset.') # create figure fig, ax = plt.subplots(1,1, figsize=[18,9], tight_layout=True) mng = plt.get_current_fig_manager() mng.window.showMaximized() # loop trhough the images for i in range(len(filenames)): # read image fn = SDS_tools.get_filenames(filenames[i],filepath, satname) im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = preprocess_single(fn, satname, settings['cloud_mask_issue']) # calculate cloud cover cloud_cover = np.divide(sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0]*cloud_mask.shape[1])) # skip image if cloud cover is above threshold if cloud_cover > settings['cloud_thresh']: continue # rescale image intensity for display purposes im_RGB = rescale_image_intensity(im_ms[:,:,[2,1,0]], cloud_mask, 99.9) # plot the image RGB on a figure ax.axis('off') ax.imshow(im_RGB) # decide if the image if good enough for digitizing the shoreline ax.set_title('Press <right arrow> if image is clear enough to digitize the shoreline.\n' + 'If the image is cloudy press <left arrow> to get another image', fontsize=14) # set a key event to accept/reject the detections (see https://stackoverflow.com/a/15033071) # this variable needs to be immuatable so we can access it after the keypress event skip_image = False key_event = {} def press(event): # store what key was pressed in the dictionary key_event['pressed'] = event.key # let the user press a key, right arrow to keep the image, left arrow to skip it # to break the loop the user can press 'escape' while True: btn_keep = plt.text(1.1, 0.9, 'keep ⇨', size=12, ha="right", va="top", transform=ax.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) btn_skip = plt.text(-0.1, 0.9, '⇦ skip', size=12, ha="left", va="top", transform=ax.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) btn_esc = plt.text(0.5, 0, '<esc> to quit', size=12, ha="center", va="top", transform=ax.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) plt.draw() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() # after button is pressed, remove the buttons btn_skip.remove() btn_keep.remove() btn_esc.remove() # keep/skip image according to the pressed key, 'escape' to break the loop if key_event.get('pressed') == 'right': skip_image = False break elif key_event.get('pressed') == 'left': skip_image = True break elif key_event.get('pressed') == 'escape': plt.close() raise StopIteration('User cancelled checking shoreline detection') else: plt.waitforbuttonpress() if skip_image: ax.clear() continue else: # create two new buttons add_button = plt.text(0, 0.9, 'add', size=16, ha="left", va="top", transform=plt.gca().transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) end_button = plt.text(1, 0.9, 'end', size=16, ha="right", va="top", transform=plt.gca().transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) # add multiple reference shorelines (until user clicks on <end> button) pts_sl = np.expand_dims(np.array([np.nan, np.nan]),axis=0) geoms = [] while 1: add_button.set_visible(False) end_button.set_visible(False) # update title (instructions) ax.set_title('Click points along the shoreline (enough points to capture the beach curvature).\n' + 'Start at one end of the beach.\n' + 'When finished digitizing, click <ENTER>', fontsize=14) plt.draw() # let user click on the shoreline pts = ginput(n=50000, timeout=1e9, show_clicks=True) pts_pix = np.array(pts) # convert pixel coordinates to world coordinates pts_world = SDS_tools.convert_pix2world(pts_pix[:,[1,0]], georef) # interpolate between points clicked by the user (1m resolution) pts_world_interp = np.expand_dims(np.array([np.nan, np.nan]),axis=0) for k in range(len(pts_world)-1): pt_dist = np.linalg.norm(pts_world[k,:]-pts_world[k+1,:]) xvals = np.arange(0,pt_dist) yvals = np.zeros(len(xvals)) pt_coords = np.zeros((len(xvals),2)) pt_coords[:,0] = xvals pt_coords[:,1] = yvals phi = 0 deltax = pts_world[k+1,0] - pts_world[k,0] deltay = pts_world[k+1,1] - pts_world[k,1] phi = np.pi/2 - np.math.atan2(deltax, deltay) tf = transform.EuclideanTransform(rotation=phi, translation=pts_world[k,:]) pts_world_interp = np.append(pts_world_interp,tf(pt_coords), axis=0) pts_world_interp = np.delete(pts_world_interp,0,axis=0) # save as geometry (to create .geojson file later) geoms.append(geometry.LineString(pts_world_interp)) # convert to pixel coordinates and plot pts_pix_interp = SDS_tools.convert_world2pix(pts_world_interp, georef) pts_sl = np.append(pts_sl, pts_world_interp, axis=0) ax.plot(pts_pix_interp[:,0], pts_pix_interp[:,1], 'r--') ax.plot(pts_pix_interp[0,0], pts_pix_interp[0,1],'ko') ax.plot(pts_pix_interp[-1,0], pts_pix_interp[-1,1],'ko') # update title and buttons add_button.set_visible(True) end_button.set_visible(True) ax.set_title('click on <add> to digitize another shoreline or on <end> to finish and save the shoreline(s)', fontsize=14) plt.draw() # let the user click again (<add> another shoreline or <end>) pt_input = ginput(n=1, timeout=1e9, show_clicks=False) pt_input = np.array(pt_input) # if user clicks on <end>, save the points and break the loop if pt_input[0][0] > im_ms.shape[1]/2: add_button.set_visible(False) end_button.set_visible(False) plt.title('Reference shoreline saved as ' + sitename + '_reference_shoreline.pkl and ' + sitename + '_reference_shoreline.geojson') plt.draw() ginput(n=1, timeout=3, show_clicks=False) plt.close() break pts_sl = np.delete(pts_sl,0,axis=0) # convert world image coordinates to user-defined coordinate system image_epsg = metadata[satname]['epsg'][i] pts_coords = SDS_tools.convert_epsg(pts_sl, image_epsg, settings['output_epsg']) # save the reference shoreline as .pkl filepath = os.path.join(filepath_data, sitename) with open(os.path.join(filepath, sitename + '_reference_shoreline.pkl'), 'wb') as f: pickle.dump(pts_coords, f) # also store as .geojson in case user wants to drag-and-drop on GIS for verification for k,line in enumerate(geoms): gdf = gpd.GeoDataFrame(geometry=gpd.GeoSeries(line)) gdf.index = [k] gdf.loc[k,'name'] = 'reference shoreline ' + str(k+1) # store into geodataframe if k == 0: gdf_all = gdf else: gdf_all = gdf_all.append(gdf) gdf_all.crs = {'init':'epsg:'+str(image_epsg)} # convert from image_epsg to user-defined coordinate system gdf_all = gdf_all.to_crs({'init': 'epsg:'+str(settings['output_epsg'])}) # save as geojson gdf_all.to_file(os.path.join(filepath, sitename + '_reference_shoreline.geojson'), driver='GeoJSON', encoding='utf-8') print('Reference shoreline has been saved in ' + filepath) break # check if a shoreline was digitised if len(pts_coords) == 0: raise Exception('No cloud free images are available to digitise the reference shoreline,'+ 'download more images and try again') return pts_coords
def save_jpg(metadata, settings, **kwargs): """ Saves a .jpg image for all the images contained in metadata. KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) 'cloud_thresh': float value between 0 and 1 indicating the maximum cloud fraction in the cropped image that is accepted 'cloud_mask_issue': boolean True if there is an issue with the cloud mask and sand pixels are erroneously being masked on the images Returns: ----------- Stores the images as .jpg in a folder named /preprocessed """ sitename = settings['inputs']['sitename'] cloud_thresh = settings['cloud_thresh'] filepath_data = settings['inputs']['filepath'] # create subfolder to store the jpg files filepath_jpg = os.path.join(filepath_data, sitename, 'jpg_files', 'preprocessed') if not os.path.exists(filepath_jpg): os.makedirs(filepath_jpg) # loop through satellite list for satname in metadata.keys(): filepath = SDS_tools.get_filepath(settings['inputs'], satname) filenames = metadata[satname]['filenames'] # loop through images for i in range(len(filenames)): if os.path.exists(filenames[i]): # image filename fn = SDS_tools.get_filenames(filenames[i], filepath, satname) # read and preprocess image im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = preprocess_single( fn, satname, settings['cloud_mask_issue']) # calculate cloud cover cloud_cover = np.divide( sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0] * cloud_mask.shape[1])) # skip image if cloud cover is above threshold if cloud_cover > cloud_thresh or cloud_cover == 1: continue # save .jpg with date and satellite in the title date = filenames[i][:19] plt.ioff() # turning interactive plotting off create_jpg(im_ms, cloud_mask, date, satname, filepath_jpg) # print the location where the images have been saved print('Satellite images saved as .jpg in ' + os.path.join(filepath_data, sitename, 'jpg_files', 'preprocessed'))
def merge_overlapping_images(metadata, inputs): """ When the area of interest is located at the boundary between 2 images, there will be overlap between the 2 images and both will be downloaded from Google Earth Engine. This function merges the 2 images, so that the area of interest is covered by only 1 image. KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded inputs: dict dictionnary that contains the following fields: 'sitename': str String containig the name of the site 'polygon': list polygon containing the lon/lat coordinates to be extracted, longitudes in the first column and latitudes in the second column, there are 5 pairs of lat/lon with the fifth point equal to the first point. e.g. [[[151.3, -33.7],[151.4, -33.7],[151.4, -33.8],[151.3, -33.8], [151.3, -33.7]]] 'dates': list of str list that contains 2 strings with the initial and final dates in format 'yyyy-mm-dd' e.g. ['1987-01-01', '2018-01-01'] 'sat_list': list of str list that contains the names of the satellite missions to include e.g. ['L5', 'L7', 'L8', 'S2'] 'filepath_data': str Filepath to the directory where the images are downloaded Returns: ----------- metadata_updated: dict updated metadata with the information of the merged images """ # only for Sentinel-2 at this stage (not sure if this is needed for Landsat images) sat = 'S2' filepath = os.path.join(inputs['filepath'], inputs['sitename']) # find the images that are overlapping (same date in S2 filenames) filenames = metadata[sat]['filenames'] filenames_copy = filenames.copy() # loop through all the filenames and find the pairs of overlapping images (same date and time of acquisition) pairs = [] for i, fn in enumerate(filenames): filenames_copy[i] = [] # find duplicate boolvec = [fn[:22] == _[:22] for _ in filenames_copy] if np.any(boolvec): idx_dup = np.where(boolvec)[0][0] if len(filenames[i]) > len(filenames[idx_dup]): pairs.append([idx_dup, i]) else: pairs.append([i, idx_dup]) # for each pair of images, merge them into one complete image for i, pair in enumerate(pairs): fn_im = [] for index in range(len(pair)): # read image fn_im.append([ os.path.join(filepath, 'S2', '10m', filenames[pair[index]]), os.path.join(filepath, 'S2', '20m', filenames[pair[index]].replace('10m', '20m')), os.path.join(filepath, 'S2', '60m', filenames[pair[index]].replace('10m', '60m')), os.path.join( filepath, 'S2', 'meta', filenames[pair[index]].replace('_10m', '').replace('.tif', '.txt')) ]) im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = SDS_preprocess.preprocess_single( fn_im[index], sat, False) # in Sentinel2 images close to the edge of the image there are some artefacts, # that are squares with constant pixel intensities. They need to be masked in the # raster (GEOTIFF). It can be done using the image standard deviation, which # indicates values close to 0 for the artefacts. # First mask the 10m bands if len(im_ms) > 0: im_std = SDS_tools.image_std(im_ms[:, :, 0], 1) im_binary = np.logical_or(im_std < 1e-6, np.isnan(im_std)) mask = morphology.dilation(im_binary, morphology.square(3)) for k in range(im_ms.shape[2]): im_ms[mask, k] = np.nan SDS_tools.mask_raster(fn_im[index][0], mask) # Then mask the 20m band im_std = SDS_tools.image_std(im_extra, 1) im_binary = np.logical_or(im_std < 1e-6, np.isnan(im_std)) mask = morphology.dilation(im_binary, morphology.square(3)) im_extra[mask] = np.nan SDS_tools.mask_raster(fn_im[index][1], mask) else: continue # make a figure for quality control # plt.figure() # plt.subplot(221) # plt.imshow(im_ms[:,:,[2,1,0]]) # plt.title('imRGB') # plt.subplot(222) # plt.imshow(im20, cmap='gray') # plt.title('im20') # plt.subplot(223) # plt.imshow(imQA, cmap='gray') # plt.title('imQA') # plt.subplot(224) # plt.title(fn_im[index][0][-30:]) # merge masked 10m bands fn_merged = os.path.join(os.getcwd(), 'merged.tif') gdal_merge.main( ['', '-o', fn_merged, '-n', '0', fn_im[0][0], fn_im[1][0]]) os.chmod(fn_im[0][0], 0o777) os.remove(fn_im[0][0]) os.chmod(fn_im[1][0], 0o777) os.remove(fn_im[1][0]) os.rename(fn_merged, fn_im[0][0]) # merge masked 20m band (SWIR band) fn_merged = os.path.join(os.getcwd(), 'merged.tif') gdal_merge.main( ['', '-o', fn_merged, '-n', '0', fn_im[0][1], fn_im[1][1]]) os.chmod(fn_im[0][1], 0o777) os.remove(fn_im[0][1]) os.chmod(fn_im[1][1], 0o777) os.remove(fn_im[1][1]) os.rename(fn_merged, fn_im[0][1]) # merge QA band (60m band) fn_merged = os.path.join(os.getcwd(), 'merged.tif') gdal_merge.main( ['', '-o', fn_merged, '-n', 'nan', fn_im[0][2], fn_im[1][2]]) os.chmod(fn_im[0][2], 0o777) os.remove(fn_im[0][2]) os.chmod(fn_im[1][2], 0o777) os.remove(fn_im[1][2]) os.rename(fn_merged, fn_im[0][2]) # remove the metadata .txt file of the duplicate image os.chmod(fn_im[1][3], 0o777) os.remove(fn_im[1][3]) print('%d pairs of overlapping Sentinel-2 images were merged' % len(pairs)) # update the metadata dict (delete all the duplicates) metadata_updated = copy.deepcopy(metadata) filenames_copy = metadata_updated[sat]['filenames'] index_list = [] for i in range(len(filenames_copy)): if filenames_copy[i].find('dup') == -1: index_list.append(i) for key in metadata_updated[sat].keys(): metadata_updated[sat][key] = [ metadata_updated[sat][key][_] for _ in index_list ] return metadata_updated
time[j, :] = [b.year, b.month, b.day, b.hour, b.minute, b.second] plots.plot_time_series(filepath_data, sitename, output, cross_distance) reference_elevation = ( 0 # elevation at which you would like the shoreline time-series to be ) plots.plot_shorelines_transects(filepath_data, sitename, output, transects) slope_est = dict([]) filepath = os.path.join(filepath_data, sitename, sitename + "_tides.csv") tide_data = pd.read_csv(filepath, parse_dates=["dates"]) dates_ts = [_.to_pydatetime() for _ in tide_data["dates"]] tides_ts = np.array(tide_data["tide"]) dates_sat = output["dates"] tides_sat = SDS_tools.get_closest_datapoint(dates_sat, dates_ts, tides_ts) for key in cross_distance.keys(): slope_est[key] = 0.1 cross_distance_tidally_corrected = {} for key in cross_distance.keys(): correction = (tides_sat - reference_elevation) / slope_est[key] cross_distance_tidally_corrected[key] = cross_distance[key] + correction out_dict = dict([]) out_dict["dates"] = dates_sat out_dict["geoaccuracy"] = output["geoaccuracy"] out_dict["satname"] = output["satname"] for key in cross_distance_tidally_corrected.keys(): out_dict["Transect " + str(key)] = cross_distance_tidally_corrected[key] df = pd.DataFrame(out_dict)
import matplotlib.pyplot as plt from matplotlib import gridspec plt.ion() import pandas as pd from datetime import datetime from coastsat import SDS_download, SDS_preprocess, SDS_shoreline, SDS_tools, SDS_transects # region of interest (longitude, latitude in WGS84) polygon = [[[151.301454, -33.700754], [151.311453, -33.702075], [151.307237, -33.739761], [151.294220, -33.736329], [151.301454, -33.700754]]] # can also be loaded from a .kml polygon # kml_polygon = os.path.join(os.getcwd(), 'examples', 'NARRA_polygon.kml') # polygon = SDS_tools.polygon_from_kml(kml_polygon) # convert polygon to a smallest rectangle (sides parallel to coordinate axes) polygon = SDS_tools.smallest_rectangle(polygon) # date range dates = ['2017-12-01', '2018-01-01'] # satellite missions sat_list = ['S2'] # name of the site sitename = 'NARRA' # filepath where data will be stored filepath_data = os.path.join(os.getcwd(), 'data') # put all the inputs into a dictionnary inputs = {
def set_openvsclosed(im_ms, inputs, jpg_out_path, cloud_mask, im_labels, georef, settings, date, satname, Xmin, Xmax, Ymin, Ymax): """ Shows the detected shoreline to the user for visual quality control. The user can select "keep" if the shoreline detection is correct or "skip" if it is incorrect. KV WRL 2018 Arguments: ----------- im_ms: np.array RGB + downsampled NIR and SWIR cloud_mask: np.array 2D cloud mask with True where cloud pixels are im_labels: np.array 3D image containing a boolean image for each class in the order (sand, swash, water) shoreline: np.array array of points with the X and Y coordinates of the shoreline image_epsg: int spatial reference system of the image from which the contours were extracted georef: np.array vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale] settings: dict contains the following fields: date: string date at which the image was taken satname: string indicates the satname (L5,L7,L8 or S2) Returns: ----------- skip_image: boolean True if the user wants to skip the image, False otherwise. """ keep_checking_inloop = 'True' im_mwi = SDS_tools.nd_index(im_ms[:, :, 4], im_ms[:, :, 1], cloud_mask) im_RGB = SDS_preprocess.rescale_image_intensity(im_ms[:, :, [2, 1, 0]], cloud_mask, 99.9) if plt.get_fignums(): # get open figure if it exists fig = plt.gcf() ax1 = fig.axes[0] ax2 = fig.axes[1] ax3 = fig.axes[2] else: # else create a new figure fig = plt.figure() fig.set_size_inches([12.53, 9.3]) mng = plt.get_current_fig_manager() mng.window.showMaximized() # according to the image shape, decide whether it is better to have the images # in vertical subplots or horizontal subplots if im_RGB.shape[1] > 2 * im_RGB.shape[0]: # vertical subplots gs = gridspec.GridSpec(3, 1) gs.update(bottom=0.03, top=0.97, left=0.03, right=0.97) ax1 = fig.add_subplot(gs[0, 0]) ax2 = fig.add_subplot(gs[1, 0]) ax3 = fig.add_subplot(gs[2, 0]) else: # horizontal subplots gs = gridspec.GridSpec(1, 3) gs.update(bottom=0.05, top=0.95, left=0.05, right=0.95) ax1 = fig.add_subplot(gs[0, 0]) ax2 = fig.add_subplot(gs[0, 1]) ax3 = fig.add_subplot(gs[0, 2]) # change the color of nans to either black (0.0) or white (1.0) or somewhere in between nan_color = 1.0 im_RGB = np.where(np.isnan(im_RGB), nan_color, im_RGB) # compute classified image im_class = np.copy(im_RGB) cmap = cm.get_cmap('tab20c') colorpalette = cmap(np.arange(0, 13, 1)) colours = np.zeros((3, 4)) colours[0, :] = colorpalette[5] colours[1, :] = np.array([204 / 255, 1, 1, 1]) colours[2, :] = np.array([0, 91 / 255, 1, 1]) for k in range(0, im_labels.shape[2]): im_class[im_labels[:, :, k], 0] = colours[k, 0] im_class[im_labels[:, :, k], 1] = colours[k, 1] im_class[im_labels[:, :, k], 2] = colours[k, 2] im_class = np.where(np.isnan(im_class), 1.0, im_class) # create image 1 (RGB) ax1.imshow(im_RGB) #ax1.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) ax1.axis('off') ax1.set_xlim(Xmin - 30, Xmax + 30) ax1.set_ylim(Ymax + 30, Ymin - 30) ax1.set_title(inputs['sitename'], fontweight='bold', fontsize=16) # create image 2 (classification) ax2.imshow(im_class) #ax2.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) ax2.axis('off') orange_patch = mpatches.Patch(color=colours[0, :], label='sand') white_patch = mpatches.Patch(color=colours[1, :], label='whitewater') blue_patch = mpatches.Patch(color=colours[2, :], label='water') #black_line = mlines.Line2D([],[],color='k',linestyle='-', label='shoreline') ax2.legend(handles=[orange_patch, white_patch, blue_patch], bbox_to_anchor=(1, 0.5), fontsize=10) ax2.set_xlim(Xmin - 30, Xmax + 30) ax2.set_ylim(Ymax + 30, Ymin - 30) ax2.set_title(date, fontweight='bold', fontsize=16) # create image 3 (MNDWI) ax3.imshow(im_mwi, cmap='bwr', vmin=-1, vmax=1) #ax3.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) ax3.axis('off') #plt.colorbar() ax3.set_xlim(Xmin - 30, Xmax + 30) ax3.set_ylim(Ymax + 30, Ymin - 30) ax3.set_title(satname + ' NDWI', fontweight='bold', fontsize=16) # if check_detection is True, let user manually accept/reject the images skip_image = False vis_open_vs_closed = 'NA' if settings['check_detection']: # set a key event to accept/reject the detections (see https://stackoverflow.com/a/15033071) # this variable needs to be immuatable so we can access it after the keypress event key_event = {} def press(event): # store what key was pressed in the dictionary key_event['pressed'] = event.key # let the user press a key, right arrow to keep the image, left arrow to skip it # to break the loop the user can press 'escape' while True: btn_open = plt.text(1.1, 0.95, 'open ⇨', size=12, ha="right", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) btn_closed = plt.text(-0.1, 0.95, '⇦ closed', size=12, ha="left", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) btn_skip = plt.text(0.5, 0.95, '⇧ skip', size=12, ha="center", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) btn_esc = plt.text(0.5, 0, 'esc', size=12, ha="center", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) plt.draw() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() # after button is pressed, remove the buttons btn_open.remove() btn_closed.remove() btn_skip.remove() btn_esc.remove() # keep/skip image according to the pressed key, 'escape' to break the loop if key_event.get('pressed') == 'right': skip_image = False vis_open_vs_closed = 'open' break elif key_event.get('pressed') == 'left': skip_image = False vis_open_vs_closed = 'closed' break elif key_event.get('pressed') == 'up': skip_image = True break elif key_event.get('pressed') == 'escape': plt.close() skip_image = True vis_open_vs_closed = 'exit on image' keep_checking_inloop = 'False' break #raise StopIteration('User cancelled checking shoreline detection') else: plt.waitforbuttonpress() # if save_figure is True, save a .jpg under /jpg_files/detection if settings['save_figure'] and not skip_image: fig.savefig(os.path.join(jpg_out_path, date + '_' + satname + '.jpg'), dpi=200) # Don't close the figure window, but remove all axes and settings, ready for next plot for ax in fig.axes: ax.clear() return vis_open_vs_closed, skip_image, keep_checking_inloop
def evaluate_classifier(classifier, metadata, settings): """ Apply the image classifier to all the images and save the classified images. KV WRL 2019 Arguments: ----------- classifier: joblib object classifier model to be used for image classification metadata: dict contains all the information about the satellite images that were downloaded settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) 'cloud_thresh': float value between 0 and 1 indicating the maximum cloud fraction in the cropped image that is accepted 'cloud_mask_issue': boolean True if there is an issue with the cloud mask and sand pixels are erroneously being masked on the images 'output_epsg': int output spatial reference system as EPSG code 'buffer_size': int size of the buffer (m) around the sandy pixels over which the pixels are considered in the thresholding algorithm 'min_beach_area': int minimum allowable object area (in metres^2) for the class 'sand', the area is converted to number of connected pixels 'min_length_sl': int minimum length (in metres) of shoreline contour to be valid Returns: ----------- Saves .jpg images with the output of the classification in the folder ./detection """ # create folder called evaluation fp = os.path.join(os.getcwd(), 'evaluation') if not os.path.exists(fp): os.makedirs(fp) # initialize figure (not interactive) plt.ioff() fig, ax = plt.subplots(1, 2, figsize=[17, 10], sharex=True, sharey=True, constrained_layout=True) # create colormap for labels cmap = cm.get_cmap('tab20c') colorpalette = cmap(np.arange(0, 13, 1)) colours = np.zeros((3, 4)) colours[0, :] = colorpalette[5] colours[1, :] = np.array([204 / 255, 1, 1, 1]) colours[2, :] = np.array([0, 91 / 255, 1, 1]) # loop through satellites for satname in metadata.keys(): filepath = SDS_tools.get_filepath(settings['inputs'], satname) filenames = metadata[satname]['filenames'] # load classifiers and if satname in ['L5', 'L7', 'L8']: pixel_size = 15 elif satname == 'S2': pixel_size = 10 # convert settings['min_beach_area'] and settings['buffer_size'] from metres to pixels buffer_size_pixels = np.ceil(settings['buffer_size'] / pixel_size) min_beach_area_pixels = np.ceil(settings['min_beach_area'] / pixel_size**2) # loop through images for i in range(len(filenames)): # image filename fn = SDS_tools.get_filenames(filenames[i], filepath, satname) # read and preprocess image im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = SDS_preprocess.preprocess_single( fn, satname, settings['cloud_mask_issue']) image_epsg = metadata[satname]['epsg'][i] # calculate cloud cover cloud_cover = np.divide( sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0] * cloud_mask.shape[1])) # skip image if cloud cover is above threshold if cloud_cover > settings['cloud_thresh']: continue # calculate a buffer around the reference shoreline (if any has been digitised) im_ref_buffer = SDS_shoreline.create_shoreline_buffer( cloud_mask.shape, georef, image_epsg, pixel_size, settings) # classify image in 4 classes (sand, whitewater, water, other) with NN classifier im_classif, im_labels = SDS_shoreline.classify_image_NN( im_ms, im_extra, cloud_mask, min_beach_area_pixels, classifier) # there are two options to map the contours: # if there are pixels in the 'sand' class --> use find_wl_contours2 (enhanced) # otherwise use find_wl_contours2 (traditional) try: # use try/except structure for long runs if sum(sum(im_labels[:, :, 0])) < 10: # compute MNDWI image (SWIR-G) im_mndwi = SDS_tools.nd_index(im_ms[:, :, 4], im_ms[:, :, 1], cloud_mask) # find water contours on MNDWI grayscale image contours_mwi = SDS_shoreline.find_wl_contours1( im_mndwi, cloud_mask, im_ref_buffer) else: # use classification to refine threshold and extract the sand/water interface contours_wi, contours_mwi = SDS_shoreline.find_wl_contours2( im_ms, im_labels, cloud_mask, buffer_size_pixels, im_ref_buffer) except: print('Could not map shoreline for this image: ' + filenames[i]) continue # process the water contours into a shoreline shoreline = SDS_shoreline.process_shoreline( contours_mwi, cloud_mask, georef, image_epsg, settings) try: sl_pix = SDS_tools.convert_world2pix( SDS_tools.convert_epsg(shoreline, settings['output_epsg'], image_epsg)[:, [0, 1]], georef) except: # if try fails, just add nan into the shoreline vector so the next parts can still run sl_pix = np.array([[np.nan, np.nan], [np.nan, np.nan]]) # make a plot im_RGB = SDS_preprocess.rescale_image_intensity( im_ms[:, :, [2, 1, 0]], cloud_mask, 99.9) # create classified image im_class = np.copy(im_RGB) for k in range(0, im_labels.shape[2]): im_class[im_labels[:, :, k], 0] = colours[k, 0] im_class[im_labels[:, :, k], 1] = colours[k, 1] im_class[im_labels[:, :, k], 2] = colours[k, 2] # show images ax[0].imshow(im_RGB) ax[1].imshow(im_RGB) ax[1].imshow(im_class, alpha=0.5) ax[0].axis('off') ax[1].axis('off') filename = filenames[i][:filenames[i].find('.')][:-4] ax[0].set_title(filename) ax[0].plot(sl_pix[:, 0], sl_pix[:, 1], 'k.', markersize=3) ax[1].plot(sl_pix[:, 0], sl_pix[:, 1], 'k.', markersize=3) # save figure fig.savefig(os.path.join( fp, settings['inputs']['sitename'] + filename[:19] + '.jpg'), dpi=150) # clear axes for cax in fig.axes: cax.clear() # close the figure at the end plt.close()
def create_training_data(metadata, settings): """ Function that lets user visually inspect satellite images and decide if entrance is open or closed. This can be done for the entire dataset or to a limited number of images, which will then be used to train the machine learning classifier for open vs. closed VH WRL 2020 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded settings: dict contains the following fields: sitename: str String containig the name of the site cloud_mask_issue: boolean True if there is an issue with the cloud mask and sand pixels are being masked on the images check_detection: boolean True to show each invidual satellite image and let the user decide if the entrance was open or closed Returns: ----------- output: dict contains the training data set for all inspected images """ sitename = settings['inputs']['sitename'] filepath_data = settings['inputs']['filepath'] print('Generating traning data for entrance state at: ' + sitename) print( 'Manually inspect each image to create training data. Press esc once a satisfactory number of images was inspected' ) #load shapefile that conta0ins specific shapes for each ICOLL site as per readme file Allsites = gpd.read_file( os.path.join(os.getcwd(), 'Sites', 'All_sites9.shp')) #.iloc[:,-4:] Site_shps = Allsites.loc[(Allsites.Sitename == sitename)] layers = Site_shps['layer'].values # initialise output data structure Training = {} # create a subfolder to store the .jpg images showing the detection + csv file of the generated training dataset csv_out_path = os.path.join( filepath_data, sitename, 'results_' + settings['inputs']['analysis_vrs']) if not os.path.exists(csv_out_path): os.makedirs(csv_out_path) jpg_out_path = os.path.join( filepath_data, sitename, 'jpg_files', 'classified_' + settings['inputs']['analysis_vrs']) if not os.path.exists(jpg_out_path): os.makedirs(jpg_out_path) # close all open figures plt.close('all') # loop through the user selecte satellites for satname in settings['inputs']['sat_list']: # get images filepath = SDS_tools.get_filepath(settings['inputs'], satname) filenames = metadata[satname]['filenames'] #randomize the time step to create a more independent training data set epsg_dict = dict(zip(filenames, metadata[satname]['epsg'])) if settings['shuffle_training_imgs'] == True: filenames = random.sample(filenames, len(filenames)) # load classifiers and if satname in ['L5', 'L7', 'L8']: pixel_size = 15 if settings['dark_sand']: clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_Landsat_dark.pkl')) elif settings['color_sand']: clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_Landsat_diff_col_beaches.pkl')) else: clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_Landsat_SEA.pkl')) elif satname == 'S2': pixel_size = 10 clf = joblib.load( os.path.join(os.getcwd(), 'classifiers', 'NN_4classes_S2_SEA.pkl')) # convert settings['min_beach_area'] and settings['buffer_size'] from metres to pixels min_beach_area_pixels = np.ceil(settings['min_beach_area'] / pixel_size**2) ########################################## #loop through all images and store results in pd DataFrame ########################################## plt.close() keep_checking = 'True' for i in range(len(filenames)): if keep_checking == 'True': print('\r%s: %d%%' % (satname, int(((i + 1) / len(filenames)) * 100)), end='') # get image filename fn = SDS_tools.get_filenames(filenames[i], filepath, satname) date = filenames[i][:19] # preprocess image (cloud mask + pansharpening/downsampling) im_ms, georef, cloud_mask, im_extra, imQA, im_nodata = SDS_preprocess.preprocess_single( fn, satname, settings['cloud_mask_issue']) # calculate cloud cover cloud_cover = np.divide( sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0] * cloud_mask.shape[1])) #skip image if cloud cover is above threshold if cloud_cover > settings[ 'cloud_thresh']: #####!!!!!##### Intermediate continue #load boundary shapefiles for each scene and reproject according to satellite image epsg shapes = SDS_tools.load_shapes_as_ndarrays_2( layers, Site_shps, satname, sitename, settings['shapefile_EPSG'], georef, metadata, epsg_dict[filenames[i]]) #get the min and max corner (in pixel coordinates) of the entrance area that will be used for plotting the data for visual inspection Xmin, Xmax, Ymin, Ymax = SDS_tools.get_bounding_box_minmax( shapes['entrance_bounding_box']) # classify image in 4 classes (sand, vegetation, water, other) with NN classifier im_classif, im_labels = classify_image_NN( im_ms, im_extra, cloud_mask, min_beach_area_pixels, clf) #Manually check entrance state to generate training data if settings['check_detection'] or settings['save_figure']: vis_open_vs_closed, skip_image, keep_checking = set_openvsclosed( im_ms, settings['inputs'], jpg_out_path, cloud_mask, im_labels, georef, settings, date, satname, Xmin, Xmax, Ymin, Ymax) #add results to intermediate list Training[date] = satname, vis_open_vs_closed, skip_image Training_df = pd.DataFrame(Training).transpose() Training_df.columns = ['satname', 'Entrance_state', 'skip image'] Training_df.to_csv( os.path.join(csv_out_path, sitename + '_visual_training_data.csv')) return Training_df
def label_images(metadata, settings): """ Load satellite images and interactively label different classes (hard-coded) KV WRL 2019 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded settings: dict with the following keys 'cloud_thresh': float value between 0 and 1 indicating the maximum cloud fraction in the cropped image that is accepted 'cloud_mask_issue': boolean True if there is an issue with the cloud mask and sand pixels are erroneously being masked on the images 'labels': dict list of label names (key) and label numbers (value) for each class 'flood_fill': boolean True to use the flood_fill functionality when labelling sand pixels 'tolerance': float tolerance value for flood fill when labelling the sand pixels 'filepath_train': str directory in which to save the labelled data 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) Returns: ----------- Stores the labelled data in the specified directory """ filepath_train = settings['filepath_train'] # initialize figure fig, ax = plt.subplots(1, 1, figsize=[17, 10], tight_layout=True, sharex=True, sharey=True) mng = plt.get_current_fig_manager() mng.window.showMaximized() # loop through satellites for satname in metadata.keys(): filepath = SDS_tools.get_filepath(settings['inputs'], satname) filenames = metadata[satname]['filenames'] # loop through images for i in range(len(filenames)): # image filename fn = SDS_tools.get_filenames(filenames[i], filepath, satname) # read and preprocess image im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = SDS_preprocess.preprocess_single( fn, satname, settings['cloud_mask_issue']) # calculate cloud cover cloud_cover = np.divide( sum(sum(cloud_mask.astype(int))), (cloud_mask.shape[0] * cloud_mask.shape[1])) # skip image if cloud cover is above threshold if cloud_cover > settings['cloud_thresh'] or cloud_cover == 1: continue # get individual RGB image im_RGB = SDS_preprocess.rescale_image_intensity( im_ms[:, :, [2, 1, 0]], cloud_mask, 99.9) im_NDVI = SDS_tools.nd_index(im_ms[:, :, 3], im_ms[:, :, 2], cloud_mask) im_NDWI = SDS_tools.nd_index(im_ms[:, :, 3], im_ms[:, :, 1], cloud_mask) # initialise labels im_viz = im_RGB.copy() im_labels = np.zeros([im_RGB.shape[0], im_RGB.shape[1]]) # show RGB image ax.axis('off') ax.imshow(im_RGB) implot = ax.imshow(im_viz, alpha=0.6) filename = filenames[i][:filenames[i].find('.')][:-4] ax.set_title(filename) ############################################################## # select image to label ############################################################## # set a key event to accept/reject the detections (see https://stackoverflow.com/a/15033071) # this variable needs to be immuatable so we can access it after the keypress event key_event = {} def press(event): # store what key was pressed in the dictionary key_event['pressed'] = event.key # let the user press a key, right arrow to keep the image, left arrow to skip it # to break the loop the user can press 'escape' while True: btn_keep = ax.text(1.1, 0.9, 'keep ⇨', size=12, ha="right", va="top", transform=ax.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) btn_skip = ax.text(-0.1, 0.9, '⇦ skip', size=12, ha="left", va="top", transform=ax.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) btn_esc = ax.text(0.5, 0, '<esc> to quit', size=12, ha="center", va="top", transform=ax.transAxes, bbox=dict(boxstyle="square", ec='k', fc='w')) fig.canvas.draw_idle() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() # after button is pressed, remove the buttons btn_skip.remove() btn_keep.remove() btn_esc.remove() # keep/skip image according to the pressed key, 'escape' to break the loop if key_event.get('pressed') == 'right': skip_image = False break elif key_event.get('pressed') == 'left': skip_image = True break elif key_event.get('pressed') == 'escape': plt.close() raise StopIteration('User cancelled labelling images') else: plt.waitforbuttonpress() # if user decided to skip show the next image if skip_image: ax.clear() continue # otherwise label this image else: ############################################################## # digitize sandy pixels ############################################################## ax.set_title( 'Click on SAND pixels (flood fill activated, tolerance = %.2f)\nwhen finished press <Enter>' % settings['tolerance']) # create erase button, if you click there it delets the last selection btn_erase = ax.text(im_ms.shape[1], 0, 'Erase', size=20, ha='right', va='top', bbox=dict(boxstyle="square", ec='k', fc='w')) fig.canvas.draw_idle() color_sand = settings['colors']['sand'] sand_pixels = [] while 1: seed = ginput(n=1, timeout=0, show_clicks=True) # if empty break the loop and go to next label if len(seed) == 0: break else: # round to pixel location seed = np.round(seed[0]).astype(int) # if user clicks on erase, delete the last selection if seed[0] > 0.95 * im_ms.shape[1] and seed[ 1] < 0.05 * im_ms.shape[0]: if len(sand_pixels) > 0: im_labels[sand_pixels[-1]] = 0 for k in range(im_viz.shape[2]): im_viz[sand_pixels[-1], k] = im_RGB[sand_pixels[-1], k] implot.set_data(im_viz) fig.canvas.draw_idle() del sand_pixels[-1] # otherwise label the selected sand pixels else: # flood fill the NDVI and the NDWI fill_NDVI = flood(im_NDVI, (seed[1], seed[0]), tolerance=settings['tolerance']) fill_NDWI = flood(im_NDWI, (seed[1], seed[0]), tolerance=settings['tolerance']) # compute the intersection of the two masks fill_sand = np.logical_and(fill_NDVI, fill_NDWI) im_labels[fill_sand] = settings['labels']['sand'] sand_pixels.append(fill_sand) # show the labelled pixels for k in range(im_viz.shape[2]): im_viz[im_labels == settings['labels']['sand'], k] = color_sand[k] implot.set_data(im_viz) fig.canvas.draw_idle() ############################################################## # digitize white-water pixels ############################################################## color_ww = settings['colors']['white-water'] ax.set_title( 'Click on individual WHITE-WATER pixels (no flood fill)\nwhen finished press <Enter>' ) fig.canvas.draw_idle() ww_pixels = [] while 1: seed = ginput(n=1, timeout=0, show_clicks=True) # if empty break the loop and go to next label if len(seed) == 0: break else: # round to pixel location seed = np.round(seed[0]).astype(int) # if user clicks on erase, delete the last labelled pixels if seed[0] > 0.95 * im_ms.shape[1] and seed[ 1] < 0.05 * im_ms.shape[0]: if len(ww_pixels) > 0: im_labels[ww_pixels[-1][1], ww_pixels[-1][0]] = 0 for k in range(im_viz.shape[2]): im_viz[ww_pixels[-1][1], ww_pixels[-1][0], k] = im_RGB[ww_pixels[-1][1], ww_pixels[-1][0], k] implot.set_data(im_viz) fig.canvas.draw_idle() del ww_pixels[-1] else: im_labels[seed[1], seed[0]] = settings['labels']['white-water'] for k in range(im_viz.shape[2]): im_viz[seed[1], seed[0], k] = color_ww[k] implot.set_data(im_viz) fig.canvas.draw_idle() ww_pixels.append(seed) im_sand_ww = im_viz.copy() btn_erase.set(text='<Esc> to Erase', fontsize=12) ############################################################## # digitize water pixels (with lassos) ############################################################## color_water = settings['colors']['water'] ax.set_title( 'Click and hold to draw lassos and select WATER pixels\nwhen finished press <Enter>' ) fig.canvas.draw_idle() selector_water = SelectFromImage(ax, implot, color_water) key_event = {} while True: fig.canvas.draw_idle() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() if key_event.get('pressed') == 'enter': selector_water.disconnect() break elif key_event.get('pressed') == 'escape': selector_water.array = im_sand_ww implot.set_data(selector_water.array) fig.canvas.draw_idle() selector_water.implot = implot selector_water.im_bool = np.zeros( (selector_water.array.shape[0], selector_water.array.shape[1])) selector_water.ind = [] # update im_viz and im_labels im_viz = selector_water.array selector_water.im_bool = selector_water.im_bool.astype(bool) im_labels[selector_water.im_bool] = settings['labels']['water'] im_sand_ww_water = im_viz.copy() ############################################################## # digitize land pixels (with lassos) ############################################################## color_land = settings['colors']['other land features'] ax.set_title( 'Click and hold to draw lassos and select OTHER LAND pixels\nwhen finished press <Enter>' ) fig.canvas.draw_idle() selector_land = SelectFromImage(ax, implot, color_land) key_event = {} while True: fig.canvas.draw_idle() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() if key_event.get('pressed') == 'enter': selector_land.disconnect() break elif key_event.get('pressed') == 'escape': selector_land.array = im_sand_ww_water implot.set_data(selector_land.array) fig.canvas.draw_idle() selector_land.implot = implot selector_land.im_bool = np.zeros( (selector_land.array.shape[0], selector_land.array.shape[1])) selector_land.ind = [] # update im_viz and im_labels im_viz = selector_land.array selector_land.im_bool = selector_land.im_bool.astype(bool) im_labels[selector_land. im_bool] = settings['labels']['other land features'] # save labelled image ax.set_title(filename) fig.canvas.draw_idle() fp = os.path.join(filepath_train, settings['inputs']['sitename']) if not os.path.exists(fp): os.makedirs(fp) fig.savefig(os.path.join(fp, filename + '.jpg'), dpi=150) ax.clear() # save labels and features features = dict([]) for key in settings['labels'].keys(): im_bool = im_labels == settings['labels'][key] features[key] = SDS_shoreline.calculate_features( im_ms, cloud_mask, im_bool) training_data = { 'labels': im_labels, 'features': features, 'label_ids': settings['labels'] } with open(os.path.join(fp, filename + '.pkl'), 'wb') as f: pickle.dump(training_data, f) # close figure when finished plt.close(fig)
def merge_overlapping_images(metadata, inputs): """ Merge simultaneous overlapping images that cover the area of interest. When the area of interest is located at the boundary between 2 images, there will be overlap between the 2 images and both will be downloaded from Google Earth Engine. This function merges the 2 images, so that the area of interest is covered by only 1 image. KV WRL 2018 Arguments: ----------- metadata: dict contains all the information about the satellite images that were downloaded inputs: dict with the following keys 'sitename': str name of the site 'polygon': list polygon containing the lon/lat coordinates to be extracted, longitudes in the first column and latitudes in the second column, there are 5 pairs of lat/lon with the fifth point equal to the first point: ``` polygon = [[[151.3, -33.7],[151.4, -33.7],[151.4, -33.8],[151.3, -33.8], [151.3, -33.7]]] ``` 'dates': list of str list that contains 2 strings with the initial and final dates in format 'yyyy-mm-dd': ``` dates = ['1987-01-01', '2018-01-01'] ``` 'sat_list': list of str list that contains the names of the satellite missions to include: ``` sat_list = ['L5', 'L7', 'L8', 'S2'] ``` 'filepath_data': str filepath to the directory where the images are downloaded Returns: ----------- metadata_updated: dict updated metadata """ # only for Sentinel-2 at this stage (not sure if this is needed for Landsat images) sat = 'S2' filepath = os.path.join(inputs['filepath'], inputs['sitename']) filenames = metadata[sat]['filenames'] # find the pairs of images that are within 5 minutes of each other time_delta = 5 * 60 # 5 minutes in seconds dates = metadata[sat]['dates'].copy() pairs = [] for i, date in enumerate(metadata[sat]['dates']): # dummy value so it does not match it again dates[i] = pytz.utc.localize(datetime(1, 1, 1) + timedelta(days=i + 1)) # calculate time difference time_diff = np.array( [np.abs((date - _).total_seconds()) for _ in dates]) # find the matching times and add to pairs list boolvec = time_diff <= time_delta if np.sum(boolvec) == 0: continue else: idx_dup = np.where(boolvec)[0][0] pairs.append([i, idx_dup]) # for each pair of image, create a mask and add no_data into the .tif file (this is needed before merging .tif files) for i, pair in enumerate(pairs): fn_im = [] for index in range(len(pair)): # get filenames of all the files corresponding to the each image in the pair fn_im.append([ os.path.join(filepath, 'S2', '10m', filenames[pair[index]]), os.path.join(filepath, 'S2', '20m', filenames[pair[index]].replace('10m', '20m')), os.path.join(filepath, 'S2', '60m', filenames[pair[index]].replace('10m', '60m')), os.path.join( filepath, 'S2', 'meta', filenames[pair[index]].replace('_10m', '').replace('.tif', '.txt')) ]) # read that image im_ms, georef, cloud_mask, im_extra, im_QA, im_nodata = SDS_preprocess.preprocess_single( fn_im[index], sat, False) # im_RGB = SDS_preprocess.rescale_image_intensity(im_ms[:,:,[2,1,0]], cloud_mask, 99.9) # in Sentinel2 images close to the edge of the image there are some artefacts, # that are squares with constant pixel intensities. They need to be masked in the # raster (GEOTIFF). It can be done using the image standard deviation, which # indicates values close to 0 for the artefacts. if len(im_ms) > 0: # calculate image std for the first 10m band im_std = SDS_tools.image_std(im_ms[:, :, 0], 1) # convert to binary im_binary = np.logical_or(im_std < 1e-6, np.isnan(im_std)) # dilate to fill the edges (which have high std) mask10 = morphology.dilation(im_binary, morphology.square(3)) # mask all 10m bands for k in range(im_ms.shape[2]): im_ms[mask10, k] = np.nan # mask the 10m .tif file (add no_data where mask is True) SDS_tools.mask_raster(fn_im[index][0], mask10) # create another mask for the 20m band (SWIR1) im_std = SDS_tools.image_std(im_extra, 1) im_binary = np.logical_or(im_std < 1e-6, np.isnan(im_std)) mask20 = morphology.dilation(im_binary, morphology.square(3)) im_extra[mask20] = np.nan # mask the 20m .tif file (im_extra) SDS_tools.mask_raster(fn_im[index][1], mask20) # use the 20m mask to create a mask for the 60m QA band (by resampling) mask60 = ndimage.zoom(mask20, zoom=1 / 3, order=0) mask60 = transform.resize(mask60, im_QA.shape, mode='constant', order=0, preserve_range=True) mask60 = mask60.astype(bool) # mask the 60m .tif file (im_QA) SDS_tools.mask_raster(fn_im[index][2], mask60) else: continue # make a figure for quality control # fig,ax= plt.subplots(2,2,tight_layout=True) # ax[0,0].imshow(im_RGB) # ax[0,0].set_title('RGB original') # ax[1,0].imshow(mask10) # ax[1,0].set_title('Mask 10m') # ax[0,1].imshow(mask20) # ax[0,1].set_title('Mask 20m') # ax[1,1].imshow(mask60) # ax[1,1].set_title('Mask 60 m') # once all the pairs of .tif files have been masked with no_data, merge the using gdal_merge fn_merged = os.path.join(filepath, 'merged.tif') # merge masked 10m bands and remove duplicate file gdal_merge.main( ['', '-o', fn_merged, '-n', '0', fn_im[0][0], fn_im[1][0]]) os.chmod(fn_im[0][0], 0o777) os.remove(fn_im[0][0]) os.chmod(fn_im[1][0], 0o777) os.remove(fn_im[1][0]) os.chmod(fn_merged, 0o777) os.rename(fn_merged, fn_im[0][0]) # merge masked 20m band (SWIR band) gdal_merge.main( ['', '-o', fn_merged, '-n', '0', fn_im[0][1], fn_im[1][1]]) os.chmod(fn_im[0][1], 0o777) os.remove(fn_im[0][1]) os.chmod(fn_im[1][1], 0o777) os.remove(fn_im[1][1]) os.chmod(fn_merged, 0o777) os.rename(fn_merged, fn_im[0][1]) # merge QA band (60m band) gdal_merge.main( ['', '-o', fn_merged, '-n', '0', fn_im[0][2], fn_im[1][2]]) os.chmod(fn_im[0][2], 0o777) os.remove(fn_im[0][2]) os.chmod(fn_im[1][2], 0o777) os.remove(fn_im[1][2]) os.chmod(fn_merged, 0o777) os.rename(fn_merged, fn_im[0][2]) # remove the metadata .txt file of the duplicate image os.chmod(fn_im[1][3], 0o777) os.remove(fn_im[1][3]) print('%d pairs of overlapping Sentinel-2 images were merged' % len(pairs)) # update the metadata dict metadata_updated = copy.deepcopy(metadata) idx_removed = [] idx_kept = [] for pair in pairs: idx_removed.append(pair[1]) for idx in np.arange(0, len(metadata[sat]['dates'])): if not idx in idx_removed: idx_kept.append(idx) for key in metadata_updated[sat].keys(): metadata_updated[sat][key] = [ metadata_updated[sat][key][_] for _ in idx_kept ] return metadata_updated
def process_shoreline(contours, cloud_mask, georef, image_epsg, settings): """ Converts the contours from image coordinates to world coordinates. This function also removes the contours that are too small to be a shoreline (based on the parameter settings['min_length_sl']) KV WRL 2018 Arguments: ----------- contours: np.array or list of np.array image contours as detected by the function find_contours cloud_mask: np.array 2D cloud mask with True where cloud pixels are georef: np.array vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale] image_epsg: int spatial reference system of the image from which the contours were extracted settings: dict with the following keys 'output_epsg': int output spatial reference system 'min_length_sl': float minimum length of shoreline contour to be kept (in meters) Returns: ----------- shoreline: np.array array of points with the X and Y coordinates of the shoreline """ # convert pixel coordinates to world coordinates contours_world = SDS_tools.convert_pix2world(contours, georef) # convert world coordinates to desired spatial reference system contours_epsg = SDS_tools.convert_epsg(contours_world, image_epsg, settings['output_epsg']) # remove contours that have a perimeter < min_length_sl (provided in settings dict) # this enables to remove the very small contours that do not correspond to the shoreline contours_long = [] for l, wl in enumerate(contours_epsg): coords = [(wl[k,0], wl[k,1]) for k in range(len(wl))] a = LineString(coords) # shapely LineString structure if a.length >= settings['min_length_sl']: contours_long.append(wl) # format points into np.array x_points = np.array([]) y_points = np.array([]) for k in range(len(contours_long)): x_points = np.append(x_points,contours_long[k][:,0]) y_points = np.append(y_points,contours_long[k][:,1]) contours_array = np.transpose(np.array([x_points,y_points])) shoreline = contours_array # now remove any shoreline points that are attached to cloud pixels if sum(sum(cloud_mask)) > 0: # get the coordinates of the cloud pixels idx_cloud = np.where(cloud_mask) idx_cloud = np.array([(idx_cloud[0][k], idx_cloud[1][k]) for k in range(len(idx_cloud[0]))]) # convert to world coordinates and same epsg as the shoreline points coords_cloud = SDS_tools.convert_epsg(SDS_tools.convert_pix2world(idx_cloud, georef), image_epsg, settings['output_epsg'])[:,:-1] # only keep the shoreline points that are at least 30m from any cloud pixel idx_keep = np.ones(len(shoreline)).astype(bool) for k in range(len(shoreline)): if np.any(np.linalg.norm(shoreline[k,:] - coords_cloud, axis=1) < 30): idx_keep[k] = False shoreline = shoreline[idx_keep] return shoreline
def calculate_features(im_ms, cloud_mask, im_bool): """ Calculates features on the image that are used for the supervised classification. The features include spectral normalized-difference indices and standard deviation of the image for all the bands and indices. KV WRL 2018 Arguments: ----------- im_ms: np.array RGB + downsampled NIR and SWIR cloud_mask: np.array 2D cloud mask with True where cloud pixels are im_bool: np.array 2D array of boolean indicating where on the image to calculate the features Returns: ----------- features: np.array matrix containing each feature (columns) calculated for all the pixels (rows) indicated in im_bool """ # add all the multispectral bands features = np.expand_dims(im_ms[im_bool,0],axis=1) for k in range(1,im_ms.shape[2]): feature = np.expand_dims(im_ms[im_bool,k],axis=1) features = np.append(features, feature, axis=-1) # NIR-G im_NIRG = SDS_tools.nd_index(im_ms[:,:,3], im_ms[:,:,1], cloud_mask) features = np.append(features, np.expand_dims(im_NIRG[im_bool],axis=1), axis=-1) # SWIR-G im_SWIRG = SDS_tools.nd_index(im_ms[:,:,4], im_ms[:,:,1], cloud_mask) features = np.append(features, np.expand_dims(im_SWIRG[im_bool],axis=1), axis=-1) # NIR-R im_NIRR = SDS_tools.nd_index(im_ms[:,:,3], im_ms[:,:,2], cloud_mask) features = np.append(features, np.expand_dims(im_NIRR[im_bool],axis=1), axis=-1) # SWIR-NIR im_SWIRNIR = SDS_tools.nd_index(im_ms[:,:,4], im_ms[:,:,3], cloud_mask) features = np.append(features, np.expand_dims(im_SWIRNIR[im_bool],axis=1), axis=-1) # B-R im_BR = SDS_tools.nd_index(im_ms[:,:,0], im_ms[:,:,2], cloud_mask) features = np.append(features, np.expand_dims(im_BR[im_bool],axis=1), axis=-1) # calculate standard deviation of individual bands for k in range(im_ms.shape[2]): im_std = SDS_tools.image_std(im_ms[:,:,k], 1) features = np.append(features, np.expand_dims(im_std[im_bool],axis=1), axis=-1) # calculate standard deviation of the spectral indices im_std = SDS_tools.image_std(im_NIRG, 1) features = np.append(features, np.expand_dims(im_std[im_bool],axis=1), axis=-1) im_std = SDS_tools.image_std(im_SWIRG, 1) features = np.append(features, np.expand_dims(im_std[im_bool],axis=1), axis=-1) im_std = SDS_tools.image_std(im_NIRR, 1) features = np.append(features, np.expand_dims(im_std[im_bool],axis=1), axis=-1) im_std = SDS_tools.image_std(im_SWIRNIR, 1) features = np.append(features, np.expand_dims(im_std[im_bool],axis=1), axis=-1) im_std = SDS_tools.image_std(im_BR, 1) features = np.append(features, np.expand_dims(im_std[im_bool],axis=1), axis=-1) return features
def show_detection(im_ms, cloud_mask, im_labels, shoreline,image_epsg, georef, settings, date, satname): """ Shows the detected shoreline to the user for visual quality control. The user can accept/reject the detected shorelines by using keep/skip buttons. KV WRL 2018 Arguments: ----------- im_ms: np.array RGB + downsampled NIR and SWIR cloud_mask: np.array 2D cloud mask with True where cloud pixels are im_labels: np.array 3D image containing a boolean image for each class in the order (sand, swash, water) shoreline: np.array array of points with the X and Y coordinates of the shoreline image_epsg: int spatial reference system of the image from which the contours were extracted georef: np.array vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale] date: string date at which the image was taken satname: string indicates the satname (L5,L7,L8 or S2) settings: dict with the following keys 'inputs': dict input parameters (sitename, filepath, polygon, dates, sat_list) 'output_epsg': int output spatial reference system as EPSG code 'check_detection': bool if True, lets user manually accept/reject the mapped shorelines 'save_figure': bool if True, saves a -jpg file for each mapped shoreline Returns: ----------- skip_image: boolean True if the user wants to skip the image, False otherwise """ sitename = settings['inputs']['sitename'] filepath_data = settings['inputs']['filepath'] # subfolder where the .jpg file is stored if the user accepts the shoreline detection filepath = os.path.join(filepath_data, sitename, 'jpg_files', 'detection') im_RGB = SDS_preprocess.rescale_image_intensity(im_ms[:,:,[2,1,0]], cloud_mask, 99.9) # compute classified image im_class = np.copy(im_RGB) cmap = cm.get_cmap('tab20c') colorpalette = cmap(np.arange(0,13,1)) colours = np.zeros((3,4)) colours[0,:] = colorpalette[5] colours[1,:] = np.array([204/255,1,1,1]) colours[2,:] = np.array([0,91/255,1,1]) for k in range(0,im_labels.shape[2]): im_class[im_labels[:,:,k],0] = colours[k,0] im_class[im_labels[:,:,k],1] = colours[k,1] im_class[im_labels[:,:,k],2] = colours[k,2] # compute MNDWI grayscale image im_mwi = SDS_tools.nd_index(im_ms[:,:,4], im_ms[:,:,1], cloud_mask) # transform world coordinates of shoreline into pixel coordinates # use try/except in case there are no coordinates to be transformed (shoreline = []) try: sl_pix = SDS_tools.convert_world2pix(SDS_tools.convert_epsg(shoreline, settings['output_epsg'], image_epsg)[:,[0,1]], georef) except: # if try fails, just add nan into the shoreline vector so the next parts can still run sl_pix = np.array([[np.nan, np.nan],[np.nan, np.nan]]) if plt.get_fignums(): # get open figure if it exists fig = plt.gcf() ax1 = fig.axes[0] ax2 = fig.axes[1] ax3 = fig.axes[2] else: # else create a new figure fig = plt.figure() fig.set_size_inches([18, 9]) mng = plt.get_current_fig_manager() mng.window.showMaximized() # according to the image shape, decide whether it is better to have the images # in vertical subplots or horizontal subplots if im_RGB.shape[1] > 2.5*im_RGB.shape[0]: # vertical subplots gs = gridspec.GridSpec(3, 1) gs.update(bottom=0.03, top=0.97, left=0.03, right=0.97) ax1 = fig.add_subplot(gs[0,0]) ax2 = fig.add_subplot(gs[1,0], sharex=ax1, sharey=ax1) ax3 = fig.add_subplot(gs[2,0], sharex=ax1, sharey=ax1) else: # horizontal subplots gs = gridspec.GridSpec(1, 3) gs.update(bottom=0.05, top=0.95, left=0.05, right=0.95) ax1 = fig.add_subplot(gs[0,0]) ax2 = fig.add_subplot(gs[0,1], sharex=ax1, sharey=ax1) ax3 = fig.add_subplot(gs[0,2], sharex=ax1, sharey=ax1) # change the color of nans to either black (0.0) or white (1.0) or somewhere in between nan_color = 1.0 im_RGB = np.where(np.isnan(im_RGB), nan_color, im_RGB) im_class = np.where(np.isnan(im_class), 1.0, im_class) # create image 1 (RGB) ax1.imshow(im_RGB) ax1.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) ax1.axis('off') ax1.set_title(sitename, fontweight='bold', fontsize=16) # create image 2 (classification) ax2.imshow(im_class) ax2.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) ax2.axis('off') orange_patch = mpatches.Patch(color=colours[0,:], label='sand') white_patch = mpatches.Patch(color=colours[1,:], label='whitewater') blue_patch = mpatches.Patch(color=colours[2,:], label='water') black_line = mlines.Line2D([],[],color='k',linestyle='-', label='shoreline') ax2.legend(handles=[orange_patch,white_patch,blue_patch, black_line], bbox_to_anchor=(1, 0.5), fontsize=10) ax2.set_title(date, fontweight='bold', fontsize=16) # create image 3 (MNDWI) ax3.imshow(im_mwi, cmap='bwr') ax3.plot(sl_pix[:,0], sl_pix[:,1], 'k.', markersize=3) ax3.axis('off') ax3.set_title(satname, fontweight='bold', fontsize=16) # additional options # ax1.set_anchor('W') # ax2.set_anchor('W') # cb = plt.colorbar() # cb.ax.tick_params(labelsize=10) # cb.set_label('MNDWI values') # ax3.set_anchor('W') # if check_detection is True, let user manually accept/reject the images skip_image = False if settings['check_detection']: # set a key event to accept/reject the detections (see https://stackoverflow.com/a/15033071) # this variable needs to be immuatable so we can access it after the keypress event key_event = {} def press(event): # store what key was pressed in the dictionary key_event['pressed'] = event.key # let the user press a key, right arrow to keep the image, left arrow to skip it # to break the loop the user can press 'escape' while True: btn_keep = plt.text(1.1, 0.9, 'keep ⇨', size=12, ha="right", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) btn_skip = plt.text(-0.1, 0.9, '⇦ skip', size=12, ha="left", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) btn_esc = plt.text(0.5, 0, '<esc> to quit', size=12, ha="center", va="top", transform=ax1.transAxes, bbox=dict(boxstyle="square", ec='k',fc='w')) plt.draw() fig.canvas.mpl_connect('key_press_event', press) plt.waitforbuttonpress() # after button is pressed, remove the buttons btn_skip.remove() btn_keep.remove() btn_esc.remove() # keep/skip image according to the pressed key, 'escape' to break the loop if key_event.get('pressed') == 'right': skip_image = False break elif key_event.get('pressed') == 'left': skip_image = True break elif key_event.get('pressed') == 'escape': plt.close() raise StopIteration('User cancelled checking shoreline detection') else: plt.waitforbuttonpress() # if save_figure is True, save a .jpg under /jpg_files/detection if settings['save_figure'] and not skip_image: fig.savefig(os.path.join(filepath, date + '_' + satname + '.jpg'), dpi=150) # don't close the figure window, but remove all axes and settings, ready for next plot for ax in fig.axes: ax.clear() return skip_image
def find_wl_contours2(im_ms, im_labels, cloud_mask, buffer_size, im_ref_buffer): """ New robust method for extracting shorelines. Incorporates the classification component to refine the treshold and make it specific to the sand/water interface. KV WRL 2018 Arguments: ----------- im_ms: np.array RGB + downsampled NIR and SWIR im_labels: np.array 3D image containing a boolean image for each class in the order (sand, swash, water) cloud_mask: np.array 2D cloud mask with True where cloud pixels are buffer_size: int size of the buffer around the sandy beach over which the pixels are considered in the thresholding algorithm. im_ref_buffer: np.array binary image containing a buffer around the reference shoreline Returns: ----------- contours_mwi: list of np.arrays contains the coordinates of the contour lines extracted from the MNDWI (Modified Normalized Difference Water Index) image t_mwi: float Otsu sand/water threshold used to map the contours """ nrows = cloud_mask.shape[0] ncols = cloud_mask.shape[1] # calculate Normalized Difference Modified Water Index (SWIR - G) im_mwi = SDS_tools.nd_index(im_ms[:,:,4], im_ms[:,:,1], cloud_mask) # calculate Normalized Difference Modified Water Index (NIR - G) im_wi = SDS_tools.nd_index(im_ms[:,:,3], im_ms[:,:,1], cloud_mask) # stack indices together im_ind = np.stack((im_wi, im_mwi), axis=-1) vec_ind = im_ind.reshape(nrows*ncols,2) # reshape labels into vectors vec_sand = im_labels[:,:,0].reshape(ncols*nrows) vec_water = im_labels[:,:,2].reshape(ncols*nrows) # create a buffer around the sandy beach se = morphology.disk(buffer_size) im_buffer = morphology.binary_dilation(im_labels[:,:,0], se) vec_buffer = im_buffer.reshape(nrows*ncols) # select water/sand/swash pixels that are within the buffer int_water = vec_ind[np.logical_and(vec_buffer,vec_water),:] int_sand = vec_ind[np.logical_and(vec_buffer,vec_sand),:] # make sure both classes have the same number of pixels before thresholding if len(int_water) > 0 and len(int_sand) > 0: if np.argmin([int_sand.shape[0],int_water.shape[0]]) == 1: int_sand = int_sand[np.random.choice(int_sand.shape[0],int_water.shape[0], replace=False),:] else: int_water = int_water[np.random.choice(int_water.shape[0],int_sand.shape[0], replace=False),:] # threshold the sand/water intensities int_all = np.append(int_water,int_sand, axis=0) t_mwi = filters.threshold_otsu(int_all[:,0]) t_wi = filters.threshold_otsu(int_all[:,1]) # find contour with MS algorithm im_wi_buffer = np.copy(im_wi) im_wi_buffer[~im_ref_buffer] = np.nan im_mwi_buffer = np.copy(im_mwi) im_mwi_buffer[~im_ref_buffer] = np.nan contours_wi = measure.find_contours(im_wi_buffer, t_wi) contours_mwi = measure.find_contours(im_mwi_buffer, t_mwi) # remove contour points that are NaNs (around clouds) contours_wi = process_contours(contours_wi) contours_mwi = process_contours(contours_mwi) # only return MNDWI contours and threshold return contours_mwi, t_mwi
def create_shoreline_buffer(im_shape, georef, image_epsg, pixel_size, settings): """ Creates a buffer around the reference shoreline. The size of the buffer is given by settings['max_dist_ref']. KV WRL 2018 Arguments: ----------- im_shape: np.array size of the image (rows,columns) georef: np.array vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale] image_epsg: int spatial reference system of the image from which the contours were extracted pixel_size: int size of the pixel in metres (15 for Landsat, 10 for Sentinel-2) settings: dict with the following keys 'output_epsg': int output spatial reference system 'reference_shoreline': np.array coordinates of the reference shoreline 'max_dist_ref': int maximum distance from the reference shoreline in metres Returns: ----------- im_buffer: np.array binary image, True where the buffer is, False otherwise """ # initialise the image buffer im_buffer = np.ones(im_shape).astype(bool) if 'reference_shoreline' in settings.keys(): # convert reference shoreline to pixel coordinates ref_sl = settings['reference_shoreline'] ref_sl_conv = SDS_tools.convert_epsg(ref_sl, settings['output_epsg'], image_epsg)[:, :-1] ref_sl_pix = SDS_tools.convert_world2pix(ref_sl_conv, georef) ref_sl_pix_rounded = np.round(ref_sl_pix).astype(int) # make sure that the pixel coordinates of the reference shoreline are inside the image idx_row = np.logical_and(ref_sl_pix_rounded[:, 0] > 0, ref_sl_pix_rounded[:, 0] < im_shape[1]) idx_col = np.logical_and(ref_sl_pix_rounded[:, 1] > 0, ref_sl_pix_rounded[:, 1] < im_shape[0]) idx_inside = np.logical_and(idx_row, idx_col) ref_sl_pix_rounded = ref_sl_pix_rounded[idx_inside, :] # create binary image of the reference shoreline (1 where the shoreline is 0 otherwise) im_binary = np.zeros(im_shape) for j in range(len(ref_sl_pix_rounded)): im_binary[ref_sl_pix_rounded[j, 1], ref_sl_pix_rounded[j, 0]] = 1 im_binary = im_binary.astype(bool) # dilate the binary image to create a buffer around the reference shoreline max_dist_ref_pixels = np.ceil(settings['max_dist_ref'] / pixel_size) se = morphology.disk(max_dist_ref_pixels) im_buffer = morphology.binary_dilation(im_binary, se) return im_buffer