├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── Remote Sensed Hyperspectral Image Classification with the Extended Morphological Profiles and Support Vector Machines.ipynb ├── extended-morphological-profiles.py ├── indianpines_dataset.mat ├── indianpines_gt.mat └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__ 3 | *.pyc 4 | *.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Sensed Hyperspectral Image Classification With The Extended Morphological Profiles and Support Vector Machines 2 | 3 | This is an example of how to use the Extended Morphological Profiles and Support Vector Machines to classify remote sensed hyperspectral images using Python. 4 | 5 | # Indian Pines Dataset 6 | 7 | This scene was gathered by [AVIRIS sensor](https://aviris.jpl.nasa.gov/) recorded over Northwestern Indiana, USA, and consists of 145x145 pixels and 224 spectral reflectance bands in the wavelength range 0.4–2.5 10^(-6) meters. The Indian Pines scene contains two-thirds agriculture, and one-third forest or other natural perennial vegetation. There are two major dual lane highways, a rail line, as well as some low density housing, other built structures, and smaller roads. The ground truth available is designated into sixteen classes (seventeen if you consider the background) and is not all mutually exclusive. It is also a very common practice reducing the number of bands to 200 by removing bands covering the region of water absorption: [104-108], [150-163], 220. Indian Pines data are available through [Pursue's univeristy MultiSpec site](https://engineering.purdue.edu/~biehl/MultiSpec/hyperspectral.html). 8 | 9 | # Extended Morphological Profiles (EMP) 10 | 11 | The Extended Morphological Profiles (EMP) is a simple and effective technique to encode both spectral and spatial information in the classification process. This method connects similar structures through morphological operations and keeps the essential spectral information by using some feature-extraction method such as Principal Component Analysis (PCA). 12 | 13 | # Classification: Support Vector Machines (SVM) 14 | 15 | In this example the Support Vector Machine (SVM) machine learning algorithm, with the Radial Basis Function (RBF) Kernel, was used for the classification. -------------------------------------------------------------------------------- /extended-morphological-profiles.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import scipy.io as io 4 | 5 | # Importing the dataset from Matlab format 6 | dataset = io.loadmat('indianpines_dataset.mat') 7 | number_of_bands = int(dataset['number_of_bands']) 8 | number_of_rows = int(dataset['number_of_rows']) 9 | number_of_columns = int(dataset['number_of_columns']) 10 | pixels = np.transpose(dataset['pixels']) 11 | 12 | # Importing the Groundtruth 13 | groundtruth = io.loadmat('indianpines_gt.mat') 14 | gt = np.transpose(groundtruth['pixels']) 15 | 16 | # Feature Scaling 17 | from sklearn.preprocessing import StandardScaler 18 | sc = StandardScaler() 19 | pixels = sc.fit_transform(pixels) 20 | 21 | # visualizing the indian pines dataset hyperspectral cube in RGB 22 | indianpines_colors = np.array([[255, 255, 255], 23 | [255, 254, 137], [3, 28, 241], [255, 89, 1], [5, 255, 133], 24 | [255, 2, 251], [89, 1, 255], [3, 171, 255], [12, 255, 7], 25 | [172, 175, 84], [160, 78, 158], [101, 173, 255], [60, 91, 112], 26 | [104, 192, 63], [139, 69, 46], [119, 255, 172], [254, 255, 3]]) 27 | import sklearn.preprocessing 28 | indianpines_colors = sklearn.preprocessing.minmax_scale(indianpines_colors, feature_range=(0, 1)) 29 | pixels_normalized = sklearn.preprocessing.minmax_scale(pixels, feature_range=(0, 1)) 30 | 31 | gt_thematic_map = np.zeros(shape=(number_of_rows, number_of_columns, 3)) 32 | rgb_hyperspectral_image = np.zeros(shape=(number_of_rows, number_of_columns, 3)) 33 | cont = 0 34 | for i in range(number_of_rows): 35 | for j in range(number_of_columns): 36 | rgb_hyperspectral_image[i, j, 0] = pixels_normalized[cont, 29] 37 | rgb_hyperspectral_image[i, j, 1] = pixels_normalized[cont, 42] 38 | rgb_hyperspectral_image[i, j, 2] = pixels_normalized[cont, 89] 39 | gt_thematic_map[i, j, :] = indianpines_colors[gt[cont, 0]] 40 | cont += 1 41 | 42 | indianpines_class_names = ['background', 43 | 'alfalfa', 'corn-notill', 'corn-min', 'corn', 44 | 'grass/pasture', 'grass/trees', 'grass/pasture-mowed', 'hay-windrowed', 45 | 'oats', 'soybeans-notill', 'soybeans-min', 'soybean-clean', 46 | 'wheat', 'woods', 'bldg-grass-tree-drives', 'stone-steel towers'] 47 | 48 | fig = plt.figure(figsize=(15, 15)) 49 | columns = 2 50 | rows = 1 51 | fig.add_subplot(rows, columns, 1) 52 | plt.imshow(gt_thematic_map) 53 | fig.add_subplot(rows, columns, 2) 54 | plt.imshow(rgb_hyperspectral_image) 55 | 56 | import matplotlib.patches as mpatches 57 | patches = [mpatches.Patch(color=indianpines_colors[i], label=indianpines_class_names[i]) for i in range(len(indianpines_colors))] 58 | plt.legend(handles=patches, loc=4, borderaxespad=0.) 59 | plt.show() 60 | 61 | # Preprocessing 62 | # Applying Principal Components Analysis (PCA) 63 | from sklearn.decomposition import PCA 64 | number_of_pc = 4 65 | pca = PCA(n_components=number_of_pc) 66 | pc = pca.fit_transform(pixels) 67 | 68 | # Visualizing PCs 69 | print( 70 | f"The accumulated explained variance for the {number_of_pc} principal components is {np.sum(pca.explained_variance_ratio_)}") 71 | print(f"Individual Explained Variance: {pca.explained_variance_ratio_}") 72 | 73 | fig = plt.figure(figsize=(15, 15)) 74 | columns = number_of_pc 75 | rows = 1 76 | pc_images = np.zeros(shape=(number_of_rows, number_of_columns, number_of_pc)) 77 | for i in range(number_of_pc): 78 | pc_images[:, :, i] = np.reshape(pc[:, i], (number_of_rows, number_of_columns)) 79 | fig.add_subplot(rows, columns, i+1) 80 | plt.imshow(pc_images[:, :, i], cmap='gray', interpolation='bicubic') 81 | 82 | plt.show() 83 | 84 | # Building the Extended Morphological Profiles (EMP) 85 | from skimage.morphology import reconstruction 86 | from skimage.morphology import erosion 87 | from skimage.morphology import disk 88 | from skimage import util 89 | 90 | def opening_by_reconstruction(image, se): 91 | """ 92 | Performs an Opening by Reconstruction. 93 | 94 | Parameters: 95 | image: 2D matrix. 96 | se: structuring element 97 | Returns: 98 | 2D matrix of the reconstructed image. 99 | """ 100 | eroded = erosion(image, se) 101 | reconstructed = reconstruction(eroded, image) 102 | return reconstructed 103 | 104 | 105 | def closing_by_reconstruction(image, se): 106 | """ 107 | Performs a Closing by Reconstruction. 108 | 109 | Parameters: 110 | image: 2D matrix. 111 | se: structuring element 112 | Returns: 113 | 2D matrix of the reconstructed image. 114 | """ 115 | obr = opening_by_reconstruction(image, se) 116 | 117 | obr_inverted = util.invert(obr) 118 | obr_inverted_eroded = erosion(obr_inverted, se) 119 | obr_inverted_eroded_rec = reconstruction( 120 | obr_inverted_eroded, obr_inverted) 121 | obr_inverted_eroded_rec_inverted = util.invert(obr_inverted_eroded_rec) 122 | return obr_inverted_eroded_rec_inverted 123 | 124 | 125 | def build_morphological_profiles(image, se_size=4, se_size_increment=2, num_openings_closings=4): 126 | """ 127 | Build the morphological profiles for a given image. 128 | 129 | Parameters: 130 | base_image: 2d matrix, it is the spectral information part of the MP. 131 | se_size: int, initial size of the structuring element (or kernel). Structuring Element used: disk 132 | se_size_increment: int, structuring element increment step 133 | num_openings_closings: int, number of openings and closings by reconstruction to perform. 134 | Returns: 135 | emp: 3d matrix with both spectral (from the base_image) and spatial information 136 | """ 137 | x, y = image.shape 138 | 139 | cbr = np.zeros(shape=(x, y, num_openings_closings)) 140 | obr = np.zeros(shape=(x, y, num_openings_closings)) 141 | 142 | it = 0 143 | tam = se_size 144 | while it < num_openings_closings: 145 | se = disk(tam) 146 | temp = closing_by_reconstruction(image, se) 147 | cbr[:, :, it] = temp[:, :] 148 | temp = opening_by_reconstruction(image, se) 149 | obr[:, :, it] = temp[:, :] 150 | tam += se_size_increment 151 | it += 1 152 | 153 | mp = np.zeros(shape=(x, y, (num_openings_closings*2)+1)) 154 | cont = num_openings_closings - 1 155 | for i in range(num_openings_closings): 156 | mp[:, :, i] = cbr[:, :, cont] 157 | cont = cont - 1 158 | 159 | mp[:, :, num_openings_closings] = image[:, :] 160 | 161 | cont = 0 162 | for i in range(num_openings_closings+1, num_openings_closings*2+1): 163 | mp[:, :, i] = obr[:, :, cont] 164 | cont += 1 165 | 166 | return mp 167 | 168 | 169 | def build_emp(base_image, se_size=4, se_size_increment=2, num_openings_closings=4): 170 | """ 171 | Build the extended morphological profiles for a given set of images. 172 | 173 | Parameters: 174 | base_image: 3d matrix, each 'channel' is considered for applying the morphological profile. It is the spectral information part of the EMP. 175 | se_size: int, initial size of the structuring element (or kernel). Structuring Element used: disk 176 | se_size_increment: int, structuring element increment step 177 | num_openings_closings: int, number of openings and closings by reconstruction to perform. 178 | Returns: 179 | emp: 3d matrix with both spectral (from the base_image) and spatial information 180 | """ 181 | base_image_rows, base_image_columns, base_image_channels = base_image.shape 182 | se_size = se_size 183 | se_size_increment = se_size_increment 184 | num_openings_closings = num_openings_closings 185 | morphological_profile_size = (num_openings_closings * 2) + 1 186 | emp_size = morphological_profile_size * base_image_channels 187 | emp = np.zeros( 188 | shape=(base_image_rows, base_image_columns, emp_size)) 189 | 190 | cont = 0 191 | for i in range(base_image_channels): 192 | # build MPs 193 | mp_temp = build_morphological_profiles( 194 | base_image[:, :, i], se_size, se_size_increment, num_openings_closings) 195 | 196 | aux = morphological_profile_size * (i+1) 197 | 198 | # build the EMP 199 | cont_aux = 0 200 | for k in range(cont, aux): 201 | emp[:, :, k] = mp_temp[:, :, cont_aux] 202 | cont_aux += 1 203 | 204 | cont = morphological_profile_size * (i+1) 205 | 206 | return emp 207 | 208 | 209 | pc_images.shape 210 | num_openings_closings = 4 211 | morphological_profile_size = (num_openings_closings * 2) + 1 212 | emp_image = build_emp(base_image=pc_images, num_openings_closings=num_openings_closings) 213 | 214 | # Visualizing the EMP 215 | fig = plt.figure(figsize=(15, 15)) 216 | columns = morphological_profile_size 217 | rows = number_of_pc 218 | print("Number of Base Images: "+str(rows)) 219 | print("Morphological Profiles size: "+str(columns)) 220 | print("EMP = "+str(emp_image.shape)) 221 | 222 | emp_size = morphological_profile_size * number_of_pc 223 | for i in range(1, emp_size+1): 224 | fig.add_subplot(rows, columns, i) 225 | plt.imshow(emp_image[:, :, i-1], cmap='gray', interpolation='bicubic') 226 | 227 | plt.show() 228 | 229 | 230 | # building dataset for classification 231 | dim_x, dim_y, dim_z = emp_image.shape 232 | dim = dim_x * dim_y 233 | 234 | x = np.zeros(shape=(dim, dim_z)) 235 | y = gt 236 | 237 | cont = 0 238 | for i in range(dim_x): 239 | for j in range(dim_y): 240 | x[cont, :] = emp_image[i, j, :] 241 | cont += 1 242 | 243 | # Splitting the dataset into the Training set and Test set 244 | from sklearn.model_selection import train_test_split 245 | x_train, x_test, y_train, y_test = train_test_split( 246 | x, y, test_size=0.75, random_state=0) 247 | 248 | # Fitting Kernel SVM to the Training set 249 | from sklearn.svm import SVC 250 | classifier = SVC(kernel='rbf', random_state=0) 251 | classifier.fit(x_train, y_train) 252 | 253 | # Predicting the Test set results 254 | y_pred = classifier.predict(x_test) 255 | 256 | # Making the Confusion Matrix 257 | from sklearn.metrics import confusion_matrix 258 | cm = confusion_matrix(y_test, y_pred) 259 | 260 | # Visualizing the results 261 | import itertools 262 | def plot_confusion_matrix(cm, classes, 263 | normalize=False, 264 | title='Confusion matrix', 265 | cmap=plt.cm.Blues): 266 | """ 267 | This function prints and plots the confusion matrix. 268 | Normalization can be applied by setting `normalize=True`. 269 | """ 270 | if normalize: 271 | cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis] 272 | 273 | plt.imshow(cm, interpolation='nearest', cmap=cmap) 274 | plt.title(title) 275 | plt.colorbar() 276 | tick_marks = np.arange(len(classes)) 277 | plt.xticks(tick_marks, classes, rotation=45) 278 | plt.yticks(tick_marks, classes) 279 | 280 | fmt = '.2f' if normalize else 'd' 281 | thresh = cm.max() / 2. 282 | for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): 283 | plt.text(j, i, format(cm[i, j], fmt), 284 | horizontalalignment="center", 285 | color="white" if cm[i, j] > thresh else "black") 286 | 287 | plt.tight_layout() 288 | plt.ylabel('True label') 289 | plt.xlabel('Predicted label') 290 | 291 | 292 | # Plot normalized confusion matrix 293 | plt.figure() 294 | plot_confusion_matrix(cm, classes=indianpines_class_names, normalize=True, title='Normalized confusion matrix') 295 | plt.show() 296 | 297 | 298 | # Visualizing the thematic map 299 | predicted_thematic_map = np.zeros(shape=(number_of_rows, number_of_columns, 3)) 300 | predicted_dataset = classifier.predict(x) 301 | 302 | cont = 0 303 | for i in range(number_of_rows): 304 | for j in range(number_of_columns): 305 | gt_thematic_map[i, j, :] = indianpines_colors[gt[cont, 0]] 306 | predicted_thematic_map[i, j, :] = indianpines_colors[predicted_dataset[cont]] 307 | cont += 1 308 | 309 | fig = plt.figure(figsize=(15, 15)) 310 | columns = 2 311 | rows = 1 312 | fig.add_subplot(rows, columns, 1) 313 | plt.imshow(gt_thematic_map) 314 | fig.add_subplot(rows, columns, 2) 315 | plt.imshow(predicted_thematic_map) 316 | plt.show() 317 | -------------------------------------------------------------------------------- /indianpines_dataset.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreybicalho/ExtendedMorphologicalProfiles/62af0ee1833df53bc5232ba2abb938abd242ffda/indianpines_dataset.mat -------------------------------------------------------------------------------- /indianpines_gt.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreybicalho/ExtendedMorphologicalProfiles/62af0ee1833df53bc5232ba2abb938abd242ffda/indianpines_gt.mat -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.17.2 2 | matplotlib==3.0.3 3 | scipy==1.3.1 4 | scikit-learn==0.21.3 5 | scikit-image==0.16.2 --------------------------------------------------------------------------------