Professional Documents
Culture Documents
The approach is based on image amplitude data and is capable of detecting standing
surface water. Note that flooding under vegetation will not be detected by this
approach.
The Binder server will automatically shutdown when left idle for more than 10 minutes. Your
notebook edits will be lost when this happens. You will need to relaunch the binder to continue
working in a fresh copy of the notebook.
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 1/19
3/3/24, 10:13 SARHazards_Lab_Floods
notebook changes to it prior to shutting down the server or allowing it to time out.
1. Find relevant SAR data over your area of interest at the Alaska Satellite Facility's
SAR archive and Perform geometric and radiometric terrain correction using the
RTC processing flow by GAMMA Remote Sensing,
2. adaptive and automatic threshold calculation as discussed in the course,
3. initial flood detection by applying the calculated threshold image wide,
4. fuzzy-logic-based classification refinement,
5. final classification into permanent and flood waters using auxiliary data, and
6. dissemination of the results.
Step 1 of this workflow are operationally implemented in the Alaska Satellite Facility's
Hybrid Pluggable Processing Pipeline (HyP3) environment and accessible to the public at
the HyP3 Website. This notebook will focus on Steps 2 and 3
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 2/19
3/3/24, 10:13 SARHazards_Lab_Floods
In [ ]: import glob
import os
from typing import Tuple
import numpy as np
from osgeo import gdal
import pyproj
import pandas as pd
import rasterio
import skfuzzy
import ipywidgets as ui
%matplotlib widget
In [ ]: #def tile_image(image: np.ndarray, width: int = 200, height: int = 200) -> np.ndarr
def tile_image(image: np.ndarray, width, height) -> np.ndarray:
_nrows, _ncols = image.shape
_strides = image.strides
return np.lib.stride_tricks.as_strided(
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 3/19
3/3/24, 10:13 SARHazards_Lab_Floods
np.ravel(image),
shape=(nrows, ncols, height, width),
strides=(height * _strides[0], width * _strides[1], *_strides),
writeable=False
).reshape(nrows * ncols, height, width)
size = image.size
histogram = make_histogram(image)
nonzero_indices = np.nonzero(histogram)[0]
histogram = histogram[nonzero_indices]
histogram = histogram.flatten()
class_means = (
(np.arange(number_of_classes) + 1) * maximum /
(number_of_classes + 1)
)
class_variances = np.ones((number_of_classes)) * maximum
class_proportions = np.ones((number_of_classes)) * 1 / number_of_classes
sml = np.mean(np.diff(nonzero_indices)) / 1000
iteration = 0
while(True):
class_likelihood = make_distribution(
class_means, class_variances, class_proportions, nonzero_indices
)
sum_likelihood = np.sum(class_likelihood, 1) + np.finfo(
class_likelihood[0][0]).eps
log_likelihood = np.sum(histogram * np.log(sum_likelihood))
for j in range(0, number_of_classes):
class_posterior_probability = (
histogram * class_likelihood[:,j] / sum_likelihood
)
class_proportions[j] = np.sum(class_posterior_probability)
class_means[j] = (
np.sum(nonzero_indices * class_posterior_probability)
/ class_proportions[j]
)
vr = (nonzero_indices - class_means[j])
class_variances[j] = (
np.sum(vr *vr * class_posterior_probability)
/ class_proportions[j] +sml
)
del class_posterior_probability, vr
class_proportions = class_proportions + 1e-3
class_proportions = class_proportions / np.sum(class_proportions)
class_likelihood = make_distribution(
class_means, class_variances, class_proportions, nonzero_indices
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 4/19
3/3/24, 10:13 SARHazards_Lab_Floods
)
sum_likelihood = np.sum(class_likelihood, 1) + np.finfo(
class_likelihood[0,0]).eps
del class_likelihood
new_log_likelihood = np.sum(histogram * np.log(sum_likelihood))
del sum_likelihood
if((new_log_likelihood - log_likelihood) < 0.000001):
break
iteration = iteration + 1
del log_likelihood, new_log_likelihood
class_means = class_means + minimum - 1
s = image_copy.shape
posterior = np.zeros((s[0], s[1], number_of_classes))
posterior_lookup = dict()
for i in range(0, s[0]):
for j in range(0, s[1]):
pixel_val = image_copy2[i,j]
if pixel_val in posterior_lookup:
for n in range(0, number_of_classes):
posterior[i,j,n] = posterior_lookup[pixel_val][n]
else:
posterior_lookup.update({pixel_val: [0]*number_of_classes})
for n in range(0, number_of_classes):
x = make_distribution(
class_means[n], class_variances[n], class_proportions[n],
image_copy[i,j]
)
posterior[i,j,n] = x * class_proportions[n]
posterior_lookup[pixel_val][n] = posterior[i,j,n]
return posterior, class_means, class_variances, class_proportions
def make_histogram(image):
image = image.flatten()
indices = np.nonzero(np.isnan(image))
image[indices] = 0
indices = np.nonzero(np.isinf(image))
image[indices] = 0
del indices
size = image.size
maximum = int(np.ceil(np.amax(image)) + 1)
#maximum = (np.ceil(np.amax(image)) + 1)
histogram = np.zeros((1, maximum))
for i in range(0,size):
#floor_value = int(np.floor(image[i]))
floor_value = np.floor(image[i]).astype(np.uint8)
#floor_value = (np.floor(image[i]))
if floor_value > 0 and floor_value < maximum - 1:
temp1 = image[i] - floor_value
temp2 = 1 - temp1
histogram[0,floor_value] = histogram[0,floor_value] + temp1
histogram[0,floor_value - 1] = histogram[0,floor_value - 1] + temp2
histogram = np.convolve(histogram[0], [1,2,3,2,1])
histogram = histogram[2:(histogram.size - 3)]
histogram = histogram / np.sum(histogram)
return histogram
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 5/19
3/3/24, 10:13 SARHazards_Lab_Floods
def get_proj4(filename):
f=rasterio.open(filename)
return pyproj.Proj(f.crs, preserve_units=True) #used in pysheds
Write a function to calculate the number of rows and columns of tiles needed to tile
an image to a given size
In [ ]: def get_dates(paths):
dates = []
pths = glob.glob(paths)
for p in pths:
date = p.split('/')[-1].split("_")[0]
dates.append(date)
dates.sort()
return dates
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 6/19
3/3/24, 10:13 SARHazards_Lab_Floods
)
out_image.SetProjection(projection)
out_image.SetGeoTransform(geo_transform)
out_image.GetRasterBand(1).WriteArray(mask)
out_image.GetRasterBand(1).SetNoDataValue(0)
out_image.FlushCache()
This lab uses a subset of a Sentinel-1 SAR time series acquired near the city of Malda, on
the Indian Bangladesh border. This area experienced extensive flooding during the 2020
South Asia monsoon season. The time series covers June to August of 2020 and
combines ascending and descending RTC imagery into a joint and consistent time series
to monitoring this rapidly developing event.
In [ ]: name = "BangladeshFloodMapping_binder"
tiff_dir = path = f"/home/jovyan/{name}"
if not os.path.exists(tiff_dir):
os.mkdir(tiff_dir)
os.chdir(tiff_dir)
print(f"Current working directory: {os.getcwd()}")
time_series_path = f"s3://asf-jupyter-data-west/{name}.tar.gz"
time_series = os.path.basename(time_series_path)
!aws --no-sign-request --region us-west-2 s3 cp $time_series_path $time_series
Move into the parent directory of the directory containing the data and create a
directory in which to store the water masks
In [ ]: analysis_directory = tiff_dir
os.chdir(tiff_dir)
mask_directory = f'{analysis_directory}/Water_Masks'
if not os.path.exists(mask_directory):
os.mkdir(mask_directory)
print(f"Current working directory: {os.getcwd()}")
paths = f"{tiff_dir}/*_V*.tif*"
if os.path.exists(tiff_dir):
tiff_paths = get_tiff_paths(paths)
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 7/19
3/3/24, 10:13 SARHazards_Lab_Floods
Write a function to confirm the presence of both VV and VH images in all image sets
In [ ]: grouped_pths = group_polarizations(tiff_paths)
if not confirm_dual_polarizations(grouped_pths):
print("ERROR: AI_Water requires both VV and VH polarizations.")
else:
print("Confirmed presence of VV and VH polarities for each product.")
To create the HAND Exclusion Mask (HAND-EM) we then threshold the HAND layer by
assuming that pixels with a HAND value > 15 m are unlikely to be flooded. This means,
in other words, we assume that flood waters are unlikely to exceed a depth of 15 m.
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 8/19
3/3/24, 10:13 SARHazards_Lab_Floods
HAND_file="/home/jovyan/BangladeshFloodMapping_binder/Bangladesh_Training_DEM_hand.
print(f"Selected HAND: {HAND_file}")
try:
HAND_gT=gdal_get_geotransform(HAND_file)
except AttributeError:
raise NoHANDLayerException("Remember to select a HAND layer in the previous cel
HAND_proj4=get_proj4(HAND_file)
HAND=gdal_read(HAND_file)
hand = np.nan_to_num(HAND)
Now let's plot HAND and HAND-EM side-by-side. Dark blue regions in the HAND file
(left) area areas near drainage systems such as rivers. These areas are most likely to be
affected by floods. Areas in red, however, are at higher elevations and less likely to be
flooded.
The HAND-EM layer to the right shows pixels unlikely to contain flood waters in black.
These pixels are at least 15 m above the nearest drainage system.
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 9/19
3/3/24, 10:13 SARHazards_Lab_Floods
1. Firstly, the entire data are separated into quadratic non-overlapping parent tiles on
level $L^+$ with a size of $100 \times 100$ pixels. Each parent object is further
represented by four quadratic child objects on a second level $L^−$. The tile
selection process is based on statistical hierarchical relations between parent and
child objects.
2. A number of parent tiles is automatically selected which offer the highest (>95%
quantile) coefficient of variation on $L^+$ of the mean backscatter values of the
respective child objects on $L^−$. This criterion serves as a measure of the degree
of variation within the data and can therefore be used as an indicator of the
probability that the tiles are characterized by spatial inhomogeneity and contain
more than one semantic class. The selected parent objects should also have a
mean individual backscatter value lower than the mean of all parent tiles on $L^+$.
This ensures that tiles lying on the boundary between water and no water areas
are selected. In case that no tiles fulfil these criteria, the tile size on $L^+$ and $L^−
$ is halved and the quantile for the tile selection is reduced to 90% to guarantee a
successful tile selection also in data with a relatively low extent of water surfaces
or with smaller dispersed water bodies.
3. To improve the robustness of the automatic threshold derivation the approach
restricts the tile selection in Step (3) to only pixels situated in flood-prone regions
defined by a Height Above Nearest Drainage (HAND)-based binary Exclustion Mask
(HAND-EM). To create HAND-EM, a threshold is applied to HAND to identify non-
flood prone areas. A threshold value of $\geq 15m$ is proposed. The HAND-EM
further is shrunk by one pixel using an 8-neighbour function to account for
potential geometric inaccuracies between the exclude layer and SAR data. Tiles are
only considered in case less than 20% of its data pixels are excluded by HAND-EM.
4. Out of the number of the initially selected tiles, a limited number of N parent tiles
are finally chosen for threshold computation. This selection is accomplished by
ranking the parent tiles according to the standard deviation of the mean
backscatter values of the respective child objects. Tiles with the highest values are
chosen for $N$. Extensive testing yielded that $N = 5$ is a sufficient number of
parent tiles for threshold computation.
5. A multi-mode Expectation Maximization minimum error thresholding approach is
then employed to derive local threshold values using a cost function which is based
on the statistical parameterization of the sub-histograms of all selected tiles as bi-
modal Gaussian mixture distributions. In order to derive a global (i.e. scenebased)
threshold, the locally derived thresholds are combined by computing their
arithmetic mean.
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 10/19
3/3/24, 10:13 SARHazards_Lab_Floods
6. Using the dynamically calculated threshold, both the VV and VH scenes are
thresholded for water detection
7. The detected water maps are combined for arrive at an intial water mask that can
be further refined in post processing
Here some additional refinement steps that could be added to the notebook in the
future:
posterior_lookup = dict()
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 11/19
3/3/24, 10:13 SARHazards_Lab_Floods
original_shape = img_array.shape
n_rows, n_cols = get_tile_row_col_count(*original_shape, tile_size=tilesize
print(f'tiff: {tiff}')
if 'vv' in tiff or 'VV' in tiff:
vv_array = pad_image(f.ReadAsArray(), tilesize)
invalid_pixels = np.nonzero(vv_array == 0.0)
vv_tiles = tile_image(vv_array,width=tilesize,height=tilesize)
a = np.shape(vv_tiles)
vv_std = np.zeros(a[0])
vvt_masked = np.ma.masked_where(vv_tiles==0, vv_tiles)
vv_picktiles = np.zeros_like(vv_tiles)
for k in range(a[0]):
vv_subtiles = tile_image(vvt_masked[k,:,:],width=tilesize2,height=t
vvst_mean = np.ma.mean(vv_subtiles, axis=(1,2))
vvst_std = np.ma.std(vvst_mean)
vv_std[k] = np.ma.std(vvst_mean)
percentile2 = precentile
sort_index = 0
while np.size(sort_index) < 5:
threshold_index_vv = np.ma.min(np.where(y_vv>percentile2))
threshold_vv = x_vv[threshold_index_vv]
#sd_select_vv = np.nonzero(vv_std/vv_mean>threshold_vv)
s_select_vv = np.nonzero(vv_std/vv_mean>threshold_vv)
h_select_vv = np.nonzero(Hpercent > Hpick) # Includes
sd_select_vv = np.intersect1d(s_select_vv, h_select_vv)
# find tiles with mean values lower than the average mean
omean_vv = np.ma.median(vv_mean[h_select_vv])
mean_select_vv = np.nonzero(vv_mean<omean_vv)
# Intersect tiles with large std with tiles that have small means
msdselect_vv = np.intersect1d(sd_select_vv, mean_select_vv)
sort_index = np.flipud(np.argsort(vv_std[msdselect_vv]))
percentile2 = percentile2 - 0.01
finalselect_vv = sort_index[0:5]
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 12/19
3/3/24, 10:13 SARHazards_Lab_Floods
else:
vh_array = pad_image(f.ReadAsArray(), tilesize)
invalid_pixels = np.nonzero(vh_array == 0.0)
vh_tiles = tile_image(vh_array,width=tilesize,height=tilesize)
a = np.shape(vh_tiles)
vh_std = np.zeros(a[0])
vht_masked = np.ma.masked_where(vh_tiles==0, vh_tiles)
vh_picktiles = np.zeros_like(vh_tiles)
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 13/19
3/3/24, 10:13 SARHazards_Lab_Floods
for k in range(a[0]):
vh_subtiles = tile_image(vht_masked[k,:,:],width=tilesize2,height=t
vhst_mean = np.ma.mean(vh_subtiles, axis=(1,2))
vhst_std = np.ma.std(vhst_mean)
vh_std[k] = np.ma.std(vhst_mean)
percentile2 = precentile
sort_index = 0
while np.size(sort_index) < 5:
threshold_index_vh = np.ma.min(np.where(y_vh>percentile2))
threshold_vh = x_vh[threshold_index_vh]
#sd_select_vh = np.nonzero(vh_std/vh_mean>threshold_vh)
s_select_vh = np.nonzero(vh_std/vh_mean>threshold_vh)
h_select_vh = np.nonzero(Hpercent > Hpick) # Includes
sd_select_vh = np.intersect1d(s_select_vh, h_select_vh)
# find tiles with mean values lower than the average mean
omean_vh = np.ma.median(vh_mean[h_select_vh])
mean_select_vh = np.nonzero(vh_mean<omean_vh)
# Intersect tiles with large std with tiles that have small means
msdselect_vh = np.intersect1d(sd_select_vh, mean_select_vh)
sort_index = np.flipud(np.argsort(vh_std[msdselect_vh]))
percentile2 = percentile2 - 0.01
finalselect_vh = sort_index[0:5]
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 14/19
3/3/24, 10:13 SARHazards_Lab_Floods
# Create Maps (Pickfiles) that show which tiles were used for adaptive threshol
vv_picks = vv_picktiles.reshape((n_rows, n_cols, tilesize, tilesize)) \
.swapaxes(1, 2) \
.reshape(n_rows * tilesize, n_cols * tilesize) # yapf: disable
vh_picks = vh_picktiles.reshape((n_rows, n_cols, tilesize, tilesize)) \
.swapaxes(1, 2) \
.reshape(n_rows * tilesize, n_cols * tilesize) # yapf: disable
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 15/19
3/3/24, 10:13 SARHazards_Lab_Floods
In [ ]: temp_path = f"{tiff_dir}/*.tiff"
dates = get_dates(temp_path)
time_index = pd.DatetimeIndex(dates)
The following code cells allow you to visualize individual flood maps superimposed on
the respective SAR image they were derived from.
This next code cell first creates the information we are plotting below such as surface
water and SAR data stacks.
In [ ]: wpaths = f"{tiff_dir}/Water_Masks/*combined.tif*"
spaths = f"{tiff_dir}/*VV.tif*"
vrtcommand = f"gdalbuildvrt -separate Water.vrt {wpaths}"
!{vrtcommand}
water_file="Water.vrt"
vrtcommand = f"gdalbuildvrt -separate SAR.vrt {spaths}"
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 16/19
3/3/24, 10:13 SARHazards_Lab_Floods
!{vrtcommand}
SAR_file = "SAR.vrt"
img = gdal.Open(SAR_file)
wm = gdal.Open(water_file)
SARstack = img.ReadAsArray(5, 20, 5, 5)
SARsize = np.shape(SARstack)
SARbands = SARsize[0]
Please change the band_num setting in the next code cell to visualize flood mapping
results for different SAR image acquisition dates.
Note the tool bar in the bottom left corner of the image that's created. Feel free to use
the toolbar to zoom into the image and navigate around.
In [ ]: band_num = 7# Change the band number to visualize different SAR acquisitions and re
if band_num > SARbands:
band_num = SARbands
SARraster = img.GetRasterBand(band_num).ReadAsArray()
waterraster = wm.GetRasterBand(band_num).ReadAsArray()
water_masked = np.ma.masked_where(waterraster==0, waterraster)
waterraster = 0
plt.figure(figsize=(9, 6))
vmin = np.percentile(SARraster, 5) #vh_array
vmax = np.percentile(SARraster, 95)
plt.imshow(SARraster, cmap='gray', vmin=vmin, vmax=vmax)
plt.suptitle(f'Water Mask on SAR Image: {time_index.year[band_num*2-1]} / {time_ind
plt.grid()
plt.imshow(water_masked, cmap='Blues',vmin=0, vmax=1.2)
flood map. To do so, use the toolbar in the bottom left corner of the image above to
Zoom into the map and look at the details. Are there some pxiels that you
would have added to the mask?
If you think flood areas were missed, do you seen anything in the underlying
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 17/19
3/3/24, 10:13 SARHazards_Lab_Floods
In [ ]: rasterstack = wm.ReadAsArray()
srs = np.shape(rasterstack)
floodcount = np.sum(rasterstack,0)
floodpercent = floodcount / srs[0] * 100
dt = time_index.dayofyear[SARbands] - time_index.dayofyear[0]
flooddays = floodpercent / 100 * dt
rasterstack = 0
fd_masked = np.ma.masked_where(flooddays==0, flooddays)
In [ ]: SARraster = img.GetRasterBand(SARbands).ReadAsArray()
plt.figure(figsize=(9, 6))
vmin = np.percentile(SARraster, 5) #vh_array
vmax = np.percentile(SARraster, 95)
plt.suptitle('Number of Inundated Days Per Pixel - Minimum SAR Image as Background'
plt.imshow(SARraster, cmap='gray', vmin=vmin, vmax=vmax)
im = plt.imshow(fd_masked, cmap='jet')
plt.colorbar(im, orientation='vertical')
plt.grid()
outfile = f"{mask_directory}/flooddays.tif"
write_mask_to_file(flooddays, outfile, img.GetProjection(), img.GetGeoTransform())
whether or not they make sense to you. To do so, use the toolbar in the bottom left
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 18/19
3/3/24, 10:13 SARHazards_Lab_Floods
Version Log
SARHazards_Lab_Floods.ipynb - Version 1.0.4 - 11/03/2021
Recent Changes:
https://hub.ovh2.mybinder.org/user/asfbinderrecipe-_hazards_floods-ukosgrn6/doc/tree/SARHazards_Lab_Floods.ipynb 19/19