Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #6
0
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)
Beispiel #7
0
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))
Beispiel #8
0
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")))