Sentinel-2 image clustering in python | by Wathela Hamed | Towards Data Science

Clustering or unsupervised classification is the process of grouping or aggregating the pixel values of an image into a certain number of natural classes (groups) based on statistical similarity. In this tutorial, we will be using the rasterio for sentinel-2 image manipulation and the power full scikit-learn python package for clustering in jupyter notebook.
Scikit-learn is a free software machine learning library for the Python programming language which contains various classification, regression and clustering algorithms. To install this package:
conda install -c anaconda scikit-learn
Rasterio is an open source python library that reads and writes raster datasets such as satellite imagery and terrain models in different formats like GEOTIFF and JP2.
conda install -c conda-forge rasterio
Algorithm: Scikit-learn has different algorithms for clustering, these algorithms can be directly imported form the cluster sub-library. K-Means is one of the most used algorithms in clustering for its simplicity. It basically divides n observations (pixels values in our case) into k clusters (number of classes predefined by user) where each observation belongs to the cluster with the nearest mean. This tutorial will be based on K-means but you can easily switch to try out the rest, the code will almost stay the same but you will only need to change the algorithm.
Sentinel-2 image of El-hasaheesa and Elmanagel localities in Gezira state/Sudan will be used in this tutorial for training and prediction respectively. This data and the full code of this tutorial can be found on github.
To kick this off will start by importing all the packages will need:
import rasterio as riofrom rasterio.plot import showfrom sklearn import clusterimport matplotlib.pyplot as pltimport numpy as np
Then will use rasterio to open the image and read in its meta data
# Open the image elhas_raster = rio.open("Elhasaheesa.tif")print(elhas_raster.meta)
Meta data give us information about the image such as the data type, size ( width, height), counts (number of bands) and coordinate reference system. To visualize this image nicely will need to adjust its contrast first by stretching it out, you can read more about image enhancement here.
# Read, enhance and show the imageelhas_arr = elhas_raster.read() # read the opened imagevmin, vmax = np.nanpercentile(elhas_arr, (5,95))  # 5-95% contrast stretch# show the enhanced imageplt.figure(figsize=[20,20])show(elhas_raster, cmap='gray', vmin=vmin, vmax=vmax)plt.show()
If we print the shape of this image we will see it has the order of (bands, height, width), this needs to be changed first to the shape order (height, width, bands). We will reform this shape by:
Creating an empty array using our image size, counts and data type from the meta data.
Use a for loop to slice each band and reform it in our empty array.
At the end of this loop we will get a new array with the required shape order and has the same size and number of bands.
# create an empty array with same dimension and data typeimgxyb = np.empty((elhas_raster.height, elhas_raster.width, elhas_raster.count), elhas_raster.meta['dtype'])# loop through the raster's bands to fill the empty arrayfor band in range(imgxyb.shape[2]):    imgxyb[:,:,band] = elhas_raster.read(band+1)
Now we are almost ready to train our classifier, but first, we need to convert our X (width) and Y (height) dimensions to 1 dimension, so to have a 2d array instead of 3d, then we can feed it to K-means classifier.
# convert to 1d arrayimg1d=imgxyb[:,:,:3].reshape((imgxyb.shape[0]*imgxyb.shape[1],imgxyb.shape[2]))
Training: The most important parameter to set is the n_clusters which represents the number of clusters that we want to group our pixels into, in our image we can visually see that it has three main classes (built-up areas, bare lands and cultivated areas), but we have one additional class which is the black sides outside the region of interest which its pixels can also be clustered into a group. K-means has some others parameters such as the number of maximum number iterations and the number of initialization but we will leave them to the default for this tutorials.
cl = cluster.KMeans(n_clusters=4) # create an object of the classifierparam = cl.fit(img1d) # train itimg_cl = cl.labels_ # get the labels of the classesimg_cl = img_cl.reshape(imgxyb[:,:,0].shape) # reshape labels to a 3d array (one band only)
To show the resulting image, I would like to use a custom color map where I can control the color of each class. We will keep the outsides region as black and use red, green and yellow for the rest of the classes.
# Create a custom color map to represent our different 4 classescmap = mc.LinearSegmentedColormap.from_list("", ["black","red","green","yellow"])# Show the resulting array and save it as jpg imageplt.figure(figsize=[20,20])plt.imshow(img_cl, cmap=cmap)plt.axis('off')plt.savefig("elhas_clustered.jpg", bbox_inches='tight')plt.show()
Prediction: Once the classifier is trained, it can be used to predict the same learned clusters in any unseen image. We will use an image of Elmanagel locality (adjacent to Elhasaheesa) to predict the same 4 classes on it.
To use Elmanagel’s image for prediction, all the prepossessing that we have done to the previous image (reforming the shape and reshaping to a 2d array) need to be done before we can feed it to the classifier. Also the same previous steps are required to show the resulting predicted clusters.
# open the raster imageelmanagel = rio.open(‘elmanagel.tif’)# create an empty array with same dimensions and data type elman_xyb = np.empty((elmanagel.height, elmanagel.width,elmanagel.count), elmanagel.meta['dtype'])# loop through the raster bands and fill the empty array in x-y-bands orderfor band in range(elman_xyb.shape[2]):    elman_xyb[:,:,band] = elmanagel.read(band+1)# convert to 1d arrayelman_1d = elman_xyb[:,:,:3].reshape(elman_xyb.shape[0]*elman_xyb.shape[1], elman_xyb.shape[2])# predict the clusters in the image pred = cl.predict(elman_1d)# reshape the 1d array predictions to x-y-bands shape order (only one band)elman_cul = predelman_cul = elman_cul.reshape(elman_xyb[:,:,0].shape)
Now let’s show the original and the resulting predicted image in a subplot, but first, we will enhance the original image for better viewing through contrast stretch.
elman_arr = elmanagel.read() # Read the imagevmin, vmax = np.nanpercentile(elman_arr, (5,95)) # 5–95% contrast stretch# show the original and predicted imagefig, (ax1,ax2) = plt.subplots(figsize=[15,15], nrows=1,ncols=2, sharey=False,)show(elmanagel, cmap='gray', vmin=vmin, vmax=vmax, ax=ax1)show(elman_cul, cmap=cmap, ax=ax2)ax1.set_axis_off()ax2.set_axis_off()fig.savefig("pred.png", bbox_inches='tight')plt.show()