def gamma_map(img, window=7, enl=4.9): """Gamma Map speckle filtering algorithm. Algorithm adapted from https://groups.google.com/g/google-earth-engine-developers/c/a9W0Nlrhoq0/m/tnGMC45jAgAJ. args: img (ee.Image): Earth engine image object. Expects that imagery is a SAR image window (int, optional): moving window size to apply filter (i.e. a value of 7 == 7x7 window). default = 7 enl (float, optional): equivalent number of looks (enl) per pixel from a SAR scan. See https://sentinel.esa.int/web/sentinel/user-guides/sentinel-1-sar/resolutions/level-1-ground-range-detected. default = 4.9 returns: ee.Image: filtered SAR image using the Gamma Map algorithm """ bandNames = img.bandNames() # Square kernel, window should be odd (typically 3, 5 or 7) weights = ee.List.repeat(ee.List.repeat(1, window), window) midPt = (window // 2) + 1 if (window % 2) != 0 else window // 2 # ~~(window/2) does integer division in JavaScript kernel = ee.Kernel.fixed(window, window, weights, midPt, midPt, False) # Convert image from dB to natural values nat_img = geeutils.db_to_power(img) # Get mean and variance mean = nat_img.reduceNeighborhood(ee.Reducer.mean(), kernel) variance = nat_img.reduceNeighborhood(ee.Reducer.variance(), kernel) # "Pure speckle" threshold ci = variance.sqrt().divide(mean) # square root of inverse of enl # If ci <= cu, the kernel lies in a "pure speckle" area -> return simple mean cu = 1.0 / math.sqrt(enl) # If cu < ci < cmax the kernel lies in the low textured speckle area -> return the filtered value cmax = math.sqrt(2.0) * cu alpha = ee.Image(1.0 + cu * cu).divide(ci.multiply(ci).subtract(cu * cu)) b = alpha.subtract(enl + 1.0) d = (mean.multiply(mean).multiply(b).multiply(b).add( alpha.multiply(mean).multiply(nat_img).multiply(4.0 * enl))) f = b.multiply(mean).add(d.sqrt()).divide(alpha.multiply(2.0)) caster = ee.Dictionary.fromLists( bandNames, ee.List.repeat("float", bandNames.length())) img1 = (geeutils.power_to_db(mean.updateMask( ci.lte(cu))).rename(bandNames).cast(caster)) img2 = (geeutils.power_to_db( f.updateMask(ci.gt(cu)).updateMask( ci.lt(cmax))).rename(bandNames).cast(caster)) img3 = img.updateMask(ci.gte(cmax)).rename(bandNames).cast(caster) # If ci > cmax do not filter at all (i.e. we don't do anything, other then masking) result = (ee.ImageCollection([img1, img2, img3]).reduce( ee.Reducer.firstNonNull()).rename(bandNames).clip(img.geometry())) # Compose a 3 band image with the mean filtered "pure speckle", the "low textured" filtered and the unfiltered portions return result
def gamma_map(img, window=7, enl=5): bandNames = img.bandNames() # Square kernel, window should be odd (typically 3, 5 or 7) weights = ee.List.repeat(ee.List.repeat(1, window), window) midPt = (window // 2) + 1 if (window % 2) != 0 else window // 2 # ~~(window/2) does integer division in JavaScript kernel = ee.Kernel.fixed(window, window, weights, midPt, midPt, False) # Convert image from dB to natural values nat_img = geeutils.db_to_power(img) # Get mean and variance mean = nat_img.reduceNeighborhood(ee.Reducer.mean(), kernel) variance = nat_img.reduceNeighborhood(ee.Reducer.variance(), kernel) # "Pure speckle" threshold ci = variance.sqrt().divide(mean) # square root of inverse of enl # If ci <= cu, the kernel lies in a "pure speckle" area -> return simple mean cu = 1.0 / math.sqrt(enl) # If cu < ci < cmax the kernel lies in the low textured speckle area -> return the filtered value cmax = math.sqrt(2.0) * cu alpha = ee.Image(1.0 + cu * cu).divide(ci.multiply(ci).subtract(cu * cu)) b = alpha.subtract(enl + 1.0) d = (mean.multiply(mean).multiply(b).multiply(b).add( alpha.multiply(mean).multiply(nat_img).multiply(4.0 * enl))) f = b.multiply(mean).add(d.sqrt()).divide(alpha.multiply(2.0)) caster = ee.Dictionary.fromLists(bandNames, ee.List.repeat("float", 3)) img1 = (geeutils.power_to_db(mean.updateMask( ci.lte(cu))).rename(bandNames).cast(caster)) img2 = (geeutils.power_to_db( f.updateMask(ci.gt(cu)).updateMask( ci.lt(cmax))).rename(bandNames).cast(caster)) img3 = img.updateMask(ci.gte(cmax)).rename(bandNames).cast(caster) # If ci > cmax do not filter at all (i.e. we don't do anything, other then masking) result = (ee.ImageCollection([img1, img2, img3]).reduce( ee.Reducer.firstNonNull()).rename(bandNames).clip(img.geometry())) # Compose a 3 band image with the mean filtered "pure speckle", the "low textured" filtered and the unfiltered portions return result
def lee_sigma(img, window=9, sigma=0.9, looks=4, tk=7, keep_bands=["angle"]): """Lee Sigma speckle filtering algorithm. Implemented from interpreting https://doi.org/10.1109/TGRS.2008.2002881 args: img (ee.Image): Earth engine image object. Expects that imagery is a SAR image window (int, optional): moving window size to apply filter (i.e. a value of 9 == 9x9 window). default = 9 sigma (float, optional): sigma lookup value from table 1 in paper. default = 0.9 looks (int, optional): look intensity value from table 1 in paper. default = 4 tk (int, optional): threshold value to determine values in window as point targets. default = 7 keep_bands (list[str], optional): list of band names to drop during filtering and include in the result default = ["angle"] returns: ee.Image: filtered SAR image using the Lee Sigma algorithm """ band_names = img.bandNames() if keep_bands is not None: keep_img = img.select(keep_bands) proc_bands = band_names.removeAll(keep_bands) else: proc_bands = band_names img = img.select(proc_bands) midPt = (window // 2) + 1 if (window % 2) != 0 else window // 2 kernelWeights = ee.List.repeat(ee.List.repeat(1, window), window) kernel = ee.Kernel.fixed(window, window, kernelWeights, midPt, midPt) targetWeights = ee.List.repeat(ee.List.repeat(1, 3), 3) targetkernel = ee.Kernel.fixed(3, 3, targetWeights, 1, 1) # Lookup table for range and eta values for intensity sigmaLookup = ee.Dictionary({ 1: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.436, "A2": 1.92, "η": 0.4057 }), 0.6: ee.Dictionary({ "A1": 0.343, "A2": 2.21, "η": 0.4954 }), 0.7: ee.Dictionary({ "A1": 0.254, "A2": 2.582, "η": 0.5911 }), 0.8: ee.Dictionary({ "A1": 0.168, "A2": 3.094, "η": 0.6966 }), 0.9: ee.Dictionary({ "A1": 0.084, "A2": 3.941, "η": 0.8191 }), 0.95: ee.Dictionary({ "A1": 0.043, "A2": 4.840, "η": 0.8599 }), }), 2: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.582, "A2": 1.584, "η": 0.2763 }), 0.6: ee.Dictionary({ "A1": 0.501, "A2": 1.755, "η": 0.3388 }), 0.7: ee.Dictionary({ "A1": 0.418, "A2": 1.972, "η": 0.4062 }), 0.8: ee.Dictionary({ "A1": 0.327, "A2": 2.260, "η": 0.4819 }), 0.9: ee.Dictionary({ "A1": 0.221, "A2": 2.744, "η": 0.5699 }), 0.95: ee.Dictionary({ "A1": 0.152, "A2": 3.206, "η": 0.6254 }), }), 3: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.652, "A2": 1.458, "η": 0.2222 }), 0.6: ee.Dictionary({ "A1": 0.580, "A2": 1.586, "η": 0.2736 }), 0.7: ee.Dictionary({ "A1": 0.505, "A2": 1.751, "η": 0.3280 }), 0.8: ee.Dictionary({ "A1": 0.419, "A2": 1.865, "η": 0.3892 }), 0.9: ee.Dictionary({ "A1": 0.313, "A2": 2.320, "η": 0.4624 }), 0.95: ee.Dictionary({ "A1": 0.238, "A2": 2.656, "η": 0.5084 }), }), 4: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.694, "A2": 1.385, "η": 0.1921 }), 0.6: ee.Dictionary({ "A1": 0.630, "A2": 1.495, "η": 0.2348 }), 0.7: ee.Dictionary({ "A1": 0.560, "A2": 1.627, "η": 0.2825 }), 0.8: ee.Dictionary({ "A1": 0.480, "A2": 1.804, "η": 0.3354 }), 0.9: ee.Dictionary({ "A1": 0.378, "A2": 2.094, "η": 0.3991 }), 0.95: ee.Dictionary({ "A1": 0.302, "A2": 2.360, "η": 0.4391 }), }), }) # extract data from lookup looksDict = ee.Dictionary(sigmaLookup.get(ee.String(str(looks)))) sigmaImage = ee.Dictionary(looksDict.get(ee.String(str(sigma)))).toImage() a1 = sigmaImage.select("A1") a2 = sigmaImage.select("A2") aRange = a2.subtract(a1) eta = sigmaImage.select("η").pow(2) img = geeutils.db_to_power(img) # MMSE estimator mmseMask = img.gte(a1).Or(img.lte(a2)) mmseIn = img.updateMask(mmseMask) oneImg = ee.Image(1) z = mmseIn.reduceNeighborhood(ee.Reducer.mean(), kernel, None, True) varz = mmseIn.reduceNeighborhood(ee.Reducer.variance(), kernel) varx = (varz.subtract(z.abs().pow(2).multiply(eta))).divide( oneImg.add(eta)) b = varx.divide(varz) mmse = oneImg.subtract(b).multiply(z.abs()).add(b.multiply(mmseIn)) # workflow z99 = ee.Dictionary( img.reduceRegion( reducer=ee.Reducer.percentile([99], None, 255, 0.001, 1e6), geometry=img.geometry(), scale=10, bestEffort=True, )).toImage() overThresh = img.gte(z99) K = overThresh.reduceNeighborhood(ee.Reducer.sum(), targetkernel, None, True) retainPixel = K.gte(tk) xHat = geeutils.power_to_db(img.updateMask(retainPixel).unmask(mmse)) output = ee.Image(xHat).rename(proc_bands) if keep_bands is not None: output = output.addBands(keep_img) return output
def perona_malik(img, n_iters=10, K=3, method=1): """Perona-Malik (anisotropic diffusion) convolution Developed by Gennadii Donchyts see https://groups.google.com/g/google-earth-engine-developers/c/umGlt5qIN1I/m/PD8lsJ7qBAAJ I(n+1, i, j) = I(n, i, j) + lambda * (cN * dN(I) + cS * dS(I) + cE * dE(I), cW * dW(I)) args: img (ee.Image): Earth engine image object. Expects that imagery is a SAR image n_iters (int, optional): Number of interations to apply filter K (int,optional): moving window size to apply filter (i.e. a value of 7 == 7x7 window). default = 3 method (int, optional): choose method 1 (default) or 2 returns: ee.Image: filtered SAR image using the perona malik algorithm """ def _method_1(dI_W, dI_E, dI_N, dI_S): cW = dI_W.pow(2).multiply(k1).exp() cE = dI_E.pow(2).multiply(k1).exp() cN = dI_N.pow(2).multiply(k1).exp() cS = dI_S.pow(2).multiply(k1).exp() return cW, cE, cN, cS def _method_2(dI_W, dI_E, dI_N, dI_S): cW = one.divide(one.add(dI_W.pow(2).divide(k2))) cE = one.divide(one.add(dI_E.pow(2).divide(k2))) cN = one.divide(one.add(dI_N.pow(2).divide(k2))) cS = one.divide(one.add(dI_S.pow(2).divide(k2))) return cW, cE, cN, cS # covnert db to natural units before applying filter power = geeutils.db_to_power(img) dxW = ee.Kernel.fixed(3, 3, [[0, 0, 0], [1, -1, 0], [0, 0, 0]]) dxE = ee.Kernel.fixed(3, 3, [[0, 0, 0], [0, -1, 1], [0, 0, 0]]) dyN = ee.Kernel.fixed(3, 3, [[0, 1, 0], [0, -1, 0], [0, 0, 0]]) dyS = ee.Kernel.fixed(3, 3, [[0, 0, 0], [0, -1, 0], [0, 1, 0]]) one = ee.Image.constant(1.0) l = ee.Image.constant(0.2) k = ee.Image.constant(K) k1 = ee.Image.constant(-1.0 / K) k2 = k.pow(2) if method == 1: _method = _method_1 elif method == 2: _method = _method_2 else: raise NotImplementedError( "Could not determine algorithm to apply filter...options for `method` are 1 or 2" ) for i in range(n_iters): dI_W = power.convolve(dxW) dI_E = power.convolve(dxE) dI_N = power.convolve(dyN) dI_S = power.convolve(dyS) cW, cE, cN, cS = _method(dI_W, dI_E, dI_N, dI_S) power = power.add( l.multiply( cN.multiply(dI_N).add(cS.multiply(dI_S)).add( cE.multiply(dI_E)).add(cW.multiply(dI_W)))) # covnert natural to db units after filter is done img = geeutils.power_to_db(power) return img
def refined_lee(image, keep_bands=["angle"]): """Refined Lee speckle filtering algorithm. Algorithm adapted from https://groups.google.com/g/google-earth-engine-developers/c/ExepnAmP-hQ/m/7e5DnjXXAQAJ args: image (ee.Image): Earth engine image object. Expects that imagery is a SAR image returns: ee.Image: filtered SAR image using the Refined Lee algorithm """ # TODO: include keep bands...maybe one-shot filtering if using keep_bands??? def apply_filter(b): """Closure function to apply the refined lee algorithm on individual bands""" b = ee.String(b) img = power.select(b) # img must be in natural units, i.e. not in dB! # Set up 3x3 kernels weights3 = ee.List.repeat(ee.List.repeat(1, 3), 3) kernel3 = ee.Kernel.fixed(3, 3, weights3, 1, 1, False) mean3 = img.reduceNeighborhood(ee.Reducer.mean(), kernel3) variance3 = img.reduceNeighborhood(ee.Reducer.variance(), kernel3) # Use a sample of the 3x3 windows inside a 7x7 windows to determine gradients and directions sample_weights = ee.List([ [0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0], ]) sample_kernel = ee.Kernel.fixed(7, 7, sample_weights, 3, 3, False) # Calculate mean and variance for the sampled windows and store as 9 bands sample_mean = mean3.neighborhoodToBands(sample_kernel) sample_var = variance3.neighborhoodToBands(sample_kernel) # Determine the 4 gradients for the sampled windows gradients = sample_mean.select(1).subtract(sample_mean.select(7)).abs() gradients = gradients.addBands( sample_mean.select(6).subtract(sample_mean.select(2)).abs()) gradients = gradients.addBands( sample_mean.select(3).subtract(sample_mean.select(5)).abs()) gradients = gradients.addBands( sample_mean.select(0).subtract(sample_mean.select(8)).abs()) # And find the maximum gradient amongst gradient bands max_gradient = gradients.reduce(ee.Reducer.max()) # Create a mask for band pixels that are the maximum gradient gradmask = gradients.eq(max_gradient) # duplicate gradmask bands: each gradient represents 2 directions gradmask = gradmask.addBands(gradmask) # Determine the 8 directions directions = (sample_mean.select(1).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract(sample_mean.select(7))).multiply(1)) directions = directions.addBands( sample_mean.select(6).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract( sample_mean.select(2))).multiply(2)) directions = directions.addBands( sample_mean.select(3).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract( sample_mean.select(5))).multiply(3)) directions = directions.addBands( sample_mean.select(0).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract( sample_mean.select(8))).multiply(4)) # The next 4 are the not() of the previous 4 directions = directions.addBands( directions.select(0).Not().multiply(5)) directions = directions.addBands( directions.select(1).Not().multiply(6)) directions = directions.addBands( directions.select(2).Not().multiply(7)) directions = directions.addBands( directions.select(3).Not().multiply(8)) # Mask all values that are not 1-8 directions = directions.updateMask(gradmask) # "collapse" the stack into a singe band image (due to masking, each pixel has just one value (1-8) in it's directional band, and is otherwise masked) directions = directions.reduce(ee.Reducer.sum()) sample_stats = sample_var.divide(sample_mean.multiply(sample_mean)) # Calculate localNoiseVariance sigmaV = (sample_stats.toArray().arraySort().arraySlice( 0, 0, 5).arrayReduce(ee.Reducer.mean(), [0])) # Set up the 7*7 kernels for directional statistics rect_weights = ee.List.repeat(ee.List.repeat(0, 7), 3).cat( ee.List.repeat(ee.List.repeat(1, 7), 4)) diag_weights = ee.List([ [1, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1], ]) rect_kernel = ee.Kernel.fixed(7, 7, rect_weights, 3, 3, False) diag_kernel = ee.Kernel.fixed(7, 7, diag_weights, 3, 3, False) # Create stacks for mean and variance using the original kernels. Mask with relevant direction. dir_mean = img.reduceNeighborhood(ee.Reducer.mean(), rect_kernel).updateMask( directions.eq(1)) dir_var = img.reduceNeighborhood(ee.Reducer.variance(), rect_kernel).updateMask( directions.eq(1)) dir_mean = dir_mean.addBands( img.reduceNeighborhood(ee.Reducer.mean(), diag_kernel).updateMask(directions.eq(2))) dir_var = dir_var.addBands( img.reduceNeighborhood(ee.Reducer.variance(), diag_kernel).updateMask(directions.eq(2))) # and add the bands for rotated kernels for i in range(1, 4): dir_mean = dir_mean.addBands( img.reduceNeighborhood(ee.Reducer.mean(), rect_kernel.rotate(i)).updateMask( directions.eq(2 * i + 1))) dir_var = dir_var.addBands( img.reduceNeighborhood(ee.Reducer.variance(), rect_kernel.rotate(i)).updateMask( directions.eq(2 * i + 1))) dir_mean = dir_mean.addBands( img.reduceNeighborhood(ee.Reducer.mean(), diag_kernel.rotate(i)).updateMask( directions.eq(2 * i + 2))) dir_var = dir_var.addBands( img.reduceNeighborhood(ee.Reducer.variance(), diag_kernel.rotate(i)).updateMask( directions.eq(2 * i + 2))) # "collapse" the stack into a single band image (due to masking, each pixel has just one value in it's directional band, and is otherwise masked) dir_mean = dir_mean.reduce(ee.Reducer.sum()) dir_var = dir_var.reduce(ee.Reducer.sum()) # A finally generate the filtered value varX = dir_var.subtract( dir_mean.multiply(dir_mean).multiply(sigmaV)).divide( sigmaV.add(1.0)) b = varX.divide(dir_var) # return multi-band image band from array return (dir_mean.add(b.multiply(img.subtract(dir_mean))).arrayProject( [0]).arrayFlatten([["sum"]]).float()) band_names = image.bandNames() if keep_bands is not None: keep_img = image.select(keep_bands) proc_bands = band_names.removeAll(keep_bands) else: proc_bands = band_names image = image.select(proc_bands) power = geeutils.db_to_power(image) result = ee.ImageCollection(proc_bands.map(apply_filter)).toBands() output = geeutils.power_to_db(ee.Image(result)).rename(proc_bands) if keep_bands is not None: output = output.addBands(keep_img) return output
def lee_sigma(img, window=9, sigma=0.9, looks=4, tk=7, keep_bands="angle"): band_names = img.bandNames() proc_bands = band_names.remove(keep_bands) keep_img = img.select(keep_bands) img = img.select(proc_bands) midPt = (window // 2) + 1 if (window % 2) != 0 else window // 2 kernelWeights = ee.List.repeat(ee.List.repeat(1, window), window) kernel = ee.Kernel.fixed(window, window, kernelWeights, midPt, midPt) targetWeights = ee.List.repeat(ee.List.repeat(1, 3), 3) targetkernel = ee.Kernel.fixed(3, 3, targetWeights, 1, 1) # Lookup table for range and eta values for intensity sigmaLookup = ee.Dictionary({ 1: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.436, "A2": 1.92, "η": 0.4057 }), 0.6: ee.Dictionary({ "A1": 0.343, "A2": 2.21, "η": 0.4954 }), 0.7: ee.Dictionary({ "A1": 0.254, "A2": 2.582, "η": 0.5911 }), 0.8: ee.Dictionary({ "A1": 0.168, "A2": 3.094, "η": 0.6966 }), 0.9: ee.Dictionary({ "A1": 0.084, "A2": 3.941, "η": 0.8191 }), 0.95: ee.Dictionary({ "A1": 0.043, "A2": 4.840, "η": 0.8599 }), }), 2: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.582, "A2": 1.584, "η": 0.2763 }), 0.6: ee.Dictionary({ "A1": 0.501, "A2": 1.755, "η": 0.3388 }), 0.7: ee.Dictionary({ "A1": 0.418, "A2": 1.972, "η": 0.4062 }), 0.8: ee.Dictionary({ "A1": 0.327, "A2": 2.260, "η": 0.4819 }), 0.9: ee.Dictionary({ "A1": 0.221, "A2": 2.744, "η": 0.5699 }), 0.95: ee.Dictionary({ "A1": 0.152, "A2": 3.206, "η": 0.6254 }), }), 3: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.652, "A2": 1.458, "η": 0.2222 }), 0.6: ee.Dictionary({ "A1": 0.580, "A2": 1.586, "η": 0.2736 }), 0.7: ee.Dictionary({ "A1": 0.505, "A2": 1.751, "η": 0.3280 }), 0.8: ee.Dictionary({ "A1": 0.419, "A2": 1.865, "η": 0.3892 }), 0.9: ee.Dictionary({ "A1": 0.313, "A2": 2.320, "η": 0.4624 }), 0.95: ee.Dictionary({ "A1": 0.238, "A2": 2.656, "η": 0.5084 }), }), 4: ee.Dictionary({ 0.5: ee.Dictionary({ "A1": 0.694, "A2": 1.385, "η": 0.1921 }), 0.6: ee.Dictionary({ "A1": 0.630, "A2": 1.495, "η": 0.2348 }), 0.7: ee.Dictionary({ "A1": 0.560, "A2": 1.627, "η": 0.2825 }), 0.8: ee.Dictionary({ "A1": 0.480, "A2": 1.804, "η": 0.3354 }), 0.9: ee.Dictionary({ "A1": 0.378, "A2": 2.094, "η": 0.3991 }), 0.95: ee.Dictionary({ "A1": 0.302, "A2": 2.360, "η": 0.4391 }), }), }) # extract data from lookup looksDict = ee.Dictionary(sigmaLookup.get(ee.String(str(looks)))) sigmaImage = ee.Dictionary(looksDict.get(ee.String(str(sigma)))).toImage() a1 = sigmaImage.select("A1") a2 = sigmaImage.select("A2") aRange = a2.subtract(a1) eta = sigmaImage.select("η").pow(2) img = geeutils.db_to_power(img) # MMSE estimator mmseMask = img.gte(a1).Or(img.lte(a2)) mmseIn = img.updateMask(mmseMask) oneImg = ee.Image(1) z = mmseIn.reduceNeighborhood(ee.Reducer.mean(), kernel, None, True) varz = mmseIn.reduceNeighborhood(ee.Reducer.variance(), kernel) varx = (varz.subtract(z.abs().pow(2).multiply(eta))).divide( oneImg.add(eta)) b = varx.divide(varz) mmse = oneImg.subtract(b).multiply(z.abs()).add(b.multiply(mmseIn)) # workflow z99 = ee.Dictionary( img.reduceRegion( reducer=ee.Reducer.percentile([99], None, 255, 0.001, 1e6), geometry=img.geometry(), scale=10, bestEffort=True, )).toImage() overThresh = img.gte(z99) K = overThresh.reduceNeighborhood(ee.Reducer.sum(), targetkernel, None, True) retainPixel = K.gte(tk) xHat = geeutils.power_to_db(img.updateMask(retainPixel).unmask(mmse)) return ee.Image(xHat).rename(proc_bands).addBands(keep_img)
def refined_lee(image): def apply_filter(b): img = power.select([b]) # img must be in natural units, i.e. not in dB! # Set up 3x3 kernels weights3 = ee.List.repeat(ee.List.repeat(1, 3), 3) kernel3 = ee.Kernel.fixed(3, 3, weights3, 1, 1, False) mean3 = img.reduceNeighborhood(ee.Reducer.mean(), kernel3) variance3 = img.reduceNeighborhood(ee.Reducer.variance(), kernel3) # Use a sample of the 3x3 windows inside a 7x7 windows to determine gradients and directions sample_weights = ee.List([ [0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0], ]) sample_kernel = ee.Kernel.fixed(7, 7, sample_weights, 3, 3, False) # Calculate mean and variance for the sampled windows and store as 9 bands sample_mean = mean3.neighborhoodToBands(sample_kernel) sample_var = variance3.neighborhoodToBands(sample_kernel) # Determine the 4 gradients for the sampled windows gradients = sample_mean.select(1).subtract(sample_mean.select(7)).abs() gradients = gradients.addBands( sample_mean.select(6).subtract(sample_mean.select(2)).abs()) gradients = gradients.addBands( sample_mean.select(3).subtract(sample_mean.select(5)).abs()) gradients = gradients.addBands( sample_mean.select(0).subtract(sample_mean.select(8)).abs()) # And find the maximum gradient amongst gradient bands max_gradient = gradients.reduce(ee.Reducer.max()) # Create a mask for band pixels that are the maximum gradient gradmask = gradients.eq(max_gradient) # duplicate gradmask bands: each gradient represents 2 directions gradmask = gradmask.addBands(gradmask) # Determine the 8 directions directions = (sample_mean.select(1).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract(sample_mean.select(7))).multiply(1)) directions = directions.addBands( sample_mean.select(6).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract( sample_mean.select(2))).multiply(2)) directions = directions.addBands( sample_mean.select(3).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract( sample_mean.select(5))).multiply(3)) directions = directions.addBands( sample_mean.select(0).subtract(sample_mean.select(4)).gt( sample_mean.select(4).subtract( sample_mean.select(8))).multiply(4)) # The next 4 are the not() of the previous 4 directions = directions.addBands( directions.select(0).Not().multiply(5)) directions = directions.addBands( directions.select(1).Not().multiply(6)) directions = directions.addBands( directions.select(2).Not().multiply(7)) directions = directions.addBands( directions.select(3).Not().multiply(8)) # Mask all values that are not 1-8 directions = directions.updateMask(gradmask) # "collapse" the stack into a singe band image (due to masking, each pixel has just one value (1-8) in it's directional band, and is otherwise masked) directions = directions.reduce(ee.Reducer.sum()) sample_stats = sample_var.divide(sample_mean.multiply(sample_mean)) # Calculate localNoiseVariance sigmaV = (sample_stats.toArray().arraySort().arraySlice( 0, 0, 5).arrayReduce(ee.Reducer.mean(), [0])) # Set up the 7*7 kernels for directional statistics rect_weights = ee.List.repeat(ee.List.repeat(0, 7), 3).cat( ee.List.repeat(ee.List.repeat(1, 7), 4)) diag_weights = ee.List([ [1, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1], ]) rect_kernel = ee.Kernel.fixed(7, 7, rect_weights, 3, 3, False) diag_kernel = ee.Kernel.fixed(7, 7, diag_weights, 3, 3, False) # Create stacks for mean and variance using the original kernels. Mask with relevant direction. dir_mean = img.reduceNeighborhood(ee.Reducer.mean(), rect_kernel).updateMask( directions.eq(1)) dir_var = img.reduceNeighborhood(ee.Reducer.variance(), rect_kernel).updateMask( directions.eq(1)) dir_mean = dir_mean.addBands( img.reduceNeighborhood(ee.Reducer.mean(), diag_kernel).updateMask(directions.eq(2))) dir_var = dir_var.addBands( img.reduceNeighborhood(ee.Reducer.variance(), diag_kernel).updateMask(directions.eq(2))) # and add the bands for rotated kernels for i in range(1, 4): dir_mean = dir_mean.addBands( img.reduceNeighborhood(ee.Reducer.mean(), rect_kernel.rotate(i)).updateMask( directions.eq(2 * i + 1))) dir_var = dir_var.addBands( img.reduceNeighborhood(ee.Reducer.variance(), rect_kernel.rotate(i)).updateMask( directions.eq(2 * i + 1))) dir_mean = dir_mean.addBands( img.reduceNeighborhood(ee.Reducer.mean(), diag_kernel.rotate(i)).updateMask( directions.eq(2 * i + 2))) dir_var = dir_var.addBands( img.reduceNeighborhood(ee.Reducer.variance(), diag_kernel.rotate(i)).updateMask( directions.eq(2 * i + 2))) # "collapse" the stack into a single band image (due to masking, each pixel has just one value in it's directional band, and is otherwise masked) dir_mean = dir_mean.reduce(ee.Reducer.sum()) dir_var = dir_var.reduce(ee.Reducer.sum()) # A finally generate the filtered value varX = dir_var.subtract( dir_mean.multiply(dir_mean).multiply(sigmaV)).divide( sigmaV.add(1.0)) b = varX.divide(dir_var) # return multi-band image band from array return (dir_mean.add(b.multiply(img.subtract(dir_mean))).arrayProject( [0]).arrayFlatten([["sum"]]).float()) bandNames = image.bandNames() power = geeutils.db_to_power(image) result = ee.ImageCollection( bandNames.map(apply_filter)).toBands().rename(bandNames) return geeutils.power_to_db(ee.Image(result))
def slope_correction(image, elevation, model="volume", buffer=0, scale=1000): """This function applies the slope correction on a Sentinel-1 image. Function based on https:# doi.org/10.3390/rs12111867. Adapted from https:# github.com/ESA-PhiLab/radiometric-slope-correction/blob/master/notebooks/1%20-%20Generate%20Data.ipynb args: image (ee.Image): Sentinel-1 to perform correction on elevation (ee.Image): Input DEM to calculate slope corrections from model (str, optional): physical reference model to be applied. Options are 'volume' or 'surface'. default = volume buffer (int, optional): buffer in meters for layover/shadow mask. If zero then no buffer will be applied. default = 0 scale (int, optional): reduction scale to process satellite heading compared to ground. Increasing will reduce chance of OOM errors but reduce local scale correction accuracy. default = 1000 returns: ee.Image: slope corrected SAR imagery with look and local incidence angle bands raises: NotImplementedError: when keyword model is not of 'volume' or 'surface' """ def _volumetric_model_SCF(theta_iRad, alpha_rRad): """Closure funnction for calculation of volumetric model SCF args: theta_iRad (ee.Image): incidence angle in radians alpha_rRad (ee.Image): slope steepness in range returns: ee.Image """ # model nominator = (ninetyRad.subtract(theta_iRad).add(alpha_rRad)).tan() denominator = (ninetyRad.subtract(theta_iRad)).tan() return nominator.divide(denominator) def _surface_model_SCF(theta_iRad, alpha_rRad, alpha_azRad): """Closure funnction for calculation of direct model SCF args: theta_iRad (ee.Image): incidence angle in radians alpha_rRad (ee.Image): slope steepness in range alpha_azRad (ee.Image): slope steepness in azimuth returns: ee.Image """ # model nominator = (ninetyRad.subtract(theta_iRad)).cos() denominator = alpha_azRad.cos().multiply( (ninetyRad.subtract(theta_iRad).add(alpha_rRad)).cos()) return nominator.divide(denominator) def _erode(image, distance): """Closure function to buffer raster values args: image (ee.Image): image that should be buffered distance (int): distance of buffer in meters returns: ee.Image """ d = (image.Not().unmask(1).fastDistanceTransform(10).sqrt().multiply( ee.Image.pixelArea().sqrt())) return image.updateMask(d.gt(distance)) def _masking(alpha_rRad, theta_iRad, buffer): """Closure function for masking of layover and shadow args: alpha_rRad (ee.Image): slope steepness in range theta_iRad (ee.Image): incidence angle in radians buffer (int): buffer in meters returns: ee.Image """ # layover, where slope > radar viewing angle layover = alpha_rRad.lt(theta_iRad).rename("layover") # shadow shadow = alpha_rRad.gt( ee.Image.constant(-1).multiply( ninetyRad.subtract(theta_iRad))).rename("shadow") # add buffer to layover and shadow if buffer > 0: layover = _erode(layover, buffer) shadow = _erode(shadow, buffer) # combine layover and shadow no_data_mask = layover.And(shadow).rename("no_data_mask") return no_data_mask # get the image geometry and projection geom = image.geometry(scale) proj = image.select(1).projection() # image to convert angle to radians to_radians = ee.Image.constant((math.pi / 180)) # create a 90 degree image in radians ninetyRad = ee.Image.constant(90).multiply(to_radians) # calculate the look direction heading = (ee.Terrain.aspect(image.select("angle")).reduceRegion( ee.Reducer.mean(), geom, scale).get("aspect")) # Sigma0 to Power of input image sigma0Pow = geeutils.db_to_power(image) # the numbering follows the article chapters # 2.1.1 Radar geometry theta_iRad = image.select("angle").multiply(to_radians) phi_iRad = ee.Image.constant(heading).multiply(to_radians) # 2.1.2 Terrain geometry alpha_sRad = (ee.Terrain.slope(elevation).select("slope").multiply( to_radians).setDefaultProjection(proj)) phi_sRad = (ee.Terrain.aspect(elevation).select("aspect").multiply( to_radians).setDefaultProjection(proj)) # 2.1.3 Model geometry # reduce to 3 angle phi_rRad = phi_iRad.subtract(phi_sRad) # slope steepness in range (eq. 2) alpha_rRad = (alpha_sRad.tan().multiply(phi_rRad.cos())).atan() # slope steepness in azimuth (eq 3) alpha_azRad = (alpha_sRad.tan().multiply(phi_rRad.sin())).atan() # local incidence angle (eq. 4) theta_liaRad = (alpha_azRad.cos().multiply( (theta_iRad.subtract(alpha_rRad)).cos())).acos() theta_liaDeg = theta_liaRad.multiply(180 / math.pi) # 2.2 # Gamma_nought gamma0 = sigma0Pow.divide(theta_iRad.cos()) gamma0dB = geeutils.db_to_power(gamma0).select(["VV", "VH"], ["VV_gamma0", "VH_gamma0"]) if model == "volume": scf = _volumetric_model_SCF(theta_iRad, alpha_rRad) elif model == "surface": scf = _surface_model_SCF(theta_iRad, alpha_rRad, alpha_azRad) else: raise NotImplementedError( f"Defined model, {model}, has not been implemented. Options are 'volume' or 'surface'" ) # apply model for Gamm0_f gamma0_flat = gamma0.divide(scf) gamma0_flatDB = geeutils.power_to_db(gamma0_flat).select(["VV", "VH"]) # calculate layover and shadow mask masks = _masking(alpha_rRad, theta_iRad, buffer) return (gamma0_flatDB.updateMask(masks).addBands( image.select("angle")).addBands( theta_liaDeg.rename("local_inc_angle")))