From c31ae42fbd05b9735259b727e6c6a62479061631 Mon Sep 17 00:00:00 2001 From: kvos Date: Mon, 9 Apr 2018 16:34:54 +1000 Subject: [PATCH] implemented sand classification --- classify_sand.py | 181 +++++++++++++++++++++++++++++++++++++++++++++++ functions/sds.py | 73 +++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 classify_sand.py diff --git a/classify_sand.py b/classify_sand.py new file mode 100644 index 0000000..b5349be --- /dev/null +++ b/classify_sand.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +#==========================================================# +# Extract shorelines from Landsat images +#==========================================================# + +# Initial settings +import ee +import matplotlib.pyplot as plt +import matplotlib.cm as cm +import numpy as np +import pandas as pd +from datetime import datetime +import pickle +import pdb +import pytz + + +# image processing modules +import skimage.filters as filters +import skimage.exposure as exposure +import skimage.transform as transform +import sklearn.decomposition as decomposition +import skimage.morphology as morphology +import skimage.measure as measure + +# machine learning modules +from sklearn.cluster import KMeans + +# my modules +import functions.utils as utils +import functions.sds as sds + +# some settings +np.seterr(all='ignore') # raise/ignore divisions by 0 and nans +plt.rcParams['axes.grid'] = False +plt.rcParams['figure.max_open_warning'] = 100 +ee.Initialize() + +# parameters +cloud_thresh = 0.5 # threshold for cloud cover +plot_bool = True # if you want the plots +prob_high = 99.9 # upper probability to clip and rescale pixel intensity +min_contour_points = 100# minimum number of points contained in each water line +output_epsg = 28356 # GDA94 / MGA Zone 56 +buffer_size = 10 # radius (in pixels) of disk for buffer (pixel classification) +min_beach_size = 50 # number of pixels in a beach (pixel classification) + +# select collection +satname = 'L8' +input_col = ee.ImageCollection('LANDSAT/LC08/C01/T1_RT_TOA') # Landsat 8 Tier 1 TOA + +# location (Narrabeen-Collaroy beach) +polygon = [[[151.3473129272461,-33.69035274454718], + [151.2820816040039,-33.68206818063878], + [151.27281188964844,-33.74775138989556], + [151.3425064086914,-33.75231878701767], + [151.3473129272461,-33.69035274454718]]]; + +# dates +start_date = '2013-01-01' +end_date = '2018-12-31' + +# filter by location and date +flt_col = input_col.filterBounds(ee.Geometry.Polygon(polygon)).filterDate(start_date, end_date) + +n_img = flt_col.size().getInfo() +print('Number of images covering the polygon:', n_img) +im_all = flt_col.getInfo().get('features') + +i = 0 # first image + +# find image in ee database +im = ee.Image(im_all[i].get('id')) + +# load image as np.array +im_pan, im_ms, im_cloud, crs, meta = sds.read_eeimage(im, polygon, satname, plot_bool) + +# rescale intensities +im_ms = sds.rescale_image_intensity(im_ms, im_cloud, prob_high, plot_bool) +im_pan = sds.rescale_image_intensity(im_pan, im_cloud, prob_high, plot_bool) + +# pansharpen rgb image +im_ms_ps = sds.pansharpen(im_ms[:,:,[0,1,2]], im_pan, im_cloud, plot_bool) + +# add down-sized bands for NIR and SWIR (since pansharpening is not possible) +im_ms_ps = np.append(im_ms_ps, im_ms[:,:,[3,4]], axis=2) + +# calculate NDWI +im_ndwi = sds.nd_index(im_ms_ps[:,:,3], im_ms_ps[:,:,1], im_cloud, plot_bool) + +# edge detection +wl_pix = sds.find_wl_contours(im_ndwi, im_cloud, min_contour_points, plot_bool) + +# convert from pixels to world coordinates +wl_coords = sds.convert_pix2world(wl_pix, crs['crs_15m']) + +# convert to output epsg spatial reference +wl = sds.convert_epsg(wl_coords, crs['epsg_code'], output_epsg) + +# classify sand pixels +im_sand = sds.classify_sand_unsupervised(im_ms_ps, im_pan, im_cloud, wl_pix, buffer_size, min_beach_size, plot_bool) + +#%% +plt.figure() +plt.imshow(im_ms_ps[:,:,[2,1,0]]) +for i,contour in enumerate(wl_pix): plt.plot(contour[:, 1], contour[:, 0], linewidth=2) +plt.axis('image') +plt.title('Detected water lines') +plt.show() + +vec = im_ms_ps.reshape(im_ms_ps.shape[0] * im_ms_ps.shape[1], im_ms_ps.shape[2]) +vec_pan = im_pan.reshape(im_pan.shape[0]*im_pan.shape[1]) +features = np.zeros((len(vec), 5)) +features[:,[0,1,2,3]] = vec[:,[0,1,2,3]] +features[:,4] = vec_pan +vec_mask = im_cloud.reshape(im_ms_ps.shape[0] * im_ms_ps.shape[1]) + +# create buffer +im_buffer = np.zeros((im_ms_ps.shape[0], im_ms_ps.shape[1])) +for i, contour in enumerate(wl_pix): + indices = [(int(_[0]), int(_[1])) for _ in list(np.round(contour))] + for j, idx in enumerate(indices): + im_buffer[idx] = 1 + + +plt.figure() +plt.imshow(im_buffer) +plt.draw() + +se = morphology.disk(buffer_size) +im_buffer = morphology.binary_dilation(im_buffer, se) + +plt.figure() +plt.imshow(im_buffer) +plt.draw() + +vec_buffer = (im_buffer == 1).reshape(im_ms_ps.shape[0] * im_ms_ps.shape[1]) + +vec_buffer= np.logical_and(vec_buffer, ~vec_mask) + +#vec_buffer = np.ravel_multi_index(z,(im_ms_ps.shape[0], im_ms_ps.shape[1])) + + +kmeans = KMeans(n_clusters=6, random_state=0).fit(vec[vec_buffer,:]) +labels = kmeans.labels_ +labels_full = np.ones((len(vec_mask))) * np.nan +labels_full[vec_buffer] = labels +im_labels = labels_full.reshape(im_ms_ps.shape[0], im_ms_ps.shape[1]) + +plt.figure() +plt.imshow(im_labels) +plt.axis('equal') +plt.draw() + +utils.compare_images(im_labels, im_pan) + +plt.figure() +for i in range(6): plt.plot(kmeans.cluster_centers_[i,:]) +plt.draw() + +im_sand = im_labels == np.argmax(np.mean(kmeans.cluster_centers_[:,[0,1,2,4]], axis=1)) + +im_sand2 = morphology.remove_small_objects(im_sand, min_size=min_beach_size, connectivity=2) +im_sand3 = morphology.binary_dilation(im_sand2, morphology.disk(1)) +plt.figure() +plt.imshow(im_sand3) +plt.draw() + +im_ms_ps[im_sand3,0] = 0 +im_ms_ps[im_sand3,1] = 0 +im_ms_ps[im_sand3,2] = 1 + + +plt.figure() +plt.imshow(im_ms_ps[:,:,[2,1,0]]) +plt.axis('image') +plt.title('Sand classification') +plt.show() + +#%% diff --git a/functions/sds.py b/functions/sds.py index ec62f0e..e9b73fe 100644 --- a/functions/sds.py +++ b/functions/sds.py @@ -26,6 +26,7 @@ import skimage.transform as transform import sklearn.decomposition as decomposition import skimage.measure as measure import skimage.morphology as morphology +from sklearn.cluster import KMeans # import own modules @@ -605,3 +606,75 @@ def convert_epsg(points, epsg_in, epsg_out): raise return points_converted + +def classify_sand_unsupervised(im_ms_ps, im_pan, cloud_mask, wl_pix, buffer_size, min_beach_size, plot_bool): + """ + Classifies sand pixels using an unsupervised algorithm (Kmeans) + + KV WRL 2018 + + Arguments: + ----------- + im_ms_ps: np.ndarray + Pansharpened RGB + downsampled NIR and SWIR + im_pan: + Panchromatic band + cloud_mask: np.ndarray + 2D cloud mask with True where cloud pixels are + wl_pix: list of np.ndarray + list of arrays containig the pixel coordinates of the water line + buffer_size: int + radius of the disk used to create a buffer around the water line + min_beach_size: int + minimum number of connected pixels belonging to a single beach + plot_bool: boolean + True if plot is wanted + + Returns: ----------- + im_sand: np.ndarray + 2D binary image containing True where sand pixels are located + + """ + # reshape the 2D images into vectors + vec_ms_ps = im_ms_ps.reshape(im_ms_ps.shape[0] * im_ms_ps.shape[1], im_ms_ps.shape[2]) + vec_pan = im_pan.reshape(im_pan.shape[0]*im_pan.shape[1]) + vec_mask = cloud_mask.reshape(im_ms_ps.shape[0] * im_ms_ps.shape[1]) + # add B,G,R,NIR and pan bands to the vector of features + vec_features = np.zeros((vec_ms_ps.shape[0], 5)) + vec_features[:,[0,1,2,3]] = vec_ms_ps[:,[0,1,2,3]] + vec_features[:,4] = vec_pan + + # create binary image with ones where the detected water lines is + im_buffer = np.zeros((im_ms_ps.shape[0], im_ms_ps.shape[1])) + for i, contour in enumerate(wl_pix): + indices = [(int(_[0]), int(_[1])) for _ in list(np.round(contour))] + for j, idx in enumerate(indices): + im_buffer[idx] = 1 + # perform a dilation on the binary image + se = morphology.disk(buffer_size) + im_buffer = morphology.binary_dilation(im_buffer, se) + vec_buffer = (im_buffer == 1).reshape(im_ms_ps.shape[0] * im_ms_ps.shape[1]) + # add cloud mask to buffer + vec_buffer= np.logical_and(vec_buffer, ~vec_mask) + # perform kmeans (6 clusters) + kmeans = KMeans(n_clusters=6, random_state=0).fit(vec_features[vec_buffer,:]) + labels = np.ones((len(vec_mask))) * np.nan + labels[vec_buffer] = kmeans.labels_ + im_labels = labels.reshape(im_ms_ps.shape[0], im_ms_ps.shape[1]) + # find the class with maximum reflection in the B,G,R,Pan + im_sand = im_labels == np.argmax(np.mean(kmeans.cluster_centers_[:,[0,1,2,4]], axis=1)) + im_sand = morphology.remove_small_objects(im_sand, min_size=min_beach_size, connectivity=2) +# im_sand = morphology.binary_dilation(im_sand, morphology.disk(1)) + + if plot_bool: + im = np.copy(im_ms_ps) + im[im_sand,0] = 0 + im[im_sand,1] = 0 + im[im_sand,2] = 1 + plt.figure() + plt.imshow(im[:,:,[2,1,0]]) + plt.axis('image') + plt.title('Sand classification') + plt.show() + + return im_sand