I'm using Google Earth Engine Python API in Colab to carry out surface water delineation with Sentinel-1 data. The analysis uses Otsu thresholding with edge detection as described here: https://google-earth-engine.com/Aquatic-and-Hydrological-Applications/Surface-Water-Mapping/
The function I've written is intended to be mapped over an image collection; however, it returns dictionary reference errors when mapped. I've tested it on an individual image and it works with no issues. I'm guessing there is an issue with how dictionary values are being passed within the function, but I can't figure out where the error is coming from.
Here is a snippet of my code, illustrating the issue.
# Import libraries
import ee
import geemap
import os
import geemap.chart as chart
import matplotlib.pyplot as plt
import pandas as pd
import math
import numpy as np
# Import Sentinel-1 data as collection
roi = ee.Geometry.Rectangle(89.6206, 24.4842, 89.85, 23.769174)
# Start and end date definitions
startDate =ee.Date('2019-09-18')
endDate = ee.Date('2019-09-22')
# Data import, filter, and clip
s1Collection = (ee.ImageCollection('COPERNICUS/S1_GRD')
.filterBounds(roi)
.filterDate(startDate,endDate)
.filter(ee.Filter.eq('orbitProperties_pass','DESCENDING'))
.filter(ee.Filter.eq('instrumentMode','IW'))
.filter(ee.Filter.eq('resolution_meters',10))
.select(['VV', 'VH'])
.map(lambda image: image.clip(roi))
.map(lambda img: img.set('date', img.date().format('YYYY-MM-dd')))
)
# Otsu thresholding with edge detection.
# Adapted from: https://google-earth-engine.com/Aquatic-and-Hydrological-Applications/Surface-Water-Mapping/
# Adapted from: https://github.com/gee-community/geemap/blob/d8ba370caf712edec4e002653623fec930ec7312/examples/notebooks/otsu.ipynb
def otsu(histogram):
counts = ee.Array(ee.Dictionary(histogram).get('histogram'))
means = ee.Array(ee.Dictionary(histogram).get('bucketMeans'))
size = means.length().get([0])
total = counts.reduce(ee.Reducer.sum(), [0]).get([0])
sum = means.multiply(counts).reduce(ee.Reducer.sum(), [0]).get([0])
mean = sum.divide(total)
indices = ee.List.sequence(1, size)
# Compute between sum of squares, where each mean partitions the data.
def func_xxx(i):
aCounts = counts.slice(0, 0, i)
aCount = aCounts.reduce(ee.Reducer.sum(), [0]).get([0])
aMeans = means.slice(0, 0, i)
aMean = (
aMeans.multiply(aCounts)
.reduce(ee.Reducer.sum(), [0])
.get([0])
.divide(aCount)
)
bCount = total.subtract(aCount)
bMean = sum.subtract(aCount.multiply(aMean)).divide(bCount)
return aCount.multiply(aMean.subtract(mean).pow(2)).add(
bCount.multiply(bMean.subtract(mean).pow(2))
)
bss = indices.map(func_xxx)
# Return the mean value corresponding to the maximum BSS.
return means.sort(bss).get([-1])
def edgeOtsu(image):
# Define parameters for the adaptive thresholding.
# Band for analysis
band = 'VV'
# Initial estimate of water/no-water for estimating the edges
initialThreshold=-16
# Number of connected pixels to use for length calculation.
connectedPixels=100
# Length of edges to be considered water edges.
edgeLength=20
# Buffer in meters to apply to edges.
edgeBuffer=100
# Threshold for canny edge detection.
cannyThreshold=1
# Sigma value for gaussian filter in canny edge detection.
cannySigma=1
# Lower threshold for canny detection.
cannyLt=0.05
# Get preliminary water.
binary=(image
.select(band)
.lt(initialThreshold)
.rename('binary')
)
# Get projection information to convert buffer size to pixels.
imageProj=image.select(band).projection()
# Get canny edges.
canny=ee.Algorithms.CannyEdgeDetector(
**{
'image': binary,
'threshold': cannyThreshold,
'sigma': cannySigma
}
)
# Process canny edges:
# Get the edges and length of edges.
connected=canny.updateMask(canny).lt(cannyLt).connectedPixelCount(connectedPixels,True)
# Mask short edges that can be noise.
edges=connected.gte(edgeLength)
# Calculate the buffer in pixel size.
edgeBufferPixel=ee.Number(edgeBuffer).divide(imageProj.nominalScale())
# Buffer the edges using a dilation operation.
bufferedEdges=edges.fastDistanceTransform().lt(edgeBufferPixel)
# Mask areas not within the buffer .
edgeImage= image.select(band).updateMask(bufferedEdges)
localHistogram = ee.Dictionary(
edgeImage.reduceRegion(
**{
'reducer': ee.Reducer.histogram(255, 0.1),
'geometry': edgeImage.geometry(),
'scale': 90,
'maxPixels' : 1e10,
'bestEffort': True,
}
).get(band))
threshold = otsu(localHistogram)
date = image.date().format("YYYY-MM-DD")
water = image.select(band).lt(threshold).selfMask().set({"date":date})
return water.set({"threshold": threshold})
# When mapping the function I receive dictionary errors
water_images = s1Collection.map(edgeOtsu)
print(water_images.getInfo())
# If I apply the function to a single image I receive no errors
water_images = edgeOtsu(s1Collection.first())
print(water_images.getInfo())