# Import modules
import rasterio
from rasterio.plot import show
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gpd
import numpy as np
from skimage.morphology import area_opening, binary_closing, disk
from skimage.measure import find_contours, label, regionprops_table
80 Find Kansas lakes
In this exercise we will use a cloud-free remote sensing image to extract, count, and inspect lakes in Kansas. You can use the same principles to delineate other features. This exercise is at teh intersection of image analysis and spatial analysis.
Data source:
Download GeoTiff image for the state of Kansas on 14-February-2022 from NASA Worldview Snapshots: https://wvs.earthdata.nasa.gov/
We will use the image from the MODIS Terra satellite with the following bands: - Band 7 = 2155 nm (Red channel) - Band 2 = 876 nm (Green channel) - Band 1 = 670 nm (Blue channel)
In this image:
- Vegetation appears bright green. Vegetation is very reflective in Band 2 (reason why it was assigned to the green channel)
- Liquid water: Black or dark blue
- Desert/Naturally bare soil: Sandy pink
- Burn scar: Red to reddish-brown
- Snow and ice are very reflective in band 7
- Ice and snow appear as bright turquoise
- Clouds appear white
# Read the GeoTiff file
= rasterio.open('../datasets/spatial/snapshot-2022-02-14.tiff') raster
Note
A Rasterio array order: (bands, rows, columns)
and Scikit-image, Pillow and Matplotlib follow: `(rows, columns, bands)
# Display the map
=(6,4))
plt.figure(figsize='MODIS Terra Bandas:7-2-1')
show(raster, title plt.show()
# Inspect some properties of the image
print('Data type:', raster.dtypes)
print('Shape of the image:', raster.shape)
print('Image width',raster.width)
print('Image height',raster.height)
print('Bands', raster.indexes)
print('Bounding box',raster.bounds)
print('Missing values', raster.nodatavals)
print('CRS', raster.crs)
# Show raster metadata
print(raster.meta)
Data type: ('uint8', 'uint8', 'uint8')
Shape of the image: (1465, 2733)
Image width 2733
Image height 1465
Bands (1, 2, 3)
Bounding box BoundingBox(left=-102.14937187828008, bottom=36.900982382252316, right=-94.47619487827995, top=40.12014738225228)
Missing values (None, None, None)
CRS EPSG:4326
{'driver': 'GTiff', 'dtype': 'uint8', 'nodata': None, 'width': 2733, 'height': 1465, 'count': 3, 'crs': CRS.from_epsg(4326), 'transform': Affine(0.0028076022685693805, 0.0, -102.14937187828008,
0.0, -0.0021973822525597, 40.12014738225228)}
# Learn how to convert specific rows and columns into lon and lat
=0, col=0) raster.xy(row
(-102.14796807714579, 40.119048691126)
# Plot the three bands
= plt.subplots(3,1, figsize=(6,8))
fig, (ax_red, ax_green, ax_blue) 1), ax=ax_red, cmap='Spectral', title='Red channel (Band 7)' )
show( (raster, 2), ax=ax_green, cmap='Spectral', title='Green channel (Band 2)' )
show( (raster, 3), ax=ax_blue, cmap='Spectral', title='Blue channel (Band 1)' )
show( (raster, =0.4)
fig.subplots_adjust(hspace plt.show()
# Inspect pixel values for a point on Milford Lake, KS
# Define point
= 39.100000
lat = -96.934662
lon
# Getpoint x (columns) and y (row) coordinates
= raster.index(lon, lat)
py,px
# Get R, G, and B values for the point
= raster.read(1)[py, px]
pix_red = raster.read(2)[py, px]
pix_green = raster.read(3)[py, px]
pix_blue print(pix_red, pix_green, pix_blue)
1 1 3
# Plot a small patch of land
# Syntax: rasterio.windows.Window(col_off, row_off, width, height) or
= rasterio.windows.Window(px-15, py-30, width=40, height=40)
window = raster.read(window=window)
window_data #window_data = raster.read(window=rasterio.windows.from_bounds(-96.93, 39.0, -96.83, 39.1, raster.transform))
# Get the transform for the window
= raster.window_transform(window)
window_transform
= plt.subplots(figsize=(5,5))
fig,ax ='x', color='r', s=100)
ax.scatter(lon, lat, marker=window_transform, ax=ax)
show(small_window, transform plt.show()
# Classify the lakes using thresholding of second band
= raster.read(2) < 25 BW
# Filter image
= area_opening(BW, area_threshold=100, connectivity=1)
BW = binary_closing(BW, disk(5)) BW
# Show classified boolean image
plt.figure()='binary_r')
plt.imshow(BW, cmap plt.show()
# Find percentage of the area covered by lakes
print('Area of the state covered by lakes:', BW.sum()/BW.size*100)
Area of the state covered by lakes: 0.2117964107002144
# Area of water bodies
= 0.25**2
pixel_area print('Total area with water bodies:',BW.sum()*pixel_area)
Total area with water bodies: 530.0
# Find lake contours
= find_contours(BW)
contours print('Detected:', len(contours), 'lakes')
Detected: 25 lakes
# Create a figure with the plot of each lake
=(16,16))
plt.figure(figsize
for k,contour in enumerate(contours):
6,5, k+1)
plt.subplot(f'Lake {k}')
plt.title(= raster.xy(contour[:,0], contour[:,1])
lon,lat
plt.plot(lon, lat)
plt.tight_layout()
plt.show()
# Create table with properties for each lake
= regionprops_table(label(BW), properties=('centroid','area'))
props = pd.DataFrame(props)
props 'area'] = props['area']*250*250/10_000
props[ props.head()
centroid-0 | centroid-1 | area | |
---|---|---|---|
0 | 24.872493 | 1019.070201 | 4362.50 |
1 | 100.059259 | 1455.014815 | 843.75 |
2 | 210.040909 | 1062.936364 | 1375.00 |
3 | 284.822262 | 1343.484740 | 3481.25 |
4 | 329.584507 | 960.176056 | 887.50 |
# Sort dataframe by area
='area', ascending=False, inplace=True)
props.sort_values(by=True, inplace=True)
props.reset_index(drop props.head()
centroid-0 | centroid-1 | area | |
---|---|---|---|
0 | 441.670270 | 1854.944595 | 4625.00 |
1 | 24.872493 | 1019.070201 | 4362.50 |
2 | 284.822262 | 1343.484740 | 3481.25 |
3 | 848.783002 | 2259.473779 | 3456.25 |
4 | 1073.158965 | 1536.297597 | 3381.25 |
# Get the centroid row and column and obtain the corresponding longitude and latitude
= raster.xy(props['centroid-0'].iloc[0], props['centroid-1'].iloc[0]) lon_largest, lat_largest
=(8,6))
plt.figure(figsize='x', color='r', s=100)
plt.scatter(lon_largest, lat_largest, marker='Terra MODIS 7-2-1 2019-09-05')
show(raster, title plt.show()
Save image with identified lakes
# Save resulting BW array as a raster map
= raster.profile
profile =1)
profile.update(countwith rasterio.open('lakes.tiff', 'w', **profile) as f:
1) f.write(BW,
# Read the lakes GeoTiff back into Python and display map (noticed that now the images has coordinates)
= rasterio.open('lakes.tiff')
lakes ='gray')
show(lakes, cmap plt.show()