From a35504ff1337f3202581bf672c7646112a21bcc8 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Tue, 31 Jan 2023 22:50:39 +0100 Subject: [PATCH 01/12] draft implementation of MSI cloud post-processing with a single-pass algorithm --- .../S2IdepixCloudPostProcess2Op.java | 361 ++++++++++++++++++ .../org.esa.snap.core.gpf.OperatorSpi | 1 + pom.xml | 2 +- 3 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java new file mode 100644 index 00000000..700c55c3 --- /dev/null +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java @@ -0,0 +1,361 @@ +package org.esa.snap.idepix.s2msi.operators; + +import com.bc.ceres.core.ProgressMonitor; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.gpf.Operator; +import org.esa.snap.core.gpf.OperatorException; +import org.esa.snap.core.gpf.OperatorSpi; +import org.esa.snap.core.gpf.Tile; +import org.esa.snap.core.gpf.annotations.OperatorMetadata; +import org.esa.snap.core.gpf.annotations.Parameter; +import org.esa.snap.core.gpf.annotations.SourceProduct; +import org.esa.snap.core.util.ProductUtils; +import org.esa.snap.core.util.RectangleExtender; +import org.esa.snap.idepix.s2msi.util.S2IdepixUtils; + +import java.awt.Rectangle; + +import static org.esa.snap.idepix.s2msi.util.S2IdepixConstants.*; + +/** + * Adds a cloud buffer to cloudy pixels and does a cloud discrimination on urban areas. + */ +@OperatorMetadata(alias = "Idepix.S2CloudPostProcess2", + version = "3.0", + internal = true, + authors = "Olaf Danne, Martin Boettcher", + copyright = "(c) 2016-2023 by Brockmann Consult", + description = "Performs post processing of cloudy pixels and adds optional cloud buffer.") +public class S2IdepixCloudPostProcess2Op extends Operator { + + private static final int IDEPIX_CLOUD_AMBIGUOUS_BIT = 1 << IDEPIX_CLOUD_AMBIGUOUS; + private static final int IDEPIX_CLOUD_SURE_BIT = 1 << IDEPIX_CLOUD_SURE; + private static final int IDEPIX_CLOUD_BIT = 1 << IDEPIX_CLOUD; + private static final int IDEPIX_CIRRUS_AMBIGUOUS_BIT = 1 << IDEPIX_CIRRUS_AMBIGUOUS; + private static final int IDEPIX_CIRRUS_SURE_BIT = 1 << IDEPIX_CIRRUS_SURE; + private static final int IDEPIX_WATER_BIT = 1 << IDEPIX_WATER; + + @SourceProduct(alias = "classifiedProduct") + private Product classifiedProduct; + + @Parameter(defaultValue = "true", label = "Compute cloud buffer for cloud ambiguous pixels too.") + private boolean computeCloudBufferForCloudAmbiguous; + + @Parameter(defaultValue = "2", label = "Width of cloud buffer (# of pixels)") + private int cloudBufferWidth; + + private int landWaterContextSize; + private int urbanContextSize = 11; + private int cdiStddevContextSize = 7; + private int contextSize; + private int cloudBufferSize; + + private int landWaterContextRadius; + private int urbanContextRadius = urbanContextSize / 2; + private int cdiStddevContextRadius = cdiStddevContextSize / 2; + private int contextRadius; + + private static final float COAST_BUFFER_SIZE = 1000f; + private Band origClassifFlagBand; + private Band b2Band; + private Band b7Band; + private Band b8Band; + private Band b8aBand; + private Band b11Band; + private RectangleExtender pixelStateRectCalculator; + private RectangleExtender urbanRectCalculator; + private RectangleExtender cdiRectCalculator; + private RectangleExtender contextRectCalculator; + private RectangleExtender cloudBufferRectCalculator; + + private static final double CDI_THRESHOLD = -0.5; + + @Override + public void initialize() throws OperatorException { + + final int resolution = S2IdepixUtils.determineResolution(classifiedProduct); + landWaterContextSize = (int) Math.floor((2 * COAST_BUFFER_SIZE) / resolution); // TODO is a +1 required here? + landWaterContextRadius = landWaterContextSize / 2; + contextSize = Math.max(landWaterContextSize, Math.max(urbanContextSize, cdiStddevContextSize)); + contextRadius = contextSize / 2; + cloudBufferSize = 2 * cloudBufferWidth + 1; + + pixelStateRectCalculator = createRectCalculator(contextSize/2); + urbanRectCalculator = createRectCalculator(urbanContextSize/2); + cdiRectCalculator = createRectCalculator(cdiStddevContextSize/2); + contextRectCalculator = createRectCalculator(contextSize/2); + cloudBufferRectCalculator = createRectCalculator(cloudBufferWidth); + + Product cloudBufferProduct = createTargetProduct(classifiedProduct, + classifiedProduct.getName(), + classifiedProduct.getProductType()); + origClassifFlagBand = classifiedProduct.getBand(IDEPIX_CLASSIF_FLAGS); + ProductUtils.copyBand(IDEPIX_CLASSIF_FLAGS, classifiedProduct, cloudBufferProduct, false); + b2Band = classifiedProduct.getBand("B2"); + b7Band = classifiedProduct.getBand("B7"); + b8Band = classifiedProduct.getBand("B8"); + b8aBand = classifiedProduct.getBand("B8A"); + b11Band = classifiedProduct.getBand("B11"); + + setTargetProduct(cloudBufferProduct); + } + + @Override + public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) throws OperatorException { + + Rectangle targetRectangle = targetTile.getRectangle(); + final Rectangle pixelStateRectangle = pixelStateRectCalculator.extend(targetRectangle); + final Rectangle urbanRectangle = urbanRectCalculator.extend(targetRectangle); + final Rectangle cdiRectangle = cdiRectCalculator.extend(targetRectangle); + final Rectangle contextRectangle = contextRectCalculator.extend(targetRectangle); + final Rectangle cloudBufferRectangle = cloudBufferRectCalculator.extend(targetRectangle); + + final Tile sourceFlagTile = getSourceTile(origClassifFlagBand, pixelStateRectangle); + final Tile b7Tile = getSourceTile(b7Band, cdiRectangle); + final Tile b8Tile = getSourceTile(b8Band, cdiRectangle); + final Tile b8aTile = getSourceTile(b8aBand, cdiRectangle); + final Tile b2Tile = getSourceTile(b2Band, targetRectangle); + final Tile b11Tile = getSourceTile(b11Band, targetRectangle); + + correctCoastalAndUrbanClouds(contextRectangle, pixelStateRectangle, urbanRectangle, cdiRectangle, targetRectangle, + b7Tile, b2Tile, b8Tile, b8aTile, b11Tile, sourceFlagTile, targetTile); + + // cloud buffer is based on the corrected clouds + addCloudBuffer(targetRectangle, cloudBufferRectangle, sourceFlagTile, targetTile); + } + + private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, Rectangle pixelStateRectangle, Rectangle urbanRectangle, Rectangle cdiRectangle, Rectangle targetRectangle, Tile b7Tile, Tile b2Tile, Tile b8Tile, Tile b8aTile, Tile b11Tile, Tile sourceFlagTile, Tile targetTile) { + // allocate and initialise some lines of full width to collect pixel contributions + // The lines will be re-used for later image lines in a rolling manner. + final boolean[][] landAccu = new boolean[contextSize][targetRectangle.width]; + final boolean[][] waterAccu = new boolean[contextSize][targetRectangle.width]; + final boolean[][] urbanSomeClearAccu = new boolean[contextSize][targetRectangle.width]; + final double m7[][] = new double[contextSize][targetRectangle.width]; + final double c7[][] = new double[contextSize][targetRectangle.width]; + final double m8[][] = new double[contextSize][targetRectangle.width]; + final double c8[][] = new double[contextSize][targetRectangle.width]; + final short n78[][] = new short[contextSize][targetRectangle.width]; + + // loop over extended source tile + for (int y = contextRectangle.y; y < contextRectangle.y + contextRectangle.height; y++) { + checkForCancellation(); + for (int x = contextRectangle.x; x < contextRectangle.x + contextRectangle.width; x++) { + + // write dilated land and water patch into accu, required for coastal cloud distinction + if (pixelStateRectangle.contains(x, y)) { + if (isLand(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, true, landAccu); + } + if (isWater(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, true, waterAccu); + } + } + + // write 11x11 "filtered" non-cloud patch into accu, required for urban cloud distinction + if (urbanRectangle.contains(x, y)) { + if (!isCloud(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, targetRectangle, urbanContextRadius, contextSize, true, urbanSomeClearAccu); + } + } + + // add stddev contributions to accu of sums, squares, counts, required for urban cloud distinction + if (cdiRectangle.contains(x, y)) { + final float b7 = b7Tile.getSampleFloat(x, y); + final float b8 = b8Tile.getSampleFloat(x, y); + final float b8a = b8aTile.getSampleFloat(x, y); + if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { + final float b7b8a = b7 / b8a; + final float b8b8a = b8 / b8a; + final float b7b8a2 = b7b8a * b7b8a; + final float b8b8a2 = b8b8a * b8b8a; + addStddevPatchInAccu(y, x, targetRectangle, cdiStddevContextRadius, b8b8a, b7b8a2, b8b8a2, b7b8a, m7, c7, m8, c8, n78); + } + } + + // evaluate accu if we have enough context + // target pixel yt/xt + final int yt = y - contextRadius; + final int xt = x - contextRadius; + // accu position jt/it + final int jt = (yt - targetRectangle.y) % contextSize; + final int it = xt - targetRectangle.x; + if (targetRectangle.contains(xt, yt)) { + // read pixel classif flags from source, apply corrections, write it to target + int pixelClassifFlags = sourceFlagTile.getSampleInt(xt, yt); + pixelClassifFlags = coastalCloudDistinction(yt, xt, jt, it, landAccu, waterAccu, sourceFlagTile, b2Tile, b8Tile, b11Tile, pixelClassifFlags); + pixelClassifFlags = urbanCloudDistinction(jt, it, urbanSomeClearAccu, m7, m8, c7, c8, n78, pixelClassifFlags); + targetTile.setSample(xt, yt, pixelClassifFlags); + } + } + } + } + + private void fillPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, int contextSize, boolean value, boolean[][] accu) { + // reduce patch to part overlapping with target image + final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); + final int jMax = Math.min(y - targetRectangle.y + 1 + halfWidth, targetRectangle.height); + final int iMin = Math.max(x - targetRectangle.x - halfWidth, 0); + final int iMax = Math.min(x - targetRectangle.x + 1 + halfWidth, targetRectangle.width); + // fill patch with value + for (int j = jMin; j < jMax; ++j) { + final int jj = j % contextSize; + for (int i = iMin; i < iMax; ++i) { + accu[jj][i] = value; + } + } + } + + private void addStddevPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, + float b8b8a, float b7b8a2, float b8b8a2, float b7b8a, + double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78) { + // reduce patch to part overlapping with target image + final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); + final int jMax = Math.min(y - targetRectangle.y + 1 + halfWidth, targetRectangle.height); + final int iMin = Math.max(x - targetRectangle.x - halfWidth, 0); + final int iMax = Math.min(x - targetRectangle.x + 1 + halfWidth, targetRectangle.width); + // add to mean and to sum of squares within patch + for (int j = jMin; j < jMax; ++j) { + final int jj = j % contextSize; + for (int i = iMin; i < iMax; ++i) { + m7[jj][i] += b7b8a; + c7[jj][i] += b7b8a2; + m8[jj][i] += b8b8a; + c8[jj][i] += b8b8a2; + n78[jj][i] += 1; + } + } + } + + private static int coastalCloudDistinction(int yt, int xt, int jt, int it, + boolean[][] landAccu, boolean[][] waterAccu, + Tile sourceFlagTile, Tile b2Tile, Tile b8Tile, Tile b11Tile, + int pixelClassifFlags) { + // not land but there is some land nearby, or not water and some water nearby + final boolean isCoastal = + (!isLand(sourceFlagTile, yt, xt) && landAccu[jt][it]) || + (!isWater(sourceFlagTile, yt, xt) && waterAccu[jt][it]); + if (isCoastal) { + // another cloud test + final float b2 = b2Tile.getSampleFloat(xt, yt); + final float b8 = b8Tile.getSampleFloat(xt, yt); + final float b11 = b11Tile.getSampleFloat(xt, yt); + final float idx1 = b2 / b11; + final float idx2 = b8 / b11; + final boolean notCoastalCloud = idx1 > 0.7f || (idx1 > 0.6f && idx2 > 0.9f); + if (notCoastalCloud) { + // clear cloud flags if cloud test fails + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS); + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_SURE); + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD); + } else if ((pixelClassifFlags & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE))) == 0) { + // align cloud flag with combination of ambiguous and sure if none of them is set + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD); + } else { + // align cloud flag with combination of ambiguous and sure if one of them is set + pixelClassifFlags |= IDEPIX_CLOUD_BIT; + } + } + // reset accu for re-use + landAccu[jt][it] = false; + waterAccu[jt][it] = false; + return pixelClassifFlags; + } + + private static int urbanCloudDistinction(int jt, int it, boolean[][] urbanSomeClearAccu, double[][] m7, double[][] m8, double[][] c7, double[][] c8, short[][] n78, int pixelClassifFlags) { + // some clear pixels nearby, and not cirrus or water + final boolean notInTheMiddleOfACloud = urbanSomeClearAccu[jt][it]; + if (notInTheMiddleOfACloud + && (pixelClassifFlags & (IDEPIX_CIRRUS_AMBIGUOUS_BIT | IDEPIX_CIRRUS_SURE_BIT | IDEPIX_WATER_BIT)) == 0 + && (pixelClassifFlags & (IDEPIX_CLOUD_AMBIGUOUS_BIT | IDEPIX_CLOUD_SURE_BIT | IDEPIX_CLOUD_BIT)) != 0) { + // another non-cloud test + final double variance7 = variance_of(c7[jt][it], m7[jt][it], n78[jt][it]); + final double variance8 = variance_of(c8[jt][it], m8[jt][it], n78[jt][it]); + final double cdiValue = (variance7 - variance8) / (variance7 + variance8); + if (cdiValue >= CDI_THRESHOLD) { + // clear cloud flags if CDI test succeeds + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS); + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_SURE); + pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD); + } + } + // clear accu for re-use + urbanSomeClearAccu[jt][it] = false; + return pixelClassifFlags; + } + + private void addCloudBuffer(Rectangle targetRectangle, Rectangle cloudBufferRectangle, Tile sourceFlagTile, Tile targetTile) { + final boolean[][] cloudBufferAccu = new boolean[cloudBufferSize][targetRectangle.width]; + for (int y = cloudBufferRectangle.y; y < cloudBufferRectangle.y + cloudBufferRectangle.height; y++) { + checkForCancellation(); + for (int x = cloudBufferRectangle.x; x < cloudBufferRectangle.x + cloudBufferRectangle.width; x++) { + if (isCloudForBuffer(targetRectangle.contains(x, y) ? targetTile : sourceFlagTile, x, y)) { + fillPatchInAccu(y, x, targetRectangle, cloudBufferWidth, cloudBufferSize, true, cloudBufferAccu); + } + // target pixel yt/xt + final int yt = y - cloudBufferWidth; + final int xt = x - cloudBufferWidth; + // accu position jt/it + final int jt = (yt - targetRectangle.y) % cloudBufferSize; + final int it = xt - targetRectangle.x; + // evaluate accu if we have enough context + // set cloud buffer flag if it is not cloud but there is a cloud nearby + if (targetRectangle.contains(xt, yt) + && cloudBufferAccu[jt][it] + && (targetTile.getSampleInt(xt, yt) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD))) == 0) { + targetTile.setSample(xt, yt, IDEPIX_CLOUD_BUFFER, true); + } + } + } + } + + + private RectangleExtender createRectCalculator(int width) { + return new RectangleExtender(new Rectangle(classifiedProduct.getSceneRasterWidth(), + classifiedProduct.getSceneRasterHeight()), + width, width); + } + + private static Product createTargetProduct(Product sourceProduct, String name, String type) { + final int sceneWidth = sourceProduct.getSceneRasterWidth(); + final int sceneHeight = sourceProduct.getSceneRasterHeight(); + Product targetProduct = new Product(name, type, sceneWidth, sceneHeight); + ProductUtils.copyGeoCoding(sourceProduct, targetProduct); + targetProduct.setStartTime(sourceProduct.getStartTime()); + targetProduct.setEndTime(sourceProduct.getEndTime()); + return targetProduct; + } + + private static int removeBit(int x, int i) { + final int mask = -1 << i; + return ((x ^ (x >>> 1)) & mask) ^ x; + } + + private static boolean isLand(Tile tile, int y, int x) { + return tile.getSampleBit(x, y, IDEPIX_LAND); + } + + private static boolean isWater(Tile tile, int y, int x) { + return tile.getSampleBit(x, y, IDEPIX_WATER); + } + + private static boolean isCloud(Tile tile, int y, int x) { + return (tile.getSampleInt(x, y) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD))) != 0; + } + + private static double variance_of(double c, double m, int n) { + return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / (n - 1); + } + + private boolean isCloudForBuffer(Tile targetTile, int x, int y) { + return targetTile.getSampleBit(x, y, IDEPIX_CLOUD_SURE) || + (computeCloudBufferForCloudAmbiguous && targetTile.getSampleBit(x, y, IDEPIX_CLOUD_AMBIGUOUS)); + } + + + public static class Spi extends OperatorSpi { + public Spi() { + super(S2IdepixCloudPostProcess2Op.class); + } + } +} diff --git a/idepix-s2msi/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi b/idepix-s2msi/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi index 2231f4aa..c21861e5 100644 --- a/idepix-s2msi/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi +++ b/idepix-s2msi/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi @@ -2,6 +2,7 @@ org.esa.snap.idepix.s2msi.S2IdepixOp$Spi org.esa.snap.idepix.s2msi.S2IdepixPostProcessOp$Spi org.esa.snap.idepix.s2msi.S2IdepixClassificationOp$Spi org.esa.snap.idepix.s2msi.operators.S2IdepixCloudPostProcessOp$Spi +org.esa.snap.idepix.s2msi.operators.S2IdepixCloudPostProcess2Op$Spi org.esa.snap.idepix.s2msi.operators.mountainshadow.SlopeAspectOrientationOp$Spi org.esa.snap.idepix.s2msi.operators.mountainshadow.S2IdepixMountainShadowOp$Spi org.esa.snap.idepix.s2msi.operators.cloudshadow.S2IdepixCloudShadowOp$Spi diff --git a/pom.xml b/pom.xml index 30f98c36..49185f57 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ UTF-8 - 9.0.2 + 9.0.5-SNAPSHOT RELEASE82 2.0.05 From 91b575ca6909cccbaa305fe78838878474679719 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Fri, 3 Feb 2023 12:53:21 +0100 Subject: [PATCH 02/12] implementation of MSI cloud post-processing with a single-pass algorithm --- idepix-s2msi/pom.xml | 2 +- .../org/esa/snap/idepix/s2msi/S2IdepixOp.java | 8 +- .../S2IdepixCloudPostProcess2Op.java | 199 ++++++++++++------ 3 files changed, 137 insertions(+), 72 deletions(-) diff --git a/idepix-s2msi/pom.xml b/idepix-s2msi/pom.xml index 26b700cb..e7a696e4 100644 --- a/idepix-s2msi/pom.xml +++ b/idepix-s2msi/pom.xml @@ -26,7 +26,7 @@ idepix-s2msi - 9.0.2-SNAPSHOT + 9.0.3-SNAPSHOT nbm diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java index 91381d97..c36908b1 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java @@ -11,6 +11,7 @@ import org.esa.snap.core.gpf.annotations.SourceProduct; import org.esa.snap.core.gpf.annotations.TargetProduct; import org.esa.snap.dem.gpf.AddElevationOp; +import org.esa.snap.idepix.s2msi.operators.S2IdepixCloudPostProcess2Op; import org.esa.snap.idepix.s2msi.operators.S2IdepixCloudPostProcessOp; import org.esa.snap.idepix.s2msi.util.AlgorithmSelector; import org.esa.snap.idepix.s2msi.util.S2IdepixConstants; @@ -108,6 +109,9 @@ private void processSentinel2() { int cacheSize = Integer.parseInt(System.getProperty(S2IdepixUtils.TILECACHE_PROPERTY, "1600")); s2ClassifProduct = S2IdepixUtils.computeTileCacheProduct(s2ClassifProduct, cacheSize); + targetProduct = s2ClassifProduct; + if (true) return; + // Post Cloud Classification: cloud shadow, cloud buffer, mountain shadow Product postProcessingProduct = computePostProcessProduct(sourceProduct, s2ClassifProduct); @@ -146,7 +150,9 @@ private Product computePostProcessProduct(Product l1cProduct, Product classifica Map paramsBuffer = new HashMap<>(); paramsBuffer.put("cloudBufferWidth", cloudBufferWidth); paramsBuffer.put("computeCloudBufferForCloudAmbiguous", computeCloudBufferForCloudAmbiguous); - Product cloudBufferProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(S2IdepixCloudPostProcessOp.class), + //Product cloudBufferProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(S2IdepixCloudPostProcessOp.class), + // paramsBuffer, input); + Product cloudBufferProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(S2IdepixCloudPostProcess2Op.class), paramsBuffer, input); int cacheSize = Integer.parseInt(System.getProperty(S2IdepixUtils.TILECACHE_PROPERTY, "1600")) / 5; diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java index 700c55c3..1a77bced 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java @@ -10,6 +10,7 @@ import org.esa.snap.core.gpf.annotations.OperatorMetadata; import org.esa.snap.core.gpf.annotations.Parameter; import org.esa.snap.core.gpf.annotations.SourceProduct; +import org.esa.snap.core.util.BitSetter; import org.esa.snap.core.util.ProductUtils; import org.esa.snap.core.util.RectangleExtender; import org.esa.snap.idepix.s2msi.util.S2IdepixUtils; @@ -36,6 +37,8 @@ public class S2IdepixCloudPostProcess2Op extends Operator { private static final int IDEPIX_CIRRUS_SURE_BIT = 1 << IDEPIX_CIRRUS_SURE; private static final int IDEPIX_WATER_BIT = 1 << IDEPIX_WATER; + private static final boolean BORDER_COPY = true; + @SourceProduct(alias = "classifiedProduct") private Product classifiedProduct; @@ -81,10 +84,10 @@ public void initialize() throws OperatorException { contextRadius = contextSize / 2; cloudBufferSize = 2 * cloudBufferWidth + 1; - pixelStateRectCalculator = createRectCalculator(contextSize/2); - urbanRectCalculator = createRectCalculator(urbanContextSize/2); - cdiRectCalculator = createRectCalculator(cdiStddevContextSize/2); - contextRectCalculator = createRectCalculator(contextSize/2); + pixelStateRectCalculator = createRectCalculator(contextRadius); + urbanRectCalculator = createRectCalculator(urbanContextRadius); + cdiRectCalculator = createRectCalculator(cdiStddevContextRadius); + contextRectCalculator = createRectCalculator(contextRadius); cloudBufferRectCalculator = createRectCalculator(cloudBufferWidth); Product cloudBufferProduct = createTargetProduct(classifiedProduct, @@ -98,6 +101,9 @@ public void initialize() throws OperatorException { b8aBand = classifiedProduct.getBand("B8A"); b11Band = classifiedProduct.getBand("B11"); + ProductUtils.copyBand("pixel_classif_flags", classifiedProduct, + "flags_before_postprocessing", cloudBufferProduct, true); + setTargetProduct(cloudBufferProduct); } @@ -108,8 +114,13 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th final Rectangle pixelStateRectangle = pixelStateRectCalculator.extend(targetRectangle); final Rectangle urbanRectangle = urbanRectCalculator.extend(targetRectangle); final Rectangle cdiRectangle = cdiRectCalculator.extend(targetRectangle); - final Rectangle contextRectangle = contextRectCalculator.extend(targetRectangle); + final Rectangle cdiExtendedRectangle = new Rectangle(targetRectangle); + cdiExtendedRectangle.grow(cdiStddevContextRadius, cdiStddevContextRadius); + final Rectangle contextRectangle = new Rectangle(targetRectangle); + contextRectangle.grow(contextRadius, contextRadius); //contextRectCalculator.extend(targetRectangle); final Rectangle cloudBufferRectangle = cloudBufferRectCalculator.extend(targetRectangle); + final Rectangle cloudBufferExtendedRectangle = new Rectangle(targetRectangle); + cloudBufferExtendedRectangle.grow(cloudBufferWidth, cloudBufferWidth); final Tile sourceFlagTile = getSourceTile(origClassifFlagBand, pixelStateRectangle); final Tile b7Tile = getSourceTile(b7Band, cdiRectangle); @@ -118,24 +129,31 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th final Tile b2Tile = getSourceTile(b2Band, targetRectangle); final Tile b11Tile = getSourceTile(b11Band, targetRectangle); - correctCoastalAndUrbanClouds(contextRectangle, pixelStateRectangle, urbanRectangle, cdiRectangle, targetRectangle, - b7Tile, b2Tile, b8Tile, b8aTile, b11Tile, sourceFlagTile, targetTile); + correctCoastalAndUrbanClouds(contextRectangle, + pixelStateRectangle, urbanRectangle, cdiRectangle, cdiExtendedRectangle, + targetRectangle, + b2Tile, b7Tile, b8Tile, b8aTile, b11Tile, sourceFlagTile, targetTile); // cloud buffer is based on the corrected clouds - addCloudBuffer(targetRectangle, cloudBufferRectangle, sourceFlagTile, targetTile); + addCloudBuffer(cloudBufferExtendedRectangle, cloudBufferRectangle, targetRectangle, sourceFlagTile, targetTile); } - private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, Rectangle pixelStateRectangle, Rectangle urbanRectangle, Rectangle cdiRectangle, Rectangle targetRectangle, Tile b7Tile, Tile b2Tile, Tile b8Tile, Tile b8aTile, Tile b11Tile, Tile sourceFlagTile, Tile targetTile) { + private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, + Rectangle pixelStateRectangle, Rectangle urbanRectangle, + Rectangle cdiRectangle, Rectangle cdiExtendedRectangle, + Rectangle targetRectangle, + Tile b2Tile, Tile b7Tile, Tile b8Tile, Tile b8aTile, Tile b11Tile, + Tile sourceFlagTile, Tile targetTile) { // allocate and initialise some lines of full width to collect pixel contributions // The lines will be re-used for later image lines in a rolling manner. final boolean[][] landAccu = new boolean[contextSize][targetRectangle.width]; final boolean[][] waterAccu = new boolean[contextSize][targetRectangle.width]; final boolean[][] urbanSomeClearAccu = new boolean[contextSize][targetRectangle.width]; - final double m7[][] = new double[contextSize][targetRectangle.width]; - final double c7[][] = new double[contextSize][targetRectangle.width]; - final double m8[][] = new double[contextSize][targetRectangle.width]; - final double c8[][] = new double[contextSize][targetRectangle.width]; - final short n78[][] = new short[contextSize][targetRectangle.width]; + final double[][] m7 = new double[contextSize][targetRectangle.width]; + final double[][] c7 = new double[contextSize][targetRectangle.width]; + final double[][] m8 = new double[contextSize][targetRectangle.width]; + final double[][] c8 = new double[contextSize][targetRectangle.width]; + final short[][] n78 = new short[contextSize][targetRectangle.width]; // loop over extended source tile for (int y = contextRectangle.y; y < contextRectangle.y + contextRectangle.height; y++) { @@ -145,31 +163,55 @@ private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, Rectangle // write dilated land and water patch into accu, required for coastal cloud distinction if (pixelStateRectangle.contains(x, y)) { if (isLand(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, true, landAccu); + fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, + true, landAccu); } if (isWater(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, true, waterAccu); + fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, + true, waterAccu); } } // write 11x11 "filtered" non-cloud patch into accu, required for urban cloud distinction if (urbanRectangle.contains(x, y)) { if (!isCloud(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, targetRectangle, urbanContextRadius, contextSize, true, urbanSomeClearAccu); + fillPatchInAccu(y, x, targetRectangle, urbanContextRadius, contextSize, + true, urbanSomeClearAccu); } } // add stddev contributions to accu of sums, squares, counts, required for urban cloud distinction - if (cdiRectangle.contains(x, y)) { - final float b7 = b7Tile.getSampleFloat(x, y); - final float b8 = b8Tile.getSampleFloat(x, y); - final float b8a = b8aTile.getSampleFloat(x, y); - if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { - final float b7b8a = b7 / b8a; - final float b8b8a = b8 / b8a; - final float b7b8a2 = b7b8a * b7b8a; - final float b8b8a2 = b8b8a * b8b8a; - addStddevPatchInAccu(y, x, targetRectangle, cdiStddevContextRadius, b8b8a, b7b8a2, b8b8a2, b7b8a, m7, c7, m8, c8, n78); + if (BORDER_COPY) { + if (cdiExtendedRectangle.contains(x, y)) { + final int xt = + x < cdiRectangle.x ? cdiRectangle.x + : x >= cdiRectangle.x + cdiRectangle.width ? cdiRectangle.x + cdiRectangle.width - 1 + : x; + final int yt = + y < cdiRectangle.y ? cdiRectangle.y + : y >= cdiRectangle.y + cdiRectangle.height ? cdiRectangle.y + cdiRectangle.height - 1 + : y; + final float b7 = b7Tile.getSampleFloat(xt, yt); + final float b8 = b8Tile.getSampleFloat(xt, yt); + final float b8a = b8aTile.getSampleFloat(xt, yt); + if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { + final float b7b8a = b7 / b8a; + final float b8b8a = b8 / b8a; + addStddevPatchInAccu(y, x, targetRectangle, cdiStddevContextRadius, b7b8a, b8b8a, + m7, c7, m8, c8, n78); + } + } + } else { + if (cdiRectangle.contains(x, y)) { + final float b7 = b7Tile.getSampleFloat(x, y); + final float b8 = b8Tile.getSampleFloat(x, y); + final float b8a = b8aTile.getSampleFloat(x, y); + if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { + final float b7b8a = b7 / b8a; + final float b8b8a = b8 / b8a; + addStddevPatchInAccu(y, x, targetRectangle, cdiStddevContextRadius, b7b8a, b8b8a, + m7, c7, m8, c8, n78); + } } } @@ -177,21 +219,26 @@ private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, Rectangle // target pixel yt/xt final int yt = y - contextRadius; final int xt = x - contextRadius; - // accu position jt/it - final int jt = (yt - targetRectangle.y) % contextSize; + // tile relative pixel, already mapped to accu position + final int jjt = (yt - targetRectangle.y) % contextSize; final int it = xt - targetRectangle.x; if (targetRectangle.contains(xt, yt)) { // read pixel classif flags from source, apply corrections, write it to target int pixelClassifFlags = sourceFlagTile.getSampleInt(xt, yt); - pixelClassifFlags = coastalCloudDistinction(yt, xt, jt, it, landAccu, waterAccu, sourceFlagTile, b2Tile, b8Tile, b11Tile, pixelClassifFlags); - pixelClassifFlags = urbanCloudDistinction(jt, it, urbanSomeClearAccu, m7, m8, c7, c8, n78, pixelClassifFlags); + pixelClassifFlags = coastalCloudDistinction(yt, xt, jjt, it, landAccu, waterAccu, + sourceFlagTile, b2Tile, b8Tile, b11Tile, + pixelClassifFlags); + pixelClassifFlags = urbanCloudDistinction(jjt, it, urbanSomeClearAccu, + m7, m8, c7, c8, n78, + pixelClassifFlags); targetTile.setSample(xt, yt, pixelClassifFlags); } } } } - private void fillPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, int contextSize, boolean value, boolean[][] accu) { + private void fillPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, int contextSize, + boolean value, boolean[][] accu) { // reduce patch to part overlapping with target image final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); final int jMax = Math.min(y - targetRectangle.y + 1 + halfWidth, targetRectangle.height); @@ -207,7 +254,7 @@ private void fillPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWi } private void addStddevPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, - float b8b8a, float b7b8a2, float b8b8a2, float b7b8a, + float b7b8a, float b8b8a, double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78) { // reduce patch to part overlapping with target image final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); @@ -215,7 +262,9 @@ private void addStddevPatchInAccu(int y, int x, Rectangle targetRectangle, int h final int iMin = Math.max(x - targetRectangle.x - halfWidth, 0); final int iMax = Math.min(x - targetRectangle.x + 1 + halfWidth, targetRectangle.width); // add to mean and to sum of squares within patch - for (int j = jMin; j < jMax; ++j) { + final float b7b8a2 = b7b8a * b7b8a; + final float b8b8a2 = b8b8a * b8b8a; + for (int j = jMin; j < jMax; ++j) { final int jj = j % contextSize; for (int i = iMin; i < iMax; ++i) { m7[jj][i] += b7b8a; @@ -227,14 +276,14 @@ private void addStddevPatchInAccu(int y, int x, Rectangle targetRectangle, int h } } - private static int coastalCloudDistinction(int yt, int xt, int jt, int it, + private static int coastalCloudDistinction(int yt, int xt, int jjt, int it, boolean[][] landAccu, boolean[][] waterAccu, Tile sourceFlagTile, Tile b2Tile, Tile b8Tile, Tile b11Tile, int pixelClassifFlags) { // not land but there is some land nearby, or not water and some water nearby final boolean isCoastal = - (!isLand(sourceFlagTile, yt, xt) && landAccu[jt][it]) || - (!isWater(sourceFlagTile, yt, xt) && waterAccu[jt][it]); + (!isLand(sourceFlagTile, yt, xt) && landAccu[jjt][it]) || + (!isWater(sourceFlagTile, yt, xt) && waterAccu[jjt][it]); if (isCoastal) { // another cloud test final float b2 = b2Tile.getSampleFloat(xt, yt); @@ -245,65 +294,79 @@ private static int coastalCloudDistinction(int yt, int xt, int jt, int it, final boolean notCoastalCloud = idx1 > 0.7f || (idx1 > 0.6f && idx2 > 0.9f); if (notCoastalCloud) { // clear cloud flags if cloud test fails - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS); - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_SURE); - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_SURE, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); } else if ((pixelClassifFlags & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE))) == 0) { // align cloud flag with combination of ambiguous and sure if none of them is set - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); } else { // align cloud flag with combination of ambiguous and sure if one of them is set pixelClassifFlags |= IDEPIX_CLOUD_BIT; } } // reset accu for re-use - landAccu[jt][it] = false; - waterAccu[jt][it] = false; + landAccu[jjt][it] = false; + waterAccu[jjt][it] = false; return pixelClassifFlags; } - private static int urbanCloudDistinction(int jt, int it, boolean[][] urbanSomeClearAccu, double[][] m7, double[][] m8, double[][] c7, double[][] c8, short[][] n78, int pixelClassifFlags) { + private static int urbanCloudDistinction(int jjt, int it, + boolean[][] urbanSomeClearAccu, + double[][] m7, double[][] m8, double[][] c7, double[][] c8, short[][] n78, + int pixelClassifFlags) { // some clear pixels nearby, and not cirrus or water - final boolean notInTheMiddleOfACloud = urbanSomeClearAccu[jt][it]; + final boolean notInTheMiddleOfACloud = urbanSomeClearAccu[jjt][it]; if (notInTheMiddleOfACloud && (pixelClassifFlags & (IDEPIX_CIRRUS_AMBIGUOUS_BIT | IDEPIX_CIRRUS_SURE_BIT | IDEPIX_WATER_BIT)) == 0 && (pixelClassifFlags & (IDEPIX_CLOUD_AMBIGUOUS_BIT | IDEPIX_CLOUD_SURE_BIT | IDEPIX_CLOUD_BIT)) != 0) { // another non-cloud test - final double variance7 = variance_of(c7[jt][it], m7[jt][it], n78[jt][it]); - final double variance8 = variance_of(c8[jt][it], m8[jt][it], n78[jt][it]); + final double variance7 = variance_of(c7[jjt][it], m7[jjt][it], n78[jjt][it]); + final double variance8 = variance_of(c8[jjt][it], m8[jjt][it], n78[jjt][it]); final double cdiValue = (variance7 - variance8) / (variance7 + variance8); if (cdiValue >= CDI_THRESHOLD) { // clear cloud flags if CDI test succeeds - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS); - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD_SURE); - pixelClassifFlags = removeBit(pixelClassifFlags, IDEPIX_CLOUD); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_SURE, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); } } // clear accu for re-use - urbanSomeClearAccu[jt][it] = false; + urbanSomeClearAccu[jjt][it] = false; + c7[jjt][it] = 0.0; + m7[jjt][it] = 0.0; + c8[jjt][it] = 0.0; + m8[jjt][it] = 0.0; + n78[jjt][it] = 0; return pixelClassifFlags; } - private void addCloudBuffer(Rectangle targetRectangle, Rectangle cloudBufferRectangle, Tile sourceFlagTile, Tile targetTile) { + private void addCloudBuffer(Rectangle cloudBufferExtendedRectangle, Rectangle cloudBufferRectangle, + Rectangle targetRectangle, Tile sourceFlagTile, Tile targetTile) { final boolean[][] cloudBufferAccu = new boolean[cloudBufferSize][targetRectangle.width]; - for (int y = cloudBufferRectangle.y; y < cloudBufferRectangle.y + cloudBufferRectangle.height; y++) { + for (int y = cloudBufferExtendedRectangle.y; y < cloudBufferExtendedRectangle.y + cloudBufferExtendedRectangle.height; y++) { checkForCancellation(); - for (int x = cloudBufferRectangle.x; x < cloudBufferRectangle.x + cloudBufferRectangle.width; x++) { - if (isCloudForBuffer(targetRectangle.contains(x, y) ? targetTile : sourceFlagTile, x, y)) { - fillPatchInAccu(y, x, targetRectangle, cloudBufferWidth, cloudBufferSize, true, cloudBufferAccu); + for (int x = cloudBufferExtendedRectangle.x; x < cloudBufferExtendedRectangle.x + cloudBufferExtendedRectangle.width; x++) { + if (cloudBufferRectangle.contains(x, y)) { + if (isCloudForBuffer(targetRectangle.contains(x, y) ? targetTile : sourceFlagTile, x, y)) { + fillPatchInAccu(y, x, targetRectangle, cloudBufferWidth, cloudBufferSize, true, cloudBufferAccu); + } } // target pixel yt/xt final int yt = y - cloudBufferWidth; final int xt = x - cloudBufferWidth; - // accu position jt/it - final int jt = (yt - targetRectangle.y) % cloudBufferSize; + // accu position jjt/it + final int jjt = (yt - targetRectangle.y) % cloudBufferSize; final int it = xt - targetRectangle.x; // evaluate accu if we have enough context // set cloud buffer flag if it is not cloud but there is a cloud nearby - if (targetRectangle.contains(xt, yt) - && cloudBufferAccu[jt][it] + if (targetRectangle.contains(xt, yt)) { + if (cloudBufferAccu[jjt][it] && (targetTile.getSampleInt(xt, yt) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD))) == 0) { - targetTile.setSample(xt, yt, IDEPIX_CLOUD_BUFFER, true); + targetTile.setSample(xt, yt, IDEPIX_CLOUD_BUFFER, true); + } + // clear accu for re-use + cloudBufferAccu[jjt][it] = false; } } } @@ -326,11 +389,6 @@ private static Product createTargetProduct(Product sourceProduct, String name, S return targetProduct; } - private static int removeBit(int x, int i) { - final int mask = -1 << i; - return ((x ^ (x >>> 1)) & mask) ^ x; - } - private static boolean isLand(Tile tile, int y, int x) { return tile.getSampleBit(x, y, IDEPIX_LAND); } @@ -343,15 +401,16 @@ private static boolean isCloud(Tile tile, int y, int x) { return (tile.getSampleInt(x, y) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD))) != 0; } - private static double variance_of(double c, double m, int n) { - return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / (n - 1); - } - private boolean isCloudForBuffer(Tile targetTile, int x, int y) { return targetTile.getSampleBit(x, y, IDEPIX_CLOUD_SURE) || (computeCloudBufferForCloudAmbiguous && targetTile.getSampleBit(x, y, IDEPIX_CLOUD_AMBIGUOUS)); } + private static double variance_of(double c, double m, int n) { + //return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / (n - 1); + return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / n; + } + public static class Spi extends OperatorSpi { public Spi() { From cdf9b57275992eee0d1bfc2360c6a62f6860fd88 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Mon, 6 Feb 2023 22:21:52 +0100 Subject: [PATCH 03/12] implementation of OLCI CTP with a single call into Tensorflow as recommended by Ralf --- .../java/org/esa/snap/idepix/olci/CtpOp.java | 38 ++++++++++----- .../idepix/olci/TensorflowNNCalculator.java | 46 +++++++++++++++++-- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/CtpOp.java b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/CtpOp.java index 7c874d35..f60baf86 100644 --- a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/CtpOp.java +++ b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/CtpOp.java @@ -139,8 +139,11 @@ public void doExecute(ProgressMonitor pm) throws OperatorException { @Override public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) throws OperatorException { + if (!"ctp".equals(targetBand.getName())) { + throw new OperatorException("Unexpected target band name: '" + targetBand.getName() + "' - exiting."); + } + final Rectangle targetRectangle = targetTile.getRectangle(); - final String targetBandName = targetBand.getName(); final Tile szaTile = getSourceTile(szaBand, targetRectangle); final Tile ozaTile = getSourceTile(ozaBand, targetRectangle); @@ -154,6 +157,8 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th final Tile l1FlagsTile = getSourceTile(sourceProduct.getRasterDataNode("quality_flags"), targetRectangle); + float[][] nnInputs = new float[targetRectangle.height * targetRectangle.width][]; + float[] dummyNnInput = new float[7]; for (int y = targetRectangle.y; y < targetRectangle.y + targetRectangle.height; y++) { checkForCancellation(); for (int x = targetRectangle.x; x < targetRectangle.x + targetRectangle.width; x++) { @@ -180,22 +185,33 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th final float tra15 = tra15Tile.getSampleFloat(x, y); final float mLogTra15 = (float) -Math.log(tra15); - float[] nnInput = new float[]{cosSza, cosOza, aziDiff, refl12, mLogTra13, mLogTra14, mLogTra15}; - final float[][] nnResult = nnCalculator.calculate(nnInput); - final float ctp = TensorflowNNCalculator.convertNNResultToCtp(nnResult[0][0]); + float[] nnInput = new float[] {cosSza, cosOza, aziDiff, refl12, mLogTra13, mLogTra14, mLogTra15}; + //final float[][] nnResult = nnCalculator.calculate(nnInput); + //final float ctp = TensorflowNNCalculator.convertNNResultToCtp(nnResult[0][0]); + //targetTile.setSample(x, y, ctp); + nnInputs[(y-targetRectangle.y) * targetRectangle.width + (x-targetRectangle.x)] = nnInput; + } else { + //targetTile.setSample(x, y, Float.NaN); + nnInputs[(y-targetRectangle.y) * targetRectangle.width + (x-targetRectangle.x)] = dummyNnInput; + } + } + } + + // call tensorflow once with the complete tile stack + final float[][] nnResult = nnCalculator.calculate1(nnInputs); - if (targetBandName.equals("ctp")) { - targetTile.setSample(x, y, ctp); - } else { - throw new OperatorException("Unexpected target band name: '" + - targetBandName + "' - exiting."); - } + // convert output of tf into ctp and set value into target tile + for (int y = targetRectangle.y; y < targetRectangle.y + targetRectangle.height; y++) { + checkForCancellation(); + for (int x = targetRectangle.x; x < targetRectangle.x + targetRectangle.width; x++) { + final boolean pixelIsValid = !l1FlagsTile.getSampleBit(x, y, IdepixOlciConstants.L1_F_INVALID); + if (pixelIsValid) { + targetTile.setSample(x, y, TensorflowNNCalculator.convertNNResultToCtp(nnResult[(y-targetRectangle.y) * targetRectangle.width + (x-targetRectangle.x)][0])); } else { targetTile.setSample(x, y, Float.NaN); } } } - } private void preProcess() { diff --git a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/TensorflowNNCalculator.java b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/TensorflowNNCalculator.java index e2a87f4e..94f74bbc 100644 --- a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/TensorflowNNCalculator.java +++ b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/TensorflowNNCalculator.java @@ -40,9 +40,11 @@ class TensorflowNNCalculator { try { TensorFlow.version(); // triggers init of TensorFlow } catch (LinkageError e) { - throw new IllegalStateException("TensorFlow could not be initialised. " + - "Make sure that your CPU supports 64Bit and AVX instruction set " + - "(Are you using a VM?) and that you have installed the Microsoft Visual C++ 2015 Redistributable when you are on windows.", e); + throw new IllegalStateException( + "TensorFlow could not be initialised. " + + "Make sure that your CPU supports 64Bit and AVX instruction set " + + "(Are you using a VM?) and that you have installed the Microsoft Visual C++ 2015 " + + "Redistributable when you are on windows.", e); } this.transformMethod = transformMethod; @@ -181,7 +183,6 @@ private void loadModel() throws Exception { * Requires that loadModel() is run once before. * * @param nnInput - input vector for neural net - * * @return float[][] - the converted result array */ float[][] calculate(float[] nnInput) { @@ -210,5 +211,42 @@ float[][] calculate(float[] nnInput) { } } + /** + * Applies NN to vector of pixel band stacks and returns converted array. + * Functional implementation of setNnTensorInput(.) plus getNNResult(). + * Makes sure the Tensors are closed after use. + * Requires that loadModel() is run once before. + * + * @param nnInput - image vector of band vectors, band vectors are input for neural net + * @return float[][] - image vector of output band vector (length 1) + */ + float[][] calculate1(float[][] nnInput) { + if (transformMethod.equals("sqrt")) { + for (int i = 0; i < nnInput.length; i++) { + for (int j = 0; j < nnInput[i].length; j++) { + nnInput[i][j] = (float) Math.sqrt(nnInput[i][j]); + } + } + } else if (transformMethod.equals("log")) { + for (int i = 0; i < nnInput.length; i++) { + for (int j = 0; j < nnInput[i].length; j++) { + nnInput[i][j] = (float) Math.log10(nnInput[i][j]); + } + } + } + final Session.Runner runner = model.session().runner(); + try ( + Tensor inputTensor = Tensor.create(nnInput); + Tensor outputTensor = runner.feed(firstNodeName, inputTensor).fetch(lastNodeName).run().get(0) + ) { + long[] ts = outputTensor.shape(); + int numPixels = (int) ts[0]; + int numOutputVars = (int) ts[1]; + float[][] m = new float[numPixels][numOutputVars]; + outputTensor.copyTo(m); + return m; + } + } + } From 3391b645dc4f407641d7866407ac9f1ed2c54c87 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Tue, 7 Feb 2023 09:17:13 +0100 Subject: [PATCH 04/12] implementation of OLCI CTP with a single call to Tensorflow, increased version --- idepix-olci/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idepix-olci/pom.xml b/idepix-olci/pom.xml index 0768c0ff..ffb50a0c 100644 --- a/idepix-olci/pom.xml +++ b/idepix-olci/pom.xml @@ -26,7 +26,7 @@ idepix-olci - 9.0.2-SNAPSHOT + 9.0.3-SNAPSHOT nbm From 8f0c624edc42c2d396618440e3f834e6531a63a4 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Fri, 10 Feb 2023 09:44:50 +0100 Subject: [PATCH 05/12] make coastal and urban cloud correction robust against invalid and NaN in spatial neighbourhood --- .../operators/S2IdepixCloudPostProcess2Op.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java index 1a77bced..acde77ef 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java @@ -174,7 +174,7 @@ private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, // write 11x11 "filtered" non-cloud patch into accu, required for urban cloud distinction if (urbanRectangle.contains(x, y)) { - if (!isCloud(sourceFlagTile, y, x)) { + if (!mayBeCloud(sourceFlagTile, y, x)) { fillPatchInAccu(y, x, targetRectangle, urbanContextRadius, contextSize, true, urbanSomeClearAccu); } @@ -284,15 +284,17 @@ private static int coastalCloudDistinction(int yt, int xt, int jjt, int it, final boolean isCoastal = (!isLand(sourceFlagTile, yt, xt) && landAccu[jjt][it]) || (!isWater(sourceFlagTile, yt, xt) && waterAccu[jjt][it]); - if (isCoastal) { + if (isCoastal && (pixelClassifFlags & (1 << IDEPIX_INVALID)) == 0) { // another cloud test final float b2 = b2Tile.getSampleFloat(xt, yt); final float b8 = b8Tile.getSampleFloat(xt, yt); final float b11 = b11Tile.getSampleFloat(xt, yt); final float idx1 = b2 / b11; final float idx2 = b8 / b11; - final boolean notCoastalCloud = idx1 > 0.7f || (idx1 > 0.6f && idx2 > 0.9f); - if (notCoastalCloud) { + //final boolean notCoastal = idx1 > 0.7f || (idx1 > 0.6f && idx2 > 0.9f); + // handles NaN as non-coastal + final boolean isCoastal2 = idx1 <= 0.6f || (idx1 <= 0.7f && idx2 <= 0.9f); + if (isCoastal2) { // clear cloud flags if cloud test fails pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS, false); pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_SURE, false); @@ -302,7 +304,7 @@ private static int coastalCloudDistinction(int yt, int xt, int jjt, int it, pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); } else { // align cloud flag with combination of ambiguous and sure if one of them is set - pixelClassifFlags |= IDEPIX_CLOUD_BIT; + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, true);; } } // reset accu for re-use @@ -362,7 +364,7 @@ private void addCloudBuffer(Rectangle cloudBufferExtendedRectangle, Rectangle cl // set cloud buffer flag if it is not cloud but there is a cloud nearby if (targetRectangle.contains(xt, yt)) { if (cloudBufferAccu[jjt][it] - && (targetTile.getSampleInt(xt, yt) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD))) == 0) { + && (targetTile.getSampleInt(xt, yt) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD) | (1 << IDEPIX_INVALID))) == 0) { targetTile.setSample(xt, yt, IDEPIX_CLOUD_BUFFER, true); } // clear accu for re-use @@ -397,8 +399,8 @@ private static boolean isWater(Tile tile, int y, int x) { return tile.getSampleBit(x, y, IDEPIX_WATER); } - private static boolean isCloud(Tile tile, int y, int x) { - return (tile.getSampleInt(x, y) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD))) != 0; + private static boolean mayBeCloud(Tile tile, int y, int x) { + return (tile.getSampleInt(x, y) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD) | 1 << IDEPIX_INVALID)) != 0; } private boolean isCloudForBuffer(Tile targetTile, int x, int y) { From b3d058180028397701d972cbe091593c6faaad1a Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Fri, 10 Feb 2023 09:45:38 +0100 Subject: [PATCH 06/12] correct option for debug output of CDI band, but comment out for operational use --- .../snap/idepix/s2msi/UrbanCloudDistinction.java | 12 ++++++++---- .../operators/S2IdepixCloudPostProcessOp.java | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/UrbanCloudDistinction.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/UrbanCloudDistinction.java index a1f687ec..07b14ee2 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/UrbanCloudDistinction.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/UrbanCloudDistinction.java @@ -163,17 +163,21 @@ public void correctCloudFlag(int x, int y, Tile classifFlagTile, Tile targetFlag } + public float getCdiValue(int x, int y) { + return cdiBand.getSampleFloat(x, y); + } + /** * Adds debug band to the target product. */ - public void addDebugBandsToTargetProduct() { - ucd_oldCloud_debug = s2ClassifProduct.addBand("__ucd_oldCloud__", ProductData.TYPE_INT8); + public void addDebugBandsToTargetProduct(Product targetProduct) { + ucd_oldCloud_debug = targetProduct.addBand("__ucd_oldCloud__", ProductData.TYPE_INT8); ucd_oldCloud_debug.ensureRasterData(); - ucd_cdi_debug = s2ClassifProduct.addBand("__ucd_cdi__", ProductData.TYPE_FLOAT32); + ucd_cdi_debug = targetProduct.addBand("__ucd_cdi__", ProductData.TYPE_FLOAT32); ucd_cdi_debug.setNoDataValue(Float.NaN); ucd_cdi_debug.ensureRasterData(); - ucd_cloudMean11_debug = s2ClassifProduct.addBand("__ucd_cloudMean11__", ProductData.TYPE_FLOAT32); + ucd_cloudMean11_debug = targetProduct.addBand("__ucd_cloudMean11__", ProductData.TYPE_FLOAT32); ucd_cloudMean11_debug.setNoDataValue(Float.NaN); ucd_cloudMean11_debug.ensureRasterData(); debugBandsEnabled = true; diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcessOp.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcessOp.java index fdca4f7b..c02085ab 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcessOp.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcessOp.java @@ -67,6 +67,7 @@ public void initialize() throws OperatorException { coastalCloudDistinction = new CoastalCloudDistinction(classifiedProduct); urbanCloudDistinction = new UrbanCloudDistinction(classifiedProduct); + //urbanCloudDistinction.addDebugBandsToTargetProduct(cloudBufferProduct); setTargetProduct(cloudBufferProduct); } @@ -89,6 +90,19 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th Rectangle targetRectangle = targetTile.getRectangle(); final Rectangle srcRectangle = rectCalculator.extend(targetRectangle); +// if (! "pixel_classif_flags".equals(targetBand.getName())) { +// if ("__ucd_cdi__".equals(targetBand.getName())) { +// for (int y = srcRectangle.y; y < srcRectangle.y + srcRectangle.height; y++) { +// for (int x = srcRectangle.x; x < srcRectangle.x + srcRectangle.width; x++) { +// if (targetRectangle.contains(x, y)) { +// targetTile.setSample(x, y, urbanCloudDistinction.getCdiValue(x, y)); +// } +// } +// } +// } +// return; +// } + final Tile sourceFlagTile = getSourceTile(origClassifFlagBand, srcRectangle); for (int y = srcRectangle.y; y < srcRectangle.y + srcRectangle.height; y++) { From 37ab8e0bc40bc03fea1df3da0eb97f17162d339b Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Fri, 10 Feb 2023 23:01:09 +0100 Subject: [PATCH 07/12] integration of cloud buffer into single-pass computation of cloud post-processing --- .../org/esa/snap/idepix/s2msi/S2IdepixOp.java | 4 +- .../S2IdepixCloudPostProcess2Op.java | 539 +++++++++++------- 2 files changed, 319 insertions(+), 224 deletions(-) diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java index c36908b1..42868aff 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java @@ -109,9 +109,6 @@ private void processSentinel2() { int cacheSize = Integer.parseInt(System.getProperty(S2IdepixUtils.TILECACHE_PROPERTY, "1600")); s2ClassifProduct = S2IdepixUtils.computeTileCacheProduct(s2ClassifProduct, cacheSize); - targetProduct = s2ClassifProduct; - if (true) return; - // Post Cloud Classification: cloud shadow, cloud buffer, mountain shadow Product postProcessingProduct = computePostProcessProduct(sourceProduct, s2ClassifProduct); @@ -152,6 +149,7 @@ private Product computePostProcessProduct(Product l1cProduct, Product classifica paramsBuffer.put("computeCloudBufferForCloudAmbiguous", computeCloudBufferForCloudAmbiguous); //Product cloudBufferProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(S2IdepixCloudPostProcessOp.class), // paramsBuffer, input); + paramsBuffer.put("computeCloudBuffer", computeCloudBuffer); Product cloudBufferProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(S2IdepixCloudPostProcess2Op.class), paramsBuffer, input); diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java index acde77ef..103f3f3d 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java @@ -20,7 +20,7 @@ import static org.esa.snap.idepix.s2msi.util.S2IdepixConstants.*; /** - * Adds a cloud buffer to cloudy pixels and does a cloud discrimination on urban areas. + * Adds a cloud buffer to cloudy pixels and does a cloud discrimination on coastal areas and urban areas. */ @OperatorMetadata(alias = "Idepix.S2CloudPostProcess2", version = "3.0", @@ -30,24 +30,26 @@ description = "Performs post processing of cloudy pixels and adds optional cloud buffer.") public class S2IdepixCloudPostProcess2Op extends Operator { - private static final int IDEPIX_CLOUD_AMBIGUOUS_BIT = 1 << IDEPIX_CLOUD_AMBIGUOUS; - private static final int IDEPIX_CLOUD_SURE_BIT = 1 << IDEPIX_CLOUD_SURE; - private static final int IDEPIX_CLOUD_BIT = 1 << IDEPIX_CLOUD; - private static final int IDEPIX_CIRRUS_AMBIGUOUS_BIT = 1 << IDEPIX_CIRRUS_AMBIGUOUS; - private static final int IDEPIX_CIRRUS_SURE_BIT = 1 << IDEPIX_CIRRUS_SURE; - private static final int IDEPIX_WATER_BIT = 1 << IDEPIX_WATER; - - private static final boolean BORDER_COPY = true; - @SourceProduct(alias = "classifiedProduct") private Product classifiedProduct; + @Parameter(defaultValue = "true", label = "Compute a cloud buffer") + private boolean computeCloudBuffer; + @Parameter(defaultValue = "true", label = "Compute cloud buffer for cloud ambiguous pixels too.") private boolean computeCloudBufferForCloudAmbiguous; @Parameter(defaultValue = "2", label = "Width of cloud buffer (# of pixels)") private int cloudBufferWidth; + // variables set in initialize and used in computeTile + private Band origClassifFlagBand; + private Band b2Band; + private Band b7Band; + private Band b8Band; + private Band b8aBand; + private Band b11Band; + private int landWaterContextSize; private int urbanContextSize = 11; private int cdiStddevContextSize = 7; @@ -59,232 +61,237 @@ public class S2IdepixCloudPostProcess2Op extends Operator { private int cdiStddevContextRadius = cdiStddevContextSize / 2; private int contextRadius; - private static final float COAST_BUFFER_SIZE = 1000f; - private Band origClassifFlagBand; - private Band b2Band; - private Band b7Band; - private Band b8Band; - private Band b8aBand; - private Band b11Band; private RectangleExtender pixelStateRectCalculator; private RectangleExtender urbanRectCalculator; private RectangleExtender cdiRectCalculator; - private RectangleExtender contextRectCalculator; private RectangleExtender cloudBufferRectCalculator; + private static final float COAST_BUFFER_SIZE = 1000f; // [m] private static final double CDI_THRESHOLD = -0.5; + /** + * Creates the output product with a pixel_classif_flags band, gets bands of the input product, and + * determines sizes for buffers. It prepares rectangle calculators that limit the rectangle + * to the image at the border even if source tile is extended. + * + * @throws OperatorException + */ @Override public void initialize() throws OperatorException { - final int resolution = S2IdepixUtils.determineResolution(classifiedProduct); - landWaterContextSize = (int) Math.floor((2 * COAST_BUFFER_SIZE) / resolution); // TODO is a +1 required here? - landWaterContextRadius = landWaterContextSize / 2; - contextSize = Math.max(landWaterContextSize, Math.max(urbanContextSize, cdiStddevContextSize)); - contextRadius = contextSize / 2; - cloudBufferSize = 2 * cloudBufferWidth + 1; - - pixelStateRectCalculator = createRectCalculator(contextRadius); - urbanRectCalculator = createRectCalculator(urbanContextRadius); - cdiRectCalculator = createRectCalculator(cdiStddevContextRadius); - contextRectCalculator = createRectCalculator(contextRadius); - cloudBufferRectCalculator = createRectCalculator(cloudBufferWidth); - Product cloudBufferProduct = createTargetProduct(classifiedProduct, classifiedProduct.getName(), classifiedProduct.getProductType()); - origClassifFlagBand = classifiedProduct.getBand(IDEPIX_CLASSIF_FLAGS); ProductUtils.copyBand(IDEPIX_CLASSIF_FLAGS, classifiedProduct, cloudBufferProduct, false); + setTargetProduct(cloudBufferProduct); + + origClassifFlagBand = classifiedProduct.getBand(IDEPIX_CLASSIF_FLAGS); b2Band = classifiedProduct.getBand("B2"); b7Band = classifiedProduct.getBand("B7"); b8Band = classifiedProduct.getBand("B8"); b8aBand = classifiedProduct.getBand("B8A"); b11Band = classifiedProduct.getBand("B11"); - ProductUtils.copyBand("pixel_classif_flags", classifiedProduct, - "flags_before_postprocessing", cloudBufferProduct, true); + // add previous state for debugging, comment out in operational version + //ProductUtils.copyBand("pixel_classif_flags", classifiedProduct, + // "flags_before_postprocessing", cloudBufferProduct, true); - setTargetProduct(cloudBufferProduct); + if (!computeCloudBuffer) { + cloudBufferWidth = 0; + } + + final int resolution = S2IdepixUtils.determineResolution(classifiedProduct); + landWaterContextSize = (int) Math.floor((2 * COAST_BUFFER_SIZE) / resolution); // TODO shall this be odd? + landWaterContextRadius = landWaterContextSize / 2; + contextSize = Math.max(landWaterContextSize, Math.max(urbanContextSize, cdiStddevContextSize)); + contextRadius = contextSize / 2; + cloudBufferSize = 2 * cloudBufferWidth + 1; + + pixelStateRectCalculator = createRectCalculator(contextRadius + cloudBufferWidth); + urbanRectCalculator = createRectCalculator(urbanContextRadius + cloudBufferWidth); + cdiRectCalculator = createRectCalculator(cdiStddevContextRadius + cloudBufferWidth); + cloudBufferRectCalculator = createRectCalculator(cloudBufferWidth); } + /** + * Corrects pixel classification and eliminates false cloud flags in coastal and in + * urban regions, adds cloud buffer. Cloud flags in mixed land-water areas are tested + * with a test based on B2, B8, B11. Cloud flags in regions with clear pixels nearby + * are tested with a test based on the spatial variance of a cloud displacement index + * using B7, B8, and B8A. + * + * This single-pass implementation uses several accumulators of the full tile width + * and a few lines to memorise and pre-compute various filtered values. Example: + * To determine whether there is a clear pixel in a 11x11 box around a pixel we use an + * accu with 11 lines. The accu is initialised with "false". A "clear" contribution + * writes a patch of 11x11 values "true" into the accu. This value will be used 5 lines + * and 5 rows later to determine whether there are clear pixels nearby. This is the last + * time the pixel may have got a clear contribution by some pixel passed in the + * single-pass. (Later pixels are more than 5 pixels away.) + * The accu lines are used in a rolling manner (with modulo for line selection). The + * values used are cleared for re-use. + * + * There are accumulators for + * * landNearby 33x33 (60m), 100x100 (20m), 200x200 (10m) + * * waterNearby 33x33 (60m), 100x100 (20m), 200x200 (10m) + * * clearNearby 11x11 + * * sum, square sum, and count of two band ratio expressions for the CDI + * * cloudBuffer 5x5 (cloudBufferWidth=2) + * For simplicity all the accus except cloudBuffer use the maximum number of lines that + * occurs (contextSize). This does not change the logic, just the buffer line used. + * + * Cloud buffer shall be determined based on the corrected cloud flags. It memorises + * the flag value to be written into the target image and delays its writing by cloud + * buffer width lines and columns. The flag may get updated by cloud pixels showing up + * one or two lines later. + * + * @param targetBand The target band, pixel_classif_flags. + * @param targetTile The current tile associated with the target band to be computed. + * @param pm A progress monitor which should be used to determine computation cancellation requests. + * @throws OperatorException + */ @Override public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) throws OperatorException { - Rectangle targetRectangle = targetTile.getRectangle(); + final Rectangle targetRectangle = targetTile.getRectangle(); final Rectangle pixelStateRectangle = pixelStateRectCalculator.extend(targetRectangle); final Rectangle urbanRectangle = urbanRectCalculator.extend(targetRectangle); final Rectangle cdiRectangle = cdiRectCalculator.extend(targetRectangle); final Rectangle cdiExtendedRectangle = new Rectangle(targetRectangle); - cdiExtendedRectangle.grow(cdiStddevContextRadius, cdiStddevContextRadius); - final Rectangle contextRectangle = new Rectangle(targetRectangle); - contextRectangle.grow(contextRadius, contextRadius); //contextRectCalculator.extend(targetRectangle); + cdiExtendedRectangle.grow(cdiStddevContextRadius + cloudBufferWidth, cdiStddevContextRadius + cloudBufferWidth); + final Rectangle contextExtendedRectangle = new Rectangle(targetRectangle); + contextExtendedRectangle.grow(contextRadius + cloudBufferWidth, contextRadius + cloudBufferWidth); final Rectangle cloudBufferRectangle = cloudBufferRectCalculator.extend(targetRectangle); - final Rectangle cloudBufferExtendedRectangle = new Rectangle(targetRectangle); - cloudBufferExtendedRectangle.grow(cloudBufferWidth, cloudBufferWidth); + // get source tiles with different extents final Tile sourceFlagTile = getSourceTile(origClassifFlagBand, pixelStateRectangle); final Tile b7Tile = getSourceTile(b7Band, cdiRectangle); final Tile b8Tile = getSourceTile(b8Band, cdiRectangle); final Tile b8aTile = getSourceTile(b8aBand, cdiRectangle); - final Tile b2Tile = getSourceTile(b2Band, targetRectangle); - final Tile b11Tile = getSourceTile(b11Band, targetRectangle); + final Tile b2Tile = getSourceTile(b2Band, cloudBufferRectangle); + final Tile b11Tile = getSourceTile(b11Band, cloudBufferRectangle); - correctCoastalAndUrbanClouds(contextRectangle, - pixelStateRectangle, urbanRectangle, cdiRectangle, cdiExtendedRectangle, - targetRectangle, - b2Tile, b7Tile, b8Tile, b8aTile, b11Tile, sourceFlagTile, targetTile); - - // cloud buffer is based on the corrected clouds - addCloudBuffer(cloudBufferExtendedRectangle, cloudBufferRectangle, targetRectangle, sourceFlagTile, targetTile); - } - - private void correctCoastalAndUrbanClouds(Rectangle contextRectangle, - Rectangle pixelStateRectangle, Rectangle urbanRectangle, - Rectangle cdiRectangle, Rectangle cdiExtendedRectangle, - Rectangle targetRectangle, - Tile b2Tile, Tile b7Tile, Tile b8Tile, Tile b8aTile, Tile b11Tile, - Tile sourceFlagTile, Tile targetTile) { - // allocate and initialise some lines of full width to collect pixel contributions + // allocate and initialise some lines of full width plus cloud buffer to collect pixel contributions // The lines will be re-used for later image lines in a rolling manner. - final boolean[][] landAccu = new boolean[contextSize][targetRectangle.width]; - final boolean[][] waterAccu = new boolean[contextSize][targetRectangle.width]; - final boolean[][] urbanSomeClearAccu = new boolean[contextSize][targetRectangle.width]; - final double[][] m7 = new double[contextSize][targetRectangle.width]; - final double[][] c7 = new double[contextSize][targetRectangle.width]; - final double[][] m8 = new double[contextSize][targetRectangle.width]; - final double[][] c8 = new double[contextSize][targetRectangle.width]; - final short[][] n78 = new short[contextSize][targetRectangle.width]; + final boolean[][] landNearbyAccu = new boolean[contextSize][cloudBufferRectangle.width]; + final boolean[][] waterNearbyAccu = new boolean[contextSize][cloudBufferRectangle.width]; + final boolean[][] clearNearbyAccu = new boolean[contextSize][cloudBufferRectangle.width]; + final double[][] m7 = new double[contextSize][cloudBufferRectangle.width]; + final double[][] c7 = new double[contextSize][cloudBufferRectangle.width]; + final double[][] m8 = new double[contextSize][cloudBufferRectangle.width]; + final double[][] c8 = new double[contextSize][cloudBufferRectangle.width]; + final short[][] n78 = new short[contextSize][cloudBufferRectangle.width]; + // the accu for pixelClassifFlags can be smaller because we only read it within the target tile + final int[][] cloudBufferAccu = new int[cloudBufferSize][targetRectangle.width]; // loop over extended source tile - for (int y = contextRectangle.y; y < contextRectangle.y + contextRectangle.height; y++) { + // y/x run from target tile y/x - context radius - cloud buffer width to ... + for (int y = contextExtendedRectangle.y; y < contextExtendedRectangle.y + contextExtendedRectangle.height; y++) { checkForCancellation(); - for (int x = contextRectangle.x; x < contextRectangle.x + contextRectangle.width; x++) { + for (int x = contextExtendedRectangle.x; x < contextExtendedRectangle.x + contextExtendedRectangle.width; x++) { // write dilated land and water patch into accu, required for coastal cloud distinction - if (pixelStateRectangle.contains(x, y)) { - if (isLand(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, - true, landAccu); - } - if (isWater(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, targetRectangle, landWaterContextRadius, contextSize, - true, waterAccu); - } + if (pixelStateRectangle.contains(x, y)) { + collectLand(y, x, sourceFlagTile, cloudBufferRectangle, landNearbyAccu); + collectWater(y, x, sourceFlagTile, cloudBufferRectangle, waterNearbyAccu); } // write 11x11 "filtered" non-cloud patch into accu, required for urban cloud distinction if (urbanRectangle.contains(x, y)) { - if (!mayBeCloud(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, targetRectangle, urbanContextRadius, contextSize, - true, urbanSomeClearAccu); - } + collectClear(y, x, sourceFlagTile, cloudBufferRectangle, clearNearbyAccu); } // add stddev contributions to accu of sums, squares, counts, required for urban cloud distinction - if (BORDER_COPY) { - if (cdiExtendedRectangle.contains(x, y)) { - final int xt = - x < cdiRectangle.x ? cdiRectangle.x - : x >= cdiRectangle.x + cdiRectangle.width ? cdiRectangle.x + cdiRectangle.width - 1 - : x; - final int yt = - y < cdiRectangle.y ? cdiRectangle.y - : y >= cdiRectangle.y + cdiRectangle.height ? cdiRectangle.y + cdiRectangle.height - 1 - : y; - final float b7 = b7Tile.getSampleFloat(xt, yt); - final float b8 = b8Tile.getSampleFloat(xt, yt); - final float b8a = b8aTile.getSampleFloat(xt, yt); - if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { - final float b7b8a = b7 / b8a; - final float b8b8a = b8 / b8a; - addStddevPatchInAccu(y, x, targetRectangle, cdiStddevContextRadius, b7b8a, b8b8a, - m7, c7, m8, c8, n78); - } - } - } else { - if (cdiRectangle.contains(x, y)) { - final float b7 = b7Tile.getSampleFloat(x, y); - final float b8 = b8Tile.getSampleFloat(x, y); - final float b8a = b8aTile.getSampleFloat(x, y); - if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { - final float b7b8a = b7 / b8a; - final float b8b8a = b8 / b8a; - addStddevPatchInAccu(y, x, targetRectangle, cdiStddevContextRadius, b7b8a, b8b8a, - m7, c7, m8, c8, n78); - } - } + if (cdiExtendedRectangle.contains(x, y)) { + collectCdiSumsAndSquares(y, x, b7Tile, b8Tile, b8aTile, cdiRectangle, cloudBufferRectangle, + m7, c7, m8, c8, n78); } - // evaluate accu if we have enough context - // target pixel yt/xt + // yt/xt is the target pixel where we have just seen the last pixel of its context final int yt = y - contextRadius; final int xt = x - contextRadius; - // tile relative pixel, already mapped to accu position - final int jjt = (yt - targetRectangle.y) % contextSize; - final int it = xt - targetRectangle.x; - if (targetRectangle.contains(xt, yt)) { - // read pixel classif flags from source, apply corrections, write it to target - int pixelClassifFlags = sourceFlagTile.getSampleInt(xt, yt); - pixelClassifFlags = coastalCloudDistinction(yt, xt, jjt, it, landAccu, waterAccu, - sourceFlagTile, b2Tile, b8Tile, b11Tile, - pixelClassifFlags); - pixelClassifFlags = urbanCloudDistinction(jjt, it, urbanSomeClearAccu, - m7, m8, c7, c8, n78, - pixelClassifFlags); - targetTile.setSample(xt, yt, pixelClassifFlags); + if (cloudBufferRectangle.contains(xt, yt)) { + correctFlagsOfPixel(yt, xt, + landNearbyAccu, waterNearbyAccu, clearNearbyAccu, + m7, c7, m8, c8, n78, + sourceFlagTile, b2Tile, b8Tile, b11Tile, + targetRectangle, cloudBufferRectangle, + cloudBufferAccu); + } + // yb/xb is the target pixel where we have seen just the last pixel of some possible cloud buffer + final int yb = yt - cloudBufferWidth; + final int xb = xt - cloudBufferWidth; + if (targetRectangle.contains(xb, yb)) { + writeFlagsOfPixel(yb, xb, cloudBufferAccu, targetRectangle, targetTile); } } } } - private void fillPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, int contextSize, - boolean value, boolean[][] accu) { - // reduce patch to part overlapping with target image - final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); - final int jMax = Math.min(y - targetRectangle.y + 1 + halfWidth, targetRectangle.height); - final int iMin = Math.max(x - targetRectangle.x - halfWidth, 0); - final int iMax = Math.min(x - targetRectangle.x + 1 + halfWidth, targetRectangle.width); - // fill patch with value - for (int j = jMin; j < jMax; ++j) { - final int jj = j % contextSize; - for (int i = iMin; i < iMax; ++i) { - accu[jj][i] = value; + private void correctFlagsOfPixel(int yt, int xt, + boolean[][] landAccu, boolean[][] waterAccu, boolean[][] clearNearbyAccu, + double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78, + Tile sourceFlagTile, Tile b2Tile, Tile b8Tile, Tile b11Tile, + Rectangle targetRectangle, Rectangle cloudBufferRectangle, + int[][] cloudBufferAccu) { + // land/water/urban accu pixel position + final int jt = (yt - cloudBufferRectangle.y) % contextSize; + final int it = xt - cloudBufferRectangle.x; + // read pixel classif flags from source, apply corrections + int pixelClassifFlags = sourceFlagTile.getSampleInt(xt, yt); + if (isValid(pixelClassifFlags)) { + pixelClassifFlags = coastalCloudDistinction(yt, xt, jt, it, landAccu, waterAccu, + sourceFlagTile, b2Tile, b8Tile, b11Tile, + pixelClassifFlags); + pixelClassifFlags = urbanCloudDistinction(jt, it, clearNearbyAccu, + m7, m8, c7, c8, n78, + pixelClassifFlags); + // patch cloud buffer into accu + if (computeCloudBuffer && isCloudForBuffer(pixelClassifFlags)) { + addCloudBufferInAccu(yt, xt, targetRectangle, cloudBufferWidth, cloudBufferAccu); } } - } - - private void addStddevPatchInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, - float b7b8a, float b8b8a, - double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78) { - // reduce patch to part overlapping with target image - final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); - final int jMax = Math.min(y - targetRectangle.y + 1 + halfWidth, targetRectangle.height); - final int iMin = Math.max(x - targetRectangle.x - halfWidth, 0); - final int iMax = Math.min(x - targetRectangle.x + 1 + halfWidth, targetRectangle.width); - // add to mean and to sum of squares within patch - final float b7b8a2 = b7b8a * b7b8a; - final float b8b8a2 = b8b8a * b8b8a; - for (int j = jMin; j < jMax; ++j) { - final int jj = j % contextSize; - for (int i = iMin; i < iMax; ++i) { - m7[jj][i] += b7b8a; - c7[jj][i] += b7b8a2; - m8[jj][i] += b8b8a; - c8[jj][i] += b8b8a2; - n78[jj][i] += 1; + // add cloud buffer flag from accu to pixelClassifFlags + // memorize p.c.f. in accu for later updates of cloud buffer flag and final writing + if (targetRectangle.contains(xt, yt)) { + final int jb = (yt - targetRectangle.y) % cloudBufferSize; + final int ib = xt - targetRectangle.x; + // read cloud buffer from accu (for clouds in upper left direction seen before) + if (isCloudBuffer(cloudBufferAccu[jb][ib]) && isClear(pixelClassifFlags)) { + pixelClassifFlags |= (1 << IDEPIX_CLOUD_BUFFER); } + // write corrected pixel classif flags to accu + cloudBufferAccu[jb][ib] = pixelClassifFlags; } + // reset accu for re-use + landAccu[jt][it] = false; + waterAccu[jt][it] = false; + clearNearbyAccu[jt][it] = false; + c7[jt][it] = 0.0; + m7[jt][it] = 0.0; + c8[jt][it] = 0.0; + m8[jt][it] = 0.0; + n78[jt][it] = 0; } - private static int coastalCloudDistinction(int yt, int xt, int jjt, int it, + private void writeFlagsOfPixel(int yb, int xb, int[][] cloudBufferAccu, Rectangle targetRectangle, Tile targetTile) { + // transfer accu to target + final int jb = (yb - targetRectangle.y) % cloudBufferSize; + final int ib = xb - targetRectangle.x; + targetTile.setSample(xb, yb, cloudBufferAccu[jb][ib]); + // clear accu for re-use + cloudBufferAccu[jb][ib] = 0; + } + + private static int coastalCloudDistinction(int yt, int xt, int jt, int it, boolean[][] landAccu, boolean[][] waterAccu, Tile sourceFlagTile, Tile b2Tile, Tile b8Tile, Tile b11Tile, int pixelClassifFlags) { // not land but there is some land nearby, or not water and some water nearby final boolean isCoastal = - (!isLand(sourceFlagTile, yt, xt) && landAccu[jjt][it]) || - (!isWater(sourceFlagTile, yt, xt) && waterAccu[jjt][it]); - if (isCoastal && (pixelClassifFlags & (1 << IDEPIX_INVALID)) == 0) { + (!isLand(sourceFlagTile, yt, xt) && landAccu[jt][it]) || + (!isWater(sourceFlagTile, yt, xt) && waterAccu[jt][it]); + if (isCoastal && isValid(pixelClassifFlags)) { // another cloud test final float b2 = b2Tile.getSampleFloat(xt, yt); final float b8 = b8Tile.getSampleFloat(xt, yt); @@ -299,32 +306,23 @@ private static int coastalCloudDistinction(int yt, int xt, int jjt, int it, pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS, false); pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_SURE, false); pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); - } else if ((pixelClassifFlags & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE))) == 0) { - // align cloud flag with combination of ambiguous and sure if none of them is set - pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); } else { - // align cloud flag with combination of ambiguous and sure if one of them is set - pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, true);; + // align cloud flag with combination of ambiguous and sure + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, isAmbigousOrSure(pixelClassifFlags)); } } - // reset accu for re-use - landAccu[jjt][it] = false; - waterAccu[jjt][it] = false; return pixelClassifFlags; } - private static int urbanCloudDistinction(int jjt, int it, - boolean[][] urbanSomeClearAccu, + private static int urbanCloudDistinction(int jt, int it, + boolean[][] clearNearbyAccu, double[][] m7, double[][] m8, double[][] c7, double[][] c8, short[][] n78, int pixelClassifFlags) { // some clear pixels nearby, and not cirrus or water - final boolean notInTheMiddleOfACloud = urbanSomeClearAccu[jjt][it]; - if (notInTheMiddleOfACloud - && (pixelClassifFlags & (IDEPIX_CIRRUS_AMBIGUOUS_BIT | IDEPIX_CIRRUS_SURE_BIT | IDEPIX_WATER_BIT)) == 0 - && (pixelClassifFlags & (IDEPIX_CLOUD_AMBIGUOUS_BIT | IDEPIX_CLOUD_SURE_BIT | IDEPIX_CLOUD_BIT)) != 0) { + if (isCloud(pixelClassifFlags) && isNotCirrusNotWater(pixelClassifFlags) && clearNearbyAccu[jt][it]) { // another non-cloud test - final double variance7 = variance_of(c7[jjt][it], m7[jjt][it], n78[jjt][it]); - final double variance8 = variance_of(c8[jjt][it], m8[jjt][it], n78[jjt][it]); + final double variance7 = variance_of(c7[jt][it], m7[jt][it], n78[jt][it]); + final double variance8 = variance_of(c8[jt][it], m8[jt][it], n78[jt][it]); final double cdiValue = (variance7 - variance8) / (variance7 + variance8); if (cdiValue >= CDI_THRESHOLD) { // clear cloud flags if CDI test succeeds @@ -333,42 +331,112 @@ private static int urbanCloudDistinction(int jjt, int it, pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); } } - // clear accu for re-use - urbanSomeClearAccu[jjt][it] = false; - c7[jjt][it] = 0.0; - m7[jjt][it] = 0.0; - c8[jjt][it] = 0.0; - m8[jjt][it] = 0.0; - n78[jjt][it] = 0; return pixelClassifFlags; } - private void addCloudBuffer(Rectangle cloudBufferExtendedRectangle, Rectangle cloudBufferRectangle, - Rectangle targetRectangle, Tile sourceFlagTile, Tile targetTile) { - final boolean[][] cloudBufferAccu = new boolean[cloudBufferSize][targetRectangle.width]; - for (int y = cloudBufferExtendedRectangle.y; y < cloudBufferExtendedRectangle.y + cloudBufferExtendedRectangle.height; y++) { - checkForCancellation(); - for (int x = cloudBufferExtendedRectangle.x; x < cloudBufferExtendedRectangle.x + cloudBufferExtendedRectangle.width; x++) { - if (cloudBufferRectangle.contains(x, y)) { - if (isCloudForBuffer(targetRectangle.contains(x, y) ? targetTile : sourceFlagTile, x, y)) { - fillPatchInAccu(y, x, targetRectangle, cloudBufferWidth, cloudBufferSize, true, cloudBufferAccu); - } - } - // target pixel yt/xt - final int yt = y - cloudBufferWidth; - final int xt = x - cloudBufferWidth; - // accu position jjt/it - final int jjt = (yt - targetRectangle.y) % cloudBufferSize; - final int it = xt - targetRectangle.x; - // evaluate accu if we have enough context - // set cloud buffer flag if it is not cloud but there is a cloud nearby - if (targetRectangle.contains(xt, yt)) { - if (cloudBufferAccu[jjt][it] - && (targetTile.getSampleInt(xt, yt) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD) | (1 << IDEPIX_INVALID))) == 0) { - targetTile.setSample(xt, yt, IDEPIX_CLOUD_BUFFER, true); - } - // clear accu for re-use - cloudBufferAccu[jjt][it] = false; + private void collectLand(int y, int x, + Tile sourceFlagTile, + Rectangle cloudBufferRectangle, + boolean[][] landAccu) { + if (isLand(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, landWaterContextRadius, landAccu); + } + } + + private void collectWater(int y, int x, + Tile sourceFlagTile, + Rectangle cloudBufferRectangle, + boolean[][] waterAccu) { + if (isWater(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, landWaterContextRadius, waterAccu); + } + } + + private void collectClear(int y, int x, + Tile sourceFlagTile, + Rectangle cloudBufferRectangle, + boolean[][] clearNearbyAccu) { + if (isClear(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, urbanContextRadius, clearNearbyAccu); + } + } + + private void collectCdiSumsAndSquares(int y, int x, + Tile b7Tile, Tile b8Tile, Tile b8aTile, + Rectangle cdiRectangle, Rectangle cloudBufferRectangle, + double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78) { + // determine nearest position inside complete image, COPY-extend image + final int xt = + x < cdiRectangle.x ? cdiRectangle.x + : x >= cdiRectangle.x + cdiRectangle.width ? cdiRectangle.x + cdiRectangle.width - 1 + : x; + final int yt = + y < cdiRectangle.y ? cdiRectangle.y + : y >= cdiRectangle.y + cdiRectangle.height ? cdiRectangle.y + cdiRectangle.height - 1 + : y; + // determine values + final float b7 = b7Tile.getSampleFloat(xt, yt); + final float b8 = b8Tile.getSampleFloat(xt, yt); + final float b8a = b8aTile.getSampleFloat(xt, yt); + if (!Float.isNaN(b7) && !Float.isNaN(b8) && b8a != 0.0f) { + final float b7b8a = b7 / b8a; + final float b8b8a = b8 / b8a; + addStddevPatchInAccu(y, x, cloudBufferRectangle, cdiStddevContextRadius, + b7b8a, b8b8a, + m7, c7, m8, c8, n78); + } + } + + private void fillPatchInAccu(int y, int x, Rectangle cloudBufferRectangle, int halfWidth, boolean[][] accu) { + // reduce patch to part overlapping with target image + final int jMin = Math.max(y - cloudBufferRectangle.y - halfWidth, 0); + final int jMax = Math.min(y - cloudBufferRectangle.y + 1 + halfWidth, cloudBufferRectangle.height); + final int iMin = Math.max(x - cloudBufferRectangle.x - halfWidth, 0); + final int iMax = Math.min(x - cloudBufferRectangle.x + 1 + halfWidth, cloudBufferRectangle.width); + // fill patch with value + for (int j = jMin; j < jMax; ++j) { + final int jj = j % contextSize; + for (int i = iMin; i < iMax; ++i) { + accu[jj][i] = true; + } + } + } + + private void addStddevPatchInAccu(int y, int x, Rectangle cloudBufferRectangle, int halfWidth, + float b7b8a, float b8b8a, + double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78) { + // reduce patch to part overlapping with target image + final int jMin = Math.max(y - cloudBufferRectangle.y - halfWidth, 0); + final int jMax = Math.min(y - cloudBufferRectangle.y + 1 + halfWidth, cloudBufferRectangle.height); + final int iMin = Math.max(x - cloudBufferRectangle.x - halfWidth, 0); + final int iMax = Math.min(x - cloudBufferRectangle.x + 1 + halfWidth, cloudBufferRectangle.width); + // add to mean and to sum of squares within patch + final float b7b8a2 = b7b8a * b7b8a; + final float b8b8a2 = b8b8a * b8b8a; + for (int j = jMin; j < jMax; ++j) { + final int jj = j % contextSize; + for (int i = iMin; i < iMax; ++i) { + m7[jj][i] += b7b8a; + c7[jj][i] += b7b8a2; + m8[jj][i] += b8b8a; + c8[jj][i] += b8b8a2; + n78[jj][i] += 1; + } + } + } + + private void addCloudBufferInAccu(int y, int x, Rectangle targetRectangle, int halfWidth, + int[][] cloudBufferAccu) { + // reduce patch to part overlapping with target image + final int jMin = Math.max(y - targetRectangle.y - halfWidth, 0); + final int jMax = Math.min(y - targetRectangle.y + 1 + halfWidth, targetRectangle.height); + final int iMin = Math.max(x - targetRectangle.x - halfWidth, 0); + final int iMax = Math.min(x - targetRectangle.x + 1 + halfWidth, targetRectangle.width); + for (int j = jMin; j < jMax; ++j) { + final int jj = j % cloudBufferSize; + for (int i = iMin; i < iMax; ++i) { + if (isClear(cloudBufferAccu[jj][i])) { + cloudBufferAccu[jj][i] |= (1 << IDEPIX_CLOUD_BUFFER); } } } @@ -398,18 +466,47 @@ private static boolean isLand(Tile tile, int y, int x) { private static boolean isWater(Tile tile, int y, int x) { return tile.getSampleBit(x, y, IDEPIX_WATER); } + + private static boolean isValid(int pixelClassifFlags) { + return (pixelClassifFlags & 1 << IDEPIX_INVALID) == 0; + } - private static boolean mayBeCloud(Tile tile, int y, int x) { - return (tile.getSampleInt(x, y) & ((1 << IDEPIX_CLOUD_AMBIGUOUS) | (1 << IDEPIX_CLOUD_SURE) | (1 << IDEPIX_CLOUD) | 1 << IDEPIX_INVALID)) != 0; + private static boolean isAmbigousOrSure(int pixelClassifFlags) { + return (pixelClassifFlags & (1 << IDEPIX_CLOUD_AMBIGUOUS | 1 << IDEPIX_CLOUD_SURE)) != 0; } - private boolean isCloudForBuffer(Tile targetTile, int x, int y) { - return targetTile.getSampleBit(x, y, IDEPIX_CLOUD_SURE) || - (computeCloudBufferForCloudAmbiguous && targetTile.getSampleBit(x, y, IDEPIX_CLOUD_AMBIGUOUS)); + private static boolean isCloudBuffer(int pixelClassifFlags) { + return (pixelClassifFlags & 1 << IDEPIX_CLOUD_BUFFER) != 0; + } + + private static boolean isClear(int pixelClassifFlags) { + return (pixelClassifFlags + & (1 << IDEPIX_CLOUD_AMBIGUOUS | 1 << IDEPIX_CLOUD_SURE + | 1 << IDEPIX_CLOUD | 1 << IDEPIX_INVALID)) == 0; + } + + private static boolean isCloud(int pixelClassifFlags) { + return (pixelClassifFlags & (1 << IDEPIX_CLOUD_AMBIGUOUS | 1 << IDEPIX_CLOUD_SURE | 1 << IDEPIX_CLOUD)) != 0; + } + + private static boolean isNotCirrusNotWater(int pixelClassifFlags) { + return (pixelClassifFlags & (1 << IDEPIX_CIRRUS_AMBIGUOUS | 1 << IDEPIX_CIRRUS_SURE | 1 << IDEPIX_WATER)) == 0; + } + + private static boolean isClear(Tile tile, int y, int x) { + return (tile.getSampleInt(x, y) + & (1 << IDEPIX_CLOUD_AMBIGUOUS | 1 << IDEPIX_CLOUD_SURE + | 1 << IDEPIX_CLOUD | 1 << IDEPIX_INVALID)) == 0; + } + + private boolean isCloudForBuffer(int pixelClassifFlags) { + return + (pixelClassifFlags & 1 << IDEPIX_CLOUD_SURE) != 0 || + (computeCloudBufferForCloudAmbiguous && (pixelClassifFlags & 1 << IDEPIX_CLOUD_AMBIGUOUS) != 0); } private static double variance_of(double c, double m, int n) { - //return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / (n - 1); + //return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / (n - 1); // seems JNI uses different formula return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / n; } From 2749b95c776a542c479570c40571e1e12a0d5e66 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Sat, 11 Feb 2023 22:32:06 +0100 Subject: [PATCH 08/12] small modifications to reproduce exactly the former result: asymmetric kernel with even number of pixels, comparison of float and double --- .../org/esa/snap/idepix/s2msi/S2IdepixOp.java | 4 ++ .../S2IdepixCloudPostProcess2Op.java | 59 ++++++------------- 2 files changed, 21 insertions(+), 42 deletions(-) diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java index 42868aff..f2fe4624 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixOp.java @@ -109,6 +109,10 @@ private void processSentinel2() { int cacheSize = Integer.parseInt(System.getProperty(S2IdepixUtils.TILECACHE_PROPERTY, "1600")); s2ClassifProduct = S2IdepixUtils.computeTileCacheProduct(s2ClassifProduct, cacheSize); + // breakpoint output to generate input for cloud post-processing + //targetProduct = s2ClassifProduct; + //if (true) return; + // Post Cloud Classification: cloud shadow, cloud buffer, mountain shadow Product postProcessingProduct = computePostProcessProduct(sourceProduct, s2ClassifProduct); diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java index 103f3f3d..8defa29a 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java @@ -191,14 +191,16 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th for (int x = contextExtendedRectangle.x; x < contextExtendedRectangle.x + contextExtendedRectangle.width; x++) { // write dilated land and water patch into accu, required for coastal cloud distinction - if (pixelStateRectangle.contains(x, y)) { - collectLand(y, x, sourceFlagTile, cloudBufferRectangle, landNearbyAccu); - collectWater(y, x, sourceFlagTile, cloudBufferRectangle, waterNearbyAccu); + if (pixelStateRectangle.contains(x, y) && isLand(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, landWaterContextSize, landNearbyAccu); + } + if (pixelStateRectangle.contains(x, y) && isWater(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, landWaterContextSize, waterNearbyAccu); } // write 11x11 "filtered" non-cloud patch into accu, required for urban cloud distinction - if (urbanRectangle.contains(x, y)) { - collectClear(y, x, sourceFlagTile, cloudBufferRectangle, clearNearbyAccu); + if (urbanRectangle.contains(x, y) && isClear(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, urbanContextSize, clearNearbyAccu); } // add stddev contributions to accu of sums, squares, counts, required for urban cloud distinction @@ -215,7 +217,7 @@ public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) th landNearbyAccu, waterNearbyAccu, clearNearbyAccu, m7, c7, m8, c8, n78, sourceFlagTile, b2Tile, b8Tile, b11Tile, - targetRectangle, cloudBufferRectangle, + cloudBufferRectangle, targetRectangle, cloudBufferAccu); } // yb/xb is the target pixel where we have seen just the last pixel of some possible cloud buffer @@ -232,7 +234,7 @@ private void correctFlagsOfPixel(int yt, int xt, boolean[][] landAccu, boolean[][] waterAccu, boolean[][] clearNearbyAccu, double[][] m7, double[][] c7, double[][] m8, double[][] c8, short[][] n78, Tile sourceFlagTile, Tile b2Tile, Tile b8Tile, Tile b11Tile, - Rectangle targetRectangle, Rectangle cloudBufferRectangle, + Rectangle cloudBufferRectangle, Rectangle targetRectangle, int[][] cloudBufferAccu) { // land/water/urban accu pixel position final int jt = (yt - cloudBufferRectangle.y) % contextSize; @@ -298,9 +300,9 @@ private static int coastalCloudDistinction(int yt, int xt, int jt, int it, final float b11 = b11Tile.getSampleFloat(xt, yt); final float idx1 = b2 / b11; final float idx2 = b8 / b11; - //final boolean notCoastal = idx1 > 0.7f || (idx1 > 0.6f && idx2 > 0.9f); - // handles NaN as non-coastal - final boolean isCoastal2 = idx1 <= 0.6f || (idx1 <= 0.7f && idx2 <= 0.9f); + //final boolean notCoast = idx1 > 0.7 || (idx1 < 1 && idx1 > 0.6 && idx2 > 0.9); + // inverted condition handles NaN as non-coastal, using double for constants preserves former results + final boolean isCoastal2 = idx1 <= 0.6 || (idx1 <= 0.7 && idx2 <= 0.9); if (isCoastal2) { // clear cloud flags if cloud test fails pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS, false); @@ -334,33 +336,6 @@ private static int urbanCloudDistinction(int jt, int it, return pixelClassifFlags; } - private void collectLand(int y, int x, - Tile sourceFlagTile, - Rectangle cloudBufferRectangle, - boolean[][] landAccu) { - if (isLand(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, cloudBufferRectangle, landWaterContextRadius, landAccu); - } - } - - private void collectWater(int y, int x, - Tile sourceFlagTile, - Rectangle cloudBufferRectangle, - boolean[][] waterAccu) { - if (isWater(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, cloudBufferRectangle, landWaterContextRadius, waterAccu); - } - } - - private void collectClear(int y, int x, - Tile sourceFlagTile, - Rectangle cloudBufferRectangle, - boolean[][] clearNearbyAccu) { - if (isClear(sourceFlagTile, y, x)) { - fillPatchInAccu(y, x, cloudBufferRectangle, urbanContextRadius, clearNearbyAccu); - } - } - private void collectCdiSumsAndSquares(int y, int x, Tile b7Tile, Tile b8Tile, Tile b8aTile, Rectangle cdiRectangle, Rectangle cloudBufferRectangle, @@ -387,12 +362,12 @@ private void collectCdiSumsAndSquares(int y, int x, } } - private void fillPatchInAccu(int y, int x, Rectangle cloudBufferRectangle, int halfWidth, boolean[][] accu) { + private void fillPatchInAccu(int y, int x, Rectangle cloudBufferRectangle, int width, boolean[][] accu) { // reduce patch to part overlapping with target image - final int jMin = Math.max(y - cloudBufferRectangle.y - halfWidth, 0); - final int jMax = Math.min(y - cloudBufferRectangle.y + 1 + halfWidth, cloudBufferRectangle.height); - final int iMin = Math.max(x - cloudBufferRectangle.x - halfWidth, 0); - final int iMax = Math.min(x - cloudBufferRectangle.x + 1 + halfWidth, cloudBufferRectangle.width); + final int jMin = Math.max(y - cloudBufferRectangle.y - width / 2, 0); + final int jMax = Math.min(y - cloudBufferRectangle.y - width / 2 + width, cloudBufferRectangle.height); + final int iMin = Math.max(x - cloudBufferRectangle.x - width / 2, 0); + final int iMax = Math.min(x - cloudBufferRectangle.x - width / 2 + width, cloudBufferRectangle.width); // fill patch with value for (int j = jMin; j < jMax; ++j) { final int jj = j % contextSize; From 2229dc975f79c65555968113f6a4df8ec4a7490d Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Thu, 15 Dec 2022 11:38:18 +0100 Subject: [PATCH 09/12] support SecondaryMetadata.anyelement from standard CollocationOp as alternative to SLSTRmetadata global attribute --- .../c3solcislstr/rad2refl/SlstrRadReflConverter.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/rad2refl/SlstrRadReflConverter.java b/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/rad2refl/SlstrRadReflConverter.java index 7d6702ab..0b925da0 100644 --- a/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/rad2refl/SlstrRadReflConverter.java +++ b/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/rad2refl/SlstrRadReflConverter.java @@ -74,8 +74,12 @@ static Map getSolarFluxMapFromC3SSYNMetadata(Product sourceProduc final int spectralIndex = Integer.parseInt(bandName.substring(1, 2)) - 1; final String bandNameChannel = bandName.substring(0, 2); final String stringToReplace = radToReflMode ? "radiance" : "reflectance"; - final MetadataElement qualityElement = sourceProduct.getMetadataRoot().getElement("SLSTRmetadata"); - + final MetadataElement qualityElement = + sourceProduct.getMetadataRoot().containsElement("SLSTRmetadata") ? + sourceProduct.getMetadataRoot().getElement("SLSTRmetadata") : + sourceProduct.getMetadataRoot().containsElement("SecondaryMetadata") ? + sourceProduct.getMetadataRoot().getElement("SecondaryMetadata").getElementAt(0) : + null; if (qualityElement != null) { final MetadataElement variableAttributesElement = qualityElement.getElement(bandNameChannel); if (variableAttributesElement != null) { From 461c34e253d8560dc7f24641531eecf385f9ae50 Mon Sep 17 00:00:00 2001 From: Olaf Danne Date: Mon, 20 Mar 2023 18:21:56 +0100 Subject: [PATCH 10/12] Idepix OLCI: introduced user option to use an alternative NN for pixel classification; minor cleanup of log messages. --- .../C3SOlciSlstrClassificationOp.java | 2 +- .../IdepixMerisLandClassificationOp.java | 2 +- .../olci/IdepixOlciClassificationOp.java | 42 +++++++++++++++--- .../esa/snap/idepix/olci/IdepixOlciOp.java | 7 +++ .../olci/docs/images/OlciParameters.png | Bin 95858 -> 22784 bytes .../docs/olci/OlciProcessorDescription.html | 6 +++ .../olcislstr/OlciSlstrClassificationOp.java | 2 +- .../s2msi/S2IdepixClassificationOp.java | 2 +- .../idepix/spotvgt/VgtClassificationOp.java | 2 +- 9 files changed, 53 insertions(+), 12 deletions(-) diff --git a/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/C3SOlciSlstrClassificationOp.java b/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/C3SOlciSlstrClassificationOp.java index d975f471..062cbdf9 100644 --- a/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/C3SOlciSlstrClassificationOp.java +++ b/idepix-c3solcislstr/src/main/java/org/esa/snap/idepix/c3solcislstr/C3SOlciSlstrClassificationOp.java @@ -303,7 +303,7 @@ public void computeTileStack(Map targetTiles, Rectangle rectangle, P } } } catch (Exception e) { - throw new OperatorException("Failed to provide GA cloud screening:\n" + e.getMessage(), e); + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); } } diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisLandClassificationOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisLandClassificationOp.java index a75bdf79..0e6d2699 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisLandClassificationOp.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisLandClassificationOp.java @@ -207,7 +207,7 @@ public void computeTileStack(Map targetTiles, Rectangle rectangle, P } } } catch (Exception e) { - throw new OperatorException("Failed to provide GA cloud screening:\n" + e.getMessage(), e); + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); } } diff --git a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciClassificationOp.java b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciClassificationOp.java index 107a2da0..b6fc636c 100644 --- a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciClassificationOp.java +++ b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciClassificationOp.java @@ -24,8 +24,8 @@ import org.locationtech.jts.geom.Polygon; import java.awt.Rectangle; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.nio.file.Files; import java.util.Calendar; import java.util.Map; @@ -63,6 +63,11 @@ public class IdepixOlciClassificationOp extends Operator { description = " If applied, write Schiller NN value to the target product ") private boolean outputSchillerNNValue; + @Parameter(description = "Alternative NN file. " + + "If set, it MUST follow format and input/output as used in default '11x10x4x3x2_207.9.net. ", + label = " Alternative NN file") + private File alternativeNNFile; + @Parameter(defaultValue = "false", description = "Check for sea/lake ice also outside Sea Ice Climatology area.", label = "Check for sea/lake ice also outside Sea Ice Climatology area" @@ -144,8 +149,21 @@ public void initialize() throws OperatorException { } private void readSchillerNeuralNets() { - InputStream olciAllIS = getClass().getResourceAsStream(OLCI_ALL_NET_NAME); - olciAllNeuralNet = SchillerNeuralNetWrapper.create(olciAllIS); + InputStream olciAllInputStream; + try { + olciAllInputStream = getNNInputStream(); + } catch (IOException e) { + throw new OperatorException("Cannot read specified alternative Neural Net - please check!", e); + } + olciAllNeuralNet = SchillerNeuralNetWrapper.create(olciAllInputStream); + } + + private InputStream getNNInputStream() throws IOException { + if (alternativeNNFile != null) { + return Files.newInputStream(alternativeNNFile.toPath()); + } else { + return getClass().getResourceAsStream(OLCI_ALL_NET_NAME); + } } private void initSeaIceClassifier() { @@ -243,7 +261,7 @@ public void computeTileStack(Map targetTiles, Rectangle rectangle, P } } } catch (Exception e) { - throw new OperatorException("Failed to provide GA cloud screening:\n" + e.getMessage(), e); + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); } } @@ -300,7 +318,12 @@ private void classifyOverLand(Tile[] olciReflectanceTiles, } final float olciReflectance21 = olciReflectances[Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length - 1]; - SchillerNeuralNetWrapper nnWrapper = olciAllNeuralNet.get(); + SchillerNeuralNetWrapper nnWrapper; + try { + nnWrapper = olciAllNeuralNet.get(); + } catch (Exception e) { + throw new OperatorException("Cannot get values from Neural Net file - check format!" + e.getMessage()); + } double[] inputVector = nnWrapper.getInputVector(); for (int i = 0; i < inputVector.length; i++) { inputVector[i] = Math.sqrt(olciReflectances[i]); @@ -392,7 +415,12 @@ private boolean isOlciInlandWaterPixel(int x, int y, Tile olciL1bFlagTile) { } private double[] getOlciNNOutput(int x, int y, Tile[] rhoToaTiles) { - SchillerNeuralNetWrapper nnWrapper = olciAllNeuralNet.get(); + SchillerNeuralNetWrapper nnWrapper; + try { + nnWrapper = olciAllNeuralNet.get(); + } catch (Exception e) { + throw new OperatorException("Cannot get values from Neural Net file - check format! " + e.getMessage()); + } double[] nnInput = nnWrapper.getInputVector(); for (int i = 0; i < nnInput.length; i++) { nnInput[i] = Math.sqrt(rhoToaTiles[i].getSampleFloat(x, y)); diff --git a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciOp.java b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciOp.java index d94b5bc3..3800bbe1 100644 --- a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciOp.java +++ b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciOp.java @@ -16,6 +16,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Polygon; +import java.io.File; import java.util.HashMap; import java.util.Map; @@ -79,6 +80,11 @@ public class IdepixOlciOp extends BasisOp { description = " If applied, write NN value to the target product ") private boolean outputSchillerNNValue; + @Parameter(description = "Alternative NN file. " + + "If set, it MUST follow format and input/output as used in default '11x10x4x3x2_207.9.net. ", + label = " Alternative NN file") + private File alternativeNNFile; + @Parameter(defaultValue = "true", label = " Compute mountain shadow") private boolean computeMountainShadow; @@ -243,6 +249,7 @@ private void setClassificationParameters() { classificationParameters = new HashMap<>(); classificationParameters.put("copyAllTiePoints", true); classificationParameters.put("outputSchillerNNValue", outputSchillerNNValue); + classificationParameters.put("alternativeNNFile", alternativeNNFile); classificationParameters.put("useSrtmLandWaterMask", useSrtmLandWaterMask); } diff --git a/idepix-olci/src/main/javahelp/org/esa/snap/idepix/olci/docs/images/OlciParameters.png b/idepix-olci/src/main/javahelp/org/esa/snap/idepix/olci/docs/images/OlciParameters.png index 19f0d452aff914750e0592162f1aa33608e6ca97..13a87c8165ff3f34a7a6d38a582c92ce9c14e5de 100644 GIT binary patch literal 22784 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 FKLAo#JRSf5 literal 95858 zcmbSzby(Eh);6Gk5+c$_NFyC04F-yI*N{pHLw66Lbc>{f(ls>F4519&Fp?uJ-95nf z8=pAOIp6!9>$|=`ytrn5F?+AQ_FDISueA+RRhA{hqsGI)z#x=+C8LgkftiAVffalU z2lytre}xwKgXyd;`wXLWfMyN&aMSXs(o+nKiWq#Zla;sRR-;N1s&BRuOOnT@RlE%U$jyn%b$3W3 zDeTV2Z65hh`QE1JcV|Poq;bD>S}laNAk&xi%aFA`Dr9-0|v9nhyMO59QtOo zm8#p%#@|z$_-irC8tFYQPdsvG8E8gsAOvRxXAg)*qcC?bZH}DsuA32(?@qjTTvcFQ z8Y+@nSj5C?7%s7Ao0ihxxqHOKuAS27ve&nWk`Xd8GPLjq&t+r+UdJ*pX0`Hl-6d8t zp)?B(W#?0xH9Xewn@8+w5J|tUZQ3I|{RBHNX16O{ER(oWF7a9BiSG2HJGCfFs)-=S>D;Kjp_ZL;VUnK7}yrnBPsRjcsJf-*7v6@ z@+@N#pO~2TkrH+a!q11k408GWbb$gE3OjMXmPH+YqU+3T+0WdY|8~phI)10%=e_So z$TQ?_i!lnj>5P!*XY=sM&v{&b_?ok?D zUb#DZFC!P{W$D`X7U6q3kojg2uS0gfdze>S*GD@JYdz*QC%5P{rvVe6M947e8X5}i zSa%EP<&!DB@E2}gQ}Vd%AXx#u=zuc8VDC;Y*?YFFs)2Qt@GiNm~ z}{_os*+5Oq%Tz*SB_Wryp=wt2u0a%q;!aGn&U0AO@Hkh zQwO_7DbbgXzOvEN4g)q%OfZpoO=uv#$AC&L)|VfjfVte*8Lx4zpnC+I>m?laMH3H@dckQ2OL+=J!EV-~C+*R<97aezo3wg(*Y!gG~$nhH&Y`#C|XHgTi)0V+3D7p4}8j><_BM#8^<3 zUBu@;%f<#PDk<*tt9+OlcAm`3es=>Rr-t+pcP`NY-RxTnjoidrvI9Hx^qpHr zI}3A}Ey0;y7x(v|FqDb~uef8LlbTVUlWnupJWR8Q;t6Qz`jJtZWB{0|@z#lg_$|Vb z5nR>uY(^!tvR|-XB&R+ z=`Rm&oZ2mE*<3DX{`4-pdT4Ypce2}zXo+z@ru*Jeb|H#7NFlh|N!a;WT-J04NCrB) zK*R_|*q8nMBCRLjUHF_w7pXiyn)hJT(0D=toZ^MBL8x($DzRRww745d?i5^>EuKh0 z?z821!4|9ahwMPYxlBWDBq?AcZjy2Ni+oFmepl3pWKy8Dw$QDCnnS1YcASA|qYxt)r!p(D=xpZqIm3RVccw$jRg<{Q0+@mLK`Ur(X%@gle?)8$ig-UPSDQwt1E?X^F_J#+k# zb%B27d(X=A(}_z{cE1Dex&wO6 z(U8#4J2`oQ1a#+ge!H{WHnynS+t+(1dp!_c_{r5NS<^gW*}Uc4;qx?+b8^&q3O}m$ z2GMKO<<2+A(NCt+xmrl1uoUIf!TbGEp`pK8t%@2WLqiL+l-!euiHU38Jq->G#p?K9 z5DI~Sj0`Cy4izIKUgQQmSpbXm%=_RI(#8l##!M)FR$^l8@c-onsdof&NA9VI+?kIx zrB30@(oE~<`a;}CreHz4;d&Q!j8trRx*8Q4YF+c=1J$>~b@H#ZwMW$+r9^Qp&bl*3 z-|AaJ^?<2&wQmamzup)9PO;gJy_mCq+eqPcA*r&CR<=v8r%_P}n;}F-W=xM8CxuLr z@G!ak;;y>FQCWcE9kW=zS+G8S>+)f+UIWRtMAz7;V6EP_e}n%Z%6VRKPNZ{4dM+ z2U2Yc5;ZGp+7I`9aWR=4BV}zwOl#nTFZfQei@8fCTkRnFIQ!X`)(6jX9?_cVUTkyW zq+CWl4elCIkDaGivJ}x5hO7RrVCe$5k)1$=}-!U_VPT2RRKCmj>{ zWysV}nz^Yd^}E~RuxUPJ2}%%3|E^T^7V#=WxzD7($+p)|vXsN7`|da%bJxdkXG*G+ z=W!4hSI-Hb7~`i;(}f?F%M1I*)AHQXcQdN)+@PP`Pu19&&C!SvIYxF4Q} zIJ{DM$R&!^^OENV^ULA3M6G!FpJk|guzUQG3c?Rybx{L&fZOs?L7KqYkJ=V~3ckW;00 z82s>>Qlo~Es)H5zUa`m5M*|C+x4~DFpTNlvVcNYxoc6~R#Yf7kS3P#Vjfkdy0c%pjUTA(_eY5n9B9`1CR`hH+2q%cs z-EkzGwJ28dcJi5kA#J?t&b;11i>64>Mn3ej<9R!OO_9)bCqHMh)?oY-RqcUysdi~? zH44_!TAmNdN`Kbr&Bh$mMJvvI{W^Lf5saV7KiuZpbTrWzkwsA-RBX;rn}eG{oW3KjL&4)UUa~TqYs5<# z#*SpQfZHhsmly)1@Ne>%pVo65Ssn$}XPw^=hDO$F=8iAIjPMGH!G6|-q4oPLxVCkDeCI?IiHJOTO-s>UX*1%n?GhgBpfVyf=d4Q_01OD zw%8nfSZ4bkm}*A1cR`G1oEy2$~iD$HpT+T(c8G`h5tMHT; zgM;zE{5McSDa%|dYzvfg2G9NYFvmYTZbUI!V1;Hz9h&zMy>!%V-WIPd&==OmXA3T* zNQ5S6Vz7V9T^20XSG(F+CRVa9^dw29SHU{o=(Q?A!h*U3uz04p3&w8>bU%7L6k#@H zmut(AGy}}r!%x$IwI!9nv zt_FvMz$&*n!YX{@jS~lWQ%Ube9kYW>*`j7!-w7C7s4;^g#`GjIBxAK86(L%@i;@Vt zQx}b!vb-t5y6}9(5MQzQDF~w>g4?pjKaD#yW6$$}aXFql8ODJ4hdtxLn++XmWRGad z%(LHMdiI&lWu!yv{`2w_vuWwVX2AX2TZeyL8u2=BR7Y+6A7H|PgDv#ELYJRKv3Hflu&?jk*UH5~i+k>B(y+ zQnFsWcj#7aMux{ep2;kI&4%~KFMmB^`I+e-Ec^Z?D4;|Xqf87vBQV)rZLVU$5 zc!R7p3+1L_%+4n2sXg&4V6NyOaIP9CH1TvRn|_kg)*d`~~Q z(9^9y>3X{i$SqmhLR9sBPtxIFfs0QD@eE?Os13bsLPq41G-SNO&;grk(CIY3a=O?y3YnQ*3B_oy8n3i_>2TQss!h z>#kat-GU&6KPRsk{yM(_w*h9Ur7M2hY>y}==kbip$`loI=aP^EvmYLOXO6R$9fAlWh@4?LC@#b*04oh55^d^(hBuXe%PBCsDXfszCE*4=<9ROn^zQnP^j6{ zgopc%fH(*YgkL)8AP-lKpIvG+jm|_H`|4Q1vpw~W1Qb#l)BGz2FmrdFpGMD)AzDGg zutUu!ssYQ%R-&HRd`O?f6W)WtpHsP1q?)>^qH0!Q-7c<*BXuE`TfU^q^XF?n8x}S( zNGv`BYFpO!V##b~6HWoSLL1k5=%UUp|0v@~)5vIBxplL(-bDbFlceYB0Mt0Bd@>@XX)`=OXJ+2&NU3PrLW4B2F> zU$v$+JZoWVqw^d)r>Ev&O38#4o9K<6&CL=|GUeZch>e;z)$zwKwPPp-a9ga|IkBU~XyYBD} zJRy1Xdx;Ht+0z=L3ucu_D@wIyGBjC^KSdK-Q2{`zd`KsRbHcNmiU=R2X7G*1WFKYhya6%dx=N&!qg}TykWS^L zv^vuCS{c_xB4rkUp)#{@y+(U0395(HTA_|LA0%j6_srbz(ay(NzGs%hxqdI29OPr8 z2rS>E!eaL(E84Vr7>nJ%$Y0#Y>~wq589zygUjGK=7&cR~UH2YSSl8I0Z6B@~@C)sG zW})nlXjE8fjIZ{S`im!yj*c89s2H<4m#Cw%gH$s_G8B`6slUx!@DP~w|9eaS-x_c# zgY@YV)0KWFdV^;Hlh;I+8hCCx!W|`2CIG2e?U$HTMqhvA;#w!RLB03Ga%8*1pI5qo zXi@s)jth_v)KJnGcK*!Pc~sg4eTq)+KVl-VJHlE-5-|Gl6(d$>EAPp?i!6cGBJ-n zGLKBODEsv2lMz`o+bGR%r&)!^Px42zkt7ejQ!Eq-iHj2@)%rF!c&EqF4L`%a)=ru} z9atz?U@)J>?wK7@oDVV32#j_a40El~lu4(m11L65zG0wy;JKsSH|G{;Q;h%B1BH#* zJvg1qm&F-~xK&V1ROaWw81536DKd*I1B(9frAP`>9nJ%LJO+s_&h(p@Q#hm7E=+FT8`s;O7cRhk(qYW*9*G^8pz62q{d!V5J>z}P*f-R%< zfaSB0-9xBQPP~S$UP}8D6%`F`mPt6iMyrdM9?vPWy&wlALp ?6DKo14p{OAK|Hn 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 diff --git a/idepix-olci/src/main/javahelp/org/esa/snap/idepix/olci/docs/olci/OlciProcessorDescription.html b/idepix-olci/src/main/javahelp/org/esa/snap/idepix/olci/docs/olci/OlciProcessorDescription.html index cb6c9ab6..9bd89f99 100644 --- a/idepix-olci/src/main/javahelp/org/esa/snap/idepix/olci/docs/olci/OlciProcessorDescription.html +++ b/idepix-olci/src/main/javahelp/org/esa/snap/idepix/olci/docs/olci/OlciProcessorDescription.html @@ -107,6 +107,12 @@

The Processing Parameters

(floating point number on the interval [0.0, 5.0]) is written to the target product. The default is 'false'.

+

+ Alternative NN file:
+ Alternative NN file. If selected, this file MUST follow format and input/output as used in + default '11x10x4x3x2_207.9.net'. +

+

Compute mountain shadow:
If set, a mountain shadow is computed and added to the pixel classification. The default value is 'true'. diff --git a/idepix-olcislstr/src/main/java/org/esa/snap/idepix/olcislstr/OlciSlstrClassificationOp.java b/idepix-olcislstr/src/main/java/org/esa/snap/idepix/olcislstr/OlciSlstrClassificationOp.java index 62d84c4c..9cb5f701 100644 --- a/idepix-olcislstr/src/main/java/org/esa/snap/idepix/olcislstr/OlciSlstrClassificationOp.java +++ b/idepix-olcislstr/src/main/java/org/esa/snap/idepix/olcislstr/OlciSlstrClassificationOp.java @@ -207,7 +207,7 @@ public void computeTileStack(Map targetTiles, Rectangle rectangle, P } } } catch (Exception e) { - throw new OperatorException("Failed to provide GA cloud screening:\n" + e.getMessage(), e); + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); } } diff --git a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixClassificationOp.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixClassificationOp.java index 01e92625..987bf7a1 100644 --- a/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixClassificationOp.java +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/S2IdepixClassificationOp.java @@ -218,7 +218,7 @@ public void computeTileStack(Map targetTiles, Rectangle rectangle, P } } catch (Exception e) { - throw new OperatorException("Failed to provide GA cloud screening:\n" + e.getMessage(), e); + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); } } diff --git a/idepix-spotvgt/src/main/java/org/esa/snap/idepix/spotvgt/VgtClassificationOp.java b/idepix-spotvgt/src/main/java/org/esa/snap/idepix/spotvgt/VgtClassificationOp.java index e9a2d205..df7c724a 100644 --- a/idepix-spotvgt/src/main/java/org/esa/snap/idepix/spotvgt/VgtClassificationOp.java +++ b/idepix-spotvgt/src/main/java/org/esa/snap/idepix/spotvgt/VgtClassificationOp.java @@ -211,7 +211,7 @@ public void computeTileStack(Map targetTiles, Rectangle rectangle, P } } } catch (Exception e) { - throw new OperatorException("Failed to provide GA cloud screening:\n" + e.getMessage(), e); + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); } } From 7ba4f63d519bbef23e682bd379057f001dc1c732 Mon Sep 17 00:00:00 2001 From: Olaf Danne Date: Wed, 29 Mar 2023 22:14:07 +0200 Subject: [PATCH 11/12] Idepix Meris: added mountain shadow detection (same algo as for OLCI); some cleanup (removed code duplication) --- .../util/SlopeAspectOrientationUtils.java | 183 ++++++++ .../idepix/meris/IdepixMerisConstants.java | 35 +- .../meris/IdepixMerisMountainShadowOp.java | 140 ++++++ .../esa/snap/idepix/meris/IdepixMerisOp.java | 18 +- .../meris/IdepixMerisPostProcessOp.java | 74 +++- .../IdepixMerisSlopeAspectOrientationOp.java | 202 +++++++++ .../snap/idepix/meris/IdepixMerisUtils.java | 81 +++- .../IdepixMerisViewAngleInterpolationOp.java | 189 ++++++++ .../Meris3rd4thReprocessingAdapter.java | 406 ------------------ .../Meris3rd4thReprocessingMetadata.java | 101 ----- .../reprocessing/ReprocessingAdapter.java | 27 -- .../meris/docs/images/MerisParameters.png | Bin 28161 -> 32494 bytes .../docs/meris/MerisProcessorDescription.html | 11 + .../org.esa.snap.core.gpf.OperatorSpi | 4 +- .../Meris3rd4thReprocessingAdapterTest.java | 270 ------------ .../Meris3rd4thReprocessingMetadataTest.java | 95 ---- .../IdepixOlciSlopeAspectOrientationOp.java | 184 +------- ...depixOlciSlopeAspectOrientationOpTest.java | 87 ---- 18 files changed, 928 insertions(+), 1179 deletions(-) create mode 100644 idepix-core/src/main/java/org/esa/snap/idepix/core/util/SlopeAspectOrientationUtils.java create mode 100644 idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java create mode 100644 idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java create mode 100644 idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisViewAngleInterpolationOp.java delete mode 100644 idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapter.java delete mode 100644 idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadata.java delete mode 100644 idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/ReprocessingAdapter.java delete mode 100644 idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapterTest.java delete mode 100644 idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadataTest.java delete mode 100644 idepix-olci/src/test/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOpTest.java diff --git a/idepix-core/src/main/java/org/esa/snap/idepix/core/util/SlopeAspectOrientationUtils.java b/idepix-core/src/main/java/org/esa/snap/idepix/core/util/SlopeAspectOrientationUtils.java new file mode 100644 index 00000000..786a7f84 --- /dev/null +++ b/idepix-core/src/main/java/org/esa/snap/idepix/core/util/SlopeAspectOrientationUtils.java @@ -0,0 +1,183 @@ +package org.esa.snap.idepix.core.util; + +import org.esa.snap.core.datamodel.GeoCoding; +import org.esa.snap.core.datamodel.GeoPos; +import org.esa.snap.core.datamodel.PixelPos; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.gpf.Tile; +import org.esa.snap.core.util.math.MathUtils; + +public class SlopeAspectOrientationUtils { + + private final static float EARTH_MIN_ELEVATION = -428.0f; // at shoreline of Dead Sea + private final static float EARTH_MAX_ELEVATION = 8848.0f; // Mt. Everest + + /** + * Provides data from a 3x3 macropixel of a float source tile + * + * @param sourceTile - + * @param y - + * @param x - + * @return float[9] + */ + public static float[] get3x3MacropixelData(Tile sourceTile, int y, int x) { + float[] macropixelData = new float[9]; + macropixelData[0] = sourceTile.getSampleFloat(x - 1, y - 1); + macropixelData[1] = sourceTile.getSampleFloat(x, y - 1); + macropixelData[2] = sourceTile.getSampleFloat(x + 1, y - 1); + macropixelData[3] = sourceTile.getSampleFloat(x - 1, y); + macropixelData[4] = sourceTile.getSampleFloat(x, y); + macropixelData[5] = sourceTile.getSampleFloat(x + 1, y); + macropixelData[6] = sourceTile.getSampleFloat(x - 1, y + 1); + macropixelData[7] = sourceTile.getSampleFloat(x, y + 1); + macropixelData[8] = sourceTile.getSampleFloat(x + 1, y + 1); + + return macropixelData; + } + + /** + * Checks if 3x3 box of elevations around reference pixel is valid + * (i.e. not NaN and all values inside real values possible on Earth) + * + * @param elevationData - 3x3 box of elevations + * @return boolean + */ + public static boolean is3x3ElevationDataValid(float[] elevationData) { + for (final float elev : elevationData) { + if (elev == 0.0f || Float.isNaN(elev) || elev < EARTH_MIN_ELEVATION || elev > EARTH_MAX_ELEVATION) { + return false; + } + } + return true; + } + + /** + * Computes slope and aspect for a 3x3 altitude array + * + * @param elev - 3x3 elevation array + * @param orientation - orientation angle of box + * @param vza - view zenith angle + * @param vaa - view azimuth angle + * @param saa - sun azimuth angle + * @param spatialResolution - spatial resolution in m + * @return float[]{slope, aspect} + */ + public static float[] computeSlopeAspect3x3(float[] elev, float orientation, float vza, float vaa, float saa, + double spatialResolution) { + //DM: geometric correction of resolution necessary for observations not in nadir view. + // generates steeper slopes. Needs vza, vaa, angle between y-pixel axis and north direction (orientation). + //DM: orientation in rad! + float b = (elev[2] + 2 * elev[5] + elev[8] - elev[0] - 2 * elev[3] - elev[6]) / 8f; //direction x + float c = (elev[0] + 2 * elev[1] + elev[2] - elev[6] - 2 * elev[7] - elev[8]) / 8f; //direction y + double vaa_orientation = (360.0 - (vaa + orientation / MathUtils.DTOR)) * MathUtils.DTOR; + double spatialRes = spatialResolution / Math.cos(vza * MathUtils.DTOR); + float slope = (float) Math.atan(Math.sqrt(Math.pow(b / (spatialRes * Math.sin(vaa_orientation)), 2) + + Math.pow(c / (spatialRes * Math.cos(vaa_orientation)), 2))); + float aspect = (float) Math.atan2(-b, -c); + if (saa > 270. || saa < 90) { //Sun from North (mostly southern hemisphere) + aspect -= Math.PI; + } + if (aspect < 0.0f) { + // map from [-180, 180] into [0, 360], see e.g. https://www.e-education.psu.edu/geog480/node/490 + aspect += 2.0 * Math.PI; + } + if (slope <= 0.0) { + aspect = Float.NaN; + } + + return new float[]{slope, aspect}; + } + + /** + * Computes the orientation of a 3x3 lat/lon box, i.e. uses 3rd and 5th point. + * + * @param latData - 3x3 lat values + * @param lonData - 3x3 lon values + * @return float + */ + public static float computeOrientation3x3Box(float[] latData, float[] lonData) { + final float lat1 = latData[3]; + final float lat2 = latData[5]; + final float lon1 = lonData[3]; + final float lon2 = lonData[5]; + + return computeOrientation(lat1, lat2, lon1, lon2); + } + + /** + * Provides orientation ('bearing') between two points. + * See theory e.g. at + *
... + * + * @param lat1 - first latitude + * @param lat2 - second latitude + * @param lon1 - first longitude + * @param lon2 - second longitude + * @return float + */ + public static float computeOrientation(float lat1, float lat2, float lon1, float lon2) { +// final float lat1Rad = lat1 * MathUtils.DTOR_F; +// final float deltaLon = lon2 - lon1; + // we use this formula, as in S2-MSI: +// return (float) Math.atan2(-(lat2 - lat1), deltaLon * Math.cos(lat1Rad)); + + // DM: formulas from theory, see above + double X = Math.cos(lat2 * MathUtils.DTOR) * Math.sin((lon2 - lon1) * MathUtils.DTOR); + double Y = Math.cos(lat1 * MathUtils.DTOR) * Math.sin(lat2 * MathUtils.DTOR) - Math.sin(lat1 * MathUtils.DTOR) * X; + return (float) (Math.atan2(X, Y)); + + } + + /** + * Computes product spatial resolution from great circle distances at the product edges. + * To be used as fallback if we have no CRS geocoding. + * + * @param l1bProduct - the source product + * @param sourceGeoCoding - the source scene geocoding + * @return spatial resolution in metres + */ + public static double computeSpatialResolution(Product l1bProduct, GeoCoding sourceGeoCoding) { + final double width = l1bProduct.getSceneRasterWidth(); + final double height = l1bProduct.getSceneRasterHeight(); + + final GeoPos leftPos = sourceGeoCoding.getGeoPos(new PixelPos(0, height / 2), null); + final GeoPos rightPos = sourceGeoCoding.getGeoPos(new PixelPos(width - 1, height / 2), null); + final double distance1 = + computeDistance(leftPos.getLat(), leftPos.getLon(), rightPos.getLat(), rightPos.getLon()); + final double xRes = 1000.0 * distance1 / (width - 1); + + final GeoPos upperPos = sourceGeoCoding.getGeoPos(new PixelPos(width / 2, 0), null); + final GeoPos lowerPos = sourceGeoCoding.getGeoPos(new PixelPos(width / 2, height - 1), null); + final double distance2 = + computeDistance(upperPos.getLat(), upperPos.getLon(), lowerPos.getLat(), lowerPos.getLon()); + final double yRes = 1000.0 * distance2 / (height - 1); + + return 0.5 * (xRes + yRes); + } + + /** + * Calculate the great-circle distance between two points on Earth using Haversine formula. + * See e.g. ... + * + * @param lat1 - first point latitude + * @param lon1 - first point longitude + * @param lat2 - second point latitude + * @param lon2 - second point longitude + * @return distance in km + */ + public static double computeDistance(double lat1, double lon1, double lat2, double lon2) { + final double deltaLatR = (lat1 - lat2) * MathUtils.DTOR; + final double deltaLonR = (lon1 - lon2) * MathUtils.DTOR; + + final double a = Math.sin(deltaLatR / 2.0) * Math.sin(deltaLatR / 2.0) + + Math.cos(lat1 * MathUtils.DTOR) * Math.cos(lat2 * MathUtils.DTOR) * + Math.sin(deltaLonR / 2.0) * Math.sin(deltaLonR / 2.0); + + final double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + final double R = 6371.0; // Earth radius in km + + return R * c; + } + +} diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisConstants.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisConstants.java index 0b4bb7e5..12a17427 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisConstants.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisConstants.java @@ -9,11 +9,44 @@ */ public class IdepixMerisConstants { - public static final int IDEPIX_GLINT_RISK = IdepixConstants.NUM_DEFAULT_FLAGS + 1; + public static final int IDEPIX_MOUNTAIN_SHADOW = IdepixConstants.NUM_DEFAULT_FLAGS; // first non-default flag + static final String IDEPIX_MOUNTAIN_SHADOW_DESCR_TEXT = "Pixel is affected by a mountain/hill shadow"; + public static final int IDEPIX_GLINT_RISK = IdepixConstants.NUM_DEFAULT_FLAGS + 1; static final String IDEPIX_GLINT_RISK_DESCR_TEXT = "Glint risk pixel"; /* Level 1 Flags Positions */ static final int L1_F_LAND = 4; static final int L1_F_INVALID = 7; + + + public static final String MERIS_LATITUDE_BAND_NAME = "latitude"; + public static final String MERIS_LONGITUDE_BAND_NAME = "longitude"; + + public static final String MERIS_ALTITUDE_BAND_NAME = "dem_alt"; + public static final String MERIS_SUN_ZENITH_BAND_NAME = "sun_zenith"; + public static final String MERIS_SUN_AZIMUTH_BAND_NAME = "sun_azimuth"; + public static final String MERIS_VIEW_ZENITH_BAND_NAME = "view_zenith"; + public static final String MERIS_VIEW_AZIMUTH_BAND_NAME = "view_azimuth"; + + public static final String MERIS_4RP_ALTITUDE_BAND_NAME = "altitude"; + public static final String MERIS_4RP_SUN_ZENITH_BAND_NAME = "SZA"; + public static final String MERIS_4RP_SUN_AZIMUTH_BAND_NAME = "SAA"; + public static final String MERIS_4RP_VIEW_ZENITH_BAND_NAME = "OZA"; + public static final String MERIS_4RP_VIEW_AZIMUTH_BAND_NAME = "OAA"; + + public static final String MERIS_VIEW_ZENITH_INTERPOLATED_BAND_NAME = "OZA_interp"; + public static final String MERIS_VIEW_AZIMUTH_INTERPOLATED_BAND_NAME = "OAA_interp"; + + public static final double[] POLYNOM_FIT_INITIAL = new double[]{0., 0., 0.}; + + // view angle interpolation at discontinuities: + static final int MERIS_FR_FULL_PRODUCT_WIDTH = 4481; + static final int MERIS_RR_FULL_PRODUCT_WIDTH = 1121; + static final int MERIS_FR_DEFAULT_NX_CHANGE = 2241; + static final int MERIS_RR_DEFAULT_NX_CHANGE = 561; + static final int[] MERIS_DEFAULT_FR_NX_VZA = new int[]{2100, 2180, 2302, 2382}; + static final int[] MERIS_DEFAULT_RR_NX_VZA = new int[]{525, 545, 576, 596}; + static final int[] MERIS_DEFAULT_FR_NX_VAA = new int[]{1000, 2000, 2500, MERIS_FR_FULL_PRODUCT_WIDTH}; + static final int[] MERIS_DEFAULT_RR_NX_VAA = new int[]{250, 500, 625, MERIS_RR_FULL_PRODUCT_WIDTH}; } diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java new file mode 100644 index 00000000..394b55a6 --- /dev/null +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java @@ -0,0 +1,140 @@ +package org.esa.snap.idepix.meris; + +import org.esa.snap.core.datamodel.GeoPos; +import org.esa.snap.core.datamodel.PixelPos; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.datamodel.ProductData; +import org.esa.snap.core.gpf.GPF; +import org.esa.snap.core.gpf.OperatorException; +import org.esa.snap.core.gpf.OperatorSpi; +import org.esa.snap.core.gpf.annotations.OperatorMetadata; +import org.esa.snap.core.gpf.annotations.Parameter; +import org.esa.snap.core.gpf.annotations.SourceProduct; +import org.esa.snap.core.gpf.pointop.*; +import org.esa.snap.core.util.math.MathUtils; + +import java.util.HashMap; + +/** + * Computes mountain/hill shadow for a Sentinel-3 MERIS product using slope, aspect and orientation. + * See theory e.g. at + * ..., or + * ... + * + * @author Tonio Fincke, Olaf Danne + */ +@OperatorMetadata(alias = "Idepix.Meris.MountainShadow", + version = "2.0", + internal = true, + authors = "Tonio Fincke, Olaf Danne", + copyright = "(c) 2018-2021 by Brockmann Consult", + description = "Computes mountain/hill shadow for a Sentinel-3 OLCI product using slope, aspect and orientation.") +public class IdepixMerisMountainShadowOp extends PixelOperator { + + @SourceProduct(alias = "l1b") + private Product l1bProduct; + + @Parameter(label = " Extent of mountain shadow", defaultValue = "0.9", interval = "[0,1]", + description = "Extent of mountain shadow detection") + private double mntShadowExtent; + + private final static int SZA_INDEX = 0; + private final static int SAA_INDEX = 1; + private final static int OZA_INDEX = 2; + private final static int OAA_INDEX = 3; + private final static int SLOPE_INDEX = 4; + private final static int ASPECT_INDEX = 5; + private final static int ORIENTATION_INDEX = 6; + + + private final static int MOUNTAIN_SHADOW_FLAG_BAND_INDEX = 0; + + public final static String MOUNTAIN_SHADOW_FLAG_BAND_NAME = "mountainShadowFlag"; + + private Product saoProduct; + + @Override + protected void prepareInputs() throws OperatorException { + super.prepareInputs(); + HashMap input = new HashMap<>(); + input.put("l1b", l1bProduct); + saoProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(IdepixMerisSlopeAspectOrientationOp.class), + GPF.NO_PARAMS, input); + } + + @Override + protected void configureTargetProduct(ProductConfigurer productConfigurer) { + super.configureTargetProduct(productConfigurer); + productConfigurer.addBand(MOUNTAIN_SHADOW_FLAG_BAND_NAME, ProductData.TYPE_INT8); + } + + @Override + protected void configureSourceSamples(SourceSampleConfigurer sampleConfigurer) throws OperatorException { + sampleConfigurer.defineSample(SZA_INDEX, IdepixMerisConstants.MERIS_SUN_ZENITH_BAND_NAME, l1bProduct); + sampleConfigurer.defineSample(SAA_INDEX, IdepixMerisConstants.MERIS_SUN_AZIMUTH_BAND_NAME, l1bProduct); + sampleConfigurer.defineSample(OZA_INDEX, IdepixMerisConstants.MERIS_VIEW_ZENITH_INTERPOLATED_BAND_NAME, + saoProduct); + sampleConfigurer.defineSample(OAA_INDEX, IdepixMerisConstants.MERIS_VIEW_AZIMUTH_INTERPOLATED_BAND_NAME, + saoProduct); + sampleConfigurer.defineSample(SLOPE_INDEX, IdepixMerisSlopeAspectOrientationOp.SLOPE_BAND_NAME, saoProduct); + sampleConfigurer.defineSample(ASPECT_INDEX, IdepixMerisSlopeAspectOrientationOp.ASPECT_BAND_NAME, saoProduct); + sampleConfigurer.defineSample(ORIENTATION_INDEX, IdepixMerisSlopeAspectOrientationOp.ORIENTATION_BAND_NAME, + saoProduct); + } + + @Override + protected void configureTargetSamples(TargetSampleConfigurer sampleConfigurer) throws OperatorException { + sampleConfigurer.defineSample(MOUNTAIN_SHADOW_FLAG_BAND_INDEX, MOUNTAIN_SHADOW_FLAG_BAND_NAME); + } + + @Override + protected void computePixel(int x, int y, Sample[] sourceSamples, WritableSample[] targetSamples) { + final float slope = sourceSamples[SLOPE_INDEX].getFloat(); + final float aspect = sourceSamples[ASPECT_INDEX].getFloat(); + if (!Float.isNaN(slope) && + !Float.isNaN(aspect)) { + final float sza = sourceSamples[SZA_INDEX].getFloat(); + final float saa = sourceSamples[SAA_INDEX].getFloat(); + final float oza = sourceSamples[OZA_INDEX].getFloat(); + final float oaa = sourceSamples[OAA_INDEX].getFloat(); + final float orientation = sourceSamples[ORIENTATION_INDEX].getFloat(); + + final PixelPos pixelPos = new PixelPos(x + 0.5f, y + 0.5f); + final GeoPos geoPos = l1bProduct.getSceneGeoCoding().getGeoPos(pixelPos, null); + final double saaApparent = IdepixMerisUtils.computeApparentSaa(sza, saa, oza, oaa, geoPos.getLat()); + + if (x == 2783 && y == 642) { + System.out.println("x, y = " + x + ", " + y); // small subset, shadow + } + if (x == 2783 && y == 2181) { + System.out.println("x, y = " + x + ", " + y); // large subset, no shadow + } + final boolean isMountainShadow = + isMountainShadow(sza, (float) saaApparent, slope, aspect, orientation, mntShadowExtent); + + if (isMountainShadow) { + System.out.println("x, y, slope, aspect, orientation = " + + x + ", " + y + ", " + slope + ", " + aspect + ", " + orientation); + } + targetSamples[MOUNTAIN_SHADOW_FLAG_BAND_INDEX].set(isMountainShadow); + } + } + + /* package local for testing */ + static boolean isMountainShadow(float sza, float saa, float slope, float aspect, float orientation, double mntShadowExtent) { + final double cosBeta = computeCosBeta(sza, saa, slope, aspect, orientation); + return cosBeta < (-1 * (1 - mntShadowExtent)); + } + + /* package local for testing */ + static double computeCosBeta(float sza, float saa, float slope, float aspect, float orientation) { + return Math.cos(sza * MathUtils.DTOR) * Math.cos(slope) + Math.sin(sza * MathUtils.DTOR) * Math.sin(slope) * + Math.cos(saa * MathUtils.DTOR - (aspect + orientation)); + } + + public static class Spi extends OperatorSpi { + public Spi() { + super(IdepixMerisMountainShadowOp.class); + } + } +} diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisOp.java index a87a4645..6df00007 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisOp.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisOp.java @@ -102,6 +102,13 @@ public class IdepixMerisOp extends BasisOp { description = " NN cloud ambiguous cloud sure/snow separation value") double schillerLandNNCloudSureSnowSeparationValue; + @Parameter(defaultValue = "true", label = " Compute mountain shadow") + private boolean computeMountainShadow; + + @Parameter(label = " Extent of mountain shadow", defaultValue = "0.9", interval = "[0,1]", + description = "Extent of mountain shadow detection") + private double mntShadowExtent; + @Parameter(defaultValue = "true", label = " Compute cloud shadow", description = " Compute cloud shadow with the algorithm from 'Fronts' project") @@ -137,10 +144,18 @@ public void initialize() throws OperatorException { throw new OperatorException(IdepixConstants.INPUT_INCONSISTENCY_ERROR_MESSAGE); } - if (IdepixIO.isMeris4thReprocessingL1bProduct(sourceProduct.getProductType())) { + final boolean isMeris4thReprocessingProduct = + IdepixIO.isMeris4thReprocessingL1bProduct(sourceProduct.getProductType()); + computeMountainShadow = computeMountainShadow && isMeris4thReprocessingProduct; + if (isMeris4thReprocessingProduct) { // adapt to 3rd reprocessing... Meris3rd4thReprocessingAdapter reprocessingAdapter = new Meris3rd4thReprocessingAdapter(); inputProductToProcess = reprocessingAdapter.convertToLowerVersion(sourceProduct); + if (computeMountainShadow) { + // altitude TPG is too coarse in this case! + ProductUtils.copyBand(IdepixMerisConstants.MERIS_4RP_ALTITUDE_BAND_NAME, sourceProduct, + inputProductToProcess, true); + } } else { inputProductToProcess = sourceProduct; } @@ -240,6 +255,7 @@ private void postProcess() { Map params = new HashMap<>(); params.put("computeCloudShadow", computeCloudShadow); + params.put("computeMountainShadow", computeMountainShadow); params.put("refineClassificationNearCoastlines", true); // always an improvement final Product classifiedProduct = GPF.createProduct(OperatorSpi.getOperatorAlias(IdepixMerisPostProcessOp.class), diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisPostProcessOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisPostProcessOp.java index b87021ba..bf05478e 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisPostProcessOp.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisPostProcessOp.java @@ -2,23 +2,21 @@ import com.bc.ceres.core.ProgressMonitor; import org.esa.snap.core.datamodel.*; -import org.esa.snap.core.gpf.Operator; -import org.esa.snap.core.gpf.OperatorException; -import org.esa.snap.core.gpf.OperatorSpi; -import org.esa.snap.core.gpf.Tile; +import org.esa.snap.core.gpf.*; import org.esa.snap.core.gpf.annotations.OperatorMetadata; import org.esa.snap.core.gpf.annotations.Parameter; import org.esa.snap.core.gpf.annotations.SourceProduct; import org.esa.snap.core.util.ProductUtils; import org.esa.snap.core.util.RectangleExtender; import org.esa.snap.dataio.envisat.EnvisatConstants; - import org.esa.snap.idepix.core.CloudShadowFronts; import org.esa.snap.idepix.core.IdepixConstants; import org.esa.snap.idepix.core.util.IdepixIO; import org.esa.snap.idepix.core.util.IdepixUtils; import java.awt.*; +import java.util.HashMap; +import java.util.Map; /** * Operator used to consolidate IdePix classification flag for MERIS: @@ -35,6 +33,13 @@ description = "Refines the MERIS pixel classification over both land and water.") public class IdepixMerisPostProcessOp extends Operator { + @Parameter(defaultValue = "true", label = " Compute mountain shadow") + private boolean computeMountainShadow; + + @Parameter(label = " Extent of mountain shadow", defaultValue = "0.9", interval = "[0,1]", + description = "Extent of mountain shadow detection") + private double mntShadowExtent; + @Parameter(defaultValue = "true", label = " Compute cloud shadow", description = " Compute cloud shadow with latest 'fronts' algorithm") @@ -58,10 +63,11 @@ public class IdepixMerisPostProcessOp extends Operator { private Band waterFractionBand; private Band origCloudFlagBand; private Band ctpBand; - private TiePointGrid szaTPG; - private TiePointGrid saaTPG; - private TiePointGrid altTPG; + private TiePointGrid szaTpg; + private TiePointGrid saaTpg; + private TiePointGrid altTpg; private GeoCoding geoCoding; + private Band mountainShadowFlagBand; private RectangleExtender rectCalculator; @@ -77,9 +83,13 @@ public void initialize() throws OperatorException { geoCoding = l1bProduct.getSceneGeoCoding(); origCloudFlagBand = merisCloudProduct.getBand(IdepixConstants.CLASSIF_BAND_NAME); - szaTPG = l1bProduct.getTiePointGrid(EnvisatConstants.MERIS_SUN_ZENITH_DS_NAME); - saaTPG = l1bProduct.getTiePointGrid(EnvisatConstants.MERIS_SUN_AZIMUTH_DS_NAME); - altTPG = l1bProduct.getTiePointGrid(EnvisatConstants.MERIS_DEM_ALTITUDE_DS_NAME); + szaTpg = l1bProduct.getTiePointGrid(EnvisatConstants.MERIS_SUN_ZENITH_DS_NAME); + saaTpg = l1bProduct.getTiePointGrid(EnvisatConstants.MERIS_SUN_AZIMUTH_DS_NAME); + altTpg = l1bProduct.getTiePointGrid(EnvisatConstants.MERIS_DEM_ALTITUDE_DS_NAME); + + final TiePointGrid latTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_LATITUDE_BAND_NAME); + final TiePointGrid lonTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_LONGITUDE_BAND_NAME); + if (ctpProduct != null) { ctpBand = ctpProduct.getBand("cloud_top_press"); } @@ -99,25 +109,45 @@ public void initialize() throws OperatorException { extendedWidth, extendedHeight ); + if (computeMountainShadow) { + ensureBandsAreCopied(l1bProduct, merisCloudProduct, latTpg.getName(), lonTpg.getName(), altTpg.getName()); + Map mntShadowParams = new HashMap<>(); + mntShadowParams.put("mntShadowStrength", mntShadowExtent); + + HashMap input = new HashMap<>(); + input.put("l1b", l1bProduct); + final Product mountainShadowProduct = GPF.createProduct( + OperatorSpi.getOperatorAlias(IdepixMerisMountainShadowOp.class), mntShadowParams, input); + mountainShadowFlagBand = mountainShadowProduct.getBand( + IdepixMerisMountainShadowOp.MOUNTAIN_SHADOW_FLAG_BAND_NAME); + } ProductUtils.copyBand(IdepixConstants.CLASSIF_BAND_NAME, merisCloudProduct, postProcessedCloudProduct, false); setTargetProduct(postProcessedCloudProduct); } + private void ensureBandsAreCopied(Product source, Product target, String... bandNames) { + for (String bandName : bandNames) { + if (!target.containsBand(bandName)) { + ProductUtils.copyBand(bandName, source, target, true); + } + } + } + @Override public void computeTile(Band targetBand, final Tile targetTile, ProgressMonitor pm) throws OperatorException { Rectangle targetRectangle = targetTile.getRectangle(); final Rectangle srcRectangle = rectCalculator.extend(targetRectangle); final Tile sourceFlagTile = getSourceTile(origCloudFlagBand, srcRectangle); - Tile szaTile = getSourceTile(szaTPG, srcRectangle); - Tile saaTile = getSourceTile(saaTPG, srcRectangle); + Tile szaTile = getSourceTile(szaTpg, srcRectangle); + Tile saaTile = getSourceTile(saaTpg, srcRectangle); Tile ctpTile = null; if (ctpBand != null) { ctpTile = getSourceTile(ctpBand, srcRectangle); } - Tile altTile = getSourceTile(altTPG, targetRectangle); + Tile altTile = getSourceTile(altTpg, targetRectangle); final Tile waterFractionTile = getSourceTile(waterFractionBand, srcRectangle); for (int y = srcRectangle.y; y < srcRectangle.y + srcRectangle.height; y++) { @@ -161,10 +191,7 @@ protected boolean isCloudForShadow(int x, int y) { is_cloud_current = targetTile.getSampleBit(x, y, IdepixConstants.IDEPIX_CLOUD); } if (is_cloud_current) { - final boolean isNearCoastline = isNearCoastline(x, y, waterFractionTile, srcRectangle); - if (!isNearCoastline) { - return true; - } + return !isNearCoastline(x, y, waterFractionTile, srcRectangle); } return false; } @@ -186,6 +213,17 @@ protected void setCloudShadow(int x, int y) { }; cloudShadowFronts.computeCloudShadow(); } + + if (computeMountainShadow) { + final Tile mountainShadowFlagTile = getSourceTile(mountainShadowFlagBand, targetRectangle); + for (int y = targetRectangle.y; y < targetRectangle.y + targetRectangle.height; y++) { + checkForCancellation(); + for (int x = targetRectangle.x; x < targetRectangle.x + targetRectangle.width; x++) { + final boolean mountainShadow = mountainShadowFlagTile.getSampleInt(x, y) > 0; + targetTile.setSample(x, y, IdepixMerisConstants.IDEPIX_MOUNTAIN_SHADOW, mountainShadow); + } + } + } } private void combineFlags(int x, int y, Tile sourceFlagTile, Tile targetTile) { diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java new file mode 100644 index 00000000..d6974fd1 --- /dev/null +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java @@ -0,0 +1,202 @@ +package org.esa.snap.idepix.meris; + +import com.bc.ceres.core.ProgressMonitor; +import org.esa.snap.core.datamodel.*; +import org.esa.snap.core.gpf.Operator; +import org.esa.snap.core.gpf.OperatorException; +import org.esa.snap.core.gpf.OperatorSpi; +import org.esa.snap.core.gpf.Tile; +import org.esa.snap.core.gpf.annotations.OperatorMetadata; +import org.esa.snap.core.gpf.annotations.SourceProduct; +import org.esa.snap.core.util.ProductUtils; +import org.esa.snap.idepix.core.util.SlopeAspectOrientationUtils; + +import javax.media.jai.BorderExtender; +import java.awt.*; +import java.util.Map; + +/** + * Computes Slope, Aspect and Orientation for a MERIS L1b product. + * Only 4RP, as this contains altitude band with original resolution. In 3RP we have only TPG. + * See theory e.g. at + * ..., or + * ... + * + * @author Tonio Fincke, Olaf Danne, Dagmar Mueller + */ +@OperatorMetadata(alias = "Idepix.Meris.SlopeAspect", + version = "2.0", + internal = true, + authors = "Tonio Fincke, Olaf Danne, Dagmar Mueller", + copyright = "(c) 2018-2021 by Brockmann Consult", + description = "Computes Slope, Aspect and Orientation for a Sentinel-3 OLCI product. " + + "See theory e.g. at https://www.e-education.psu.edu/geog480/node/490, or " + + "https://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-analyst-toolbox/how-hillshade-works.htm") +public class IdepixMerisSlopeAspectOrientationOp extends Operator { + + @SourceProduct(alias = "l1b") + private Product l1bProduct; + + private static final String ELEVATION_BAND_NAME = IdepixMerisConstants.MERIS_4RP_ALTITUDE_BAND_NAME; + + private double spatialResolution; + + private TiePointGrid latitudeTpg; + private TiePointGrid longitudeTpg; + private TiePointGrid sunAzimuthTpg; + private TiePointGrid viewZenithTpg; + private TiePointGrid viewAzimuthTpg; + private Band elevationBand; + private Band viewZenithBand; + private Band viewAzimuthBand; + private Band slopeBand; + private Band aspectBand; + private Band orientationBand; + private final static String TARGET_PRODUCT_NAME = "Slope-Aspect-Orientation"; + private final static String TARGET_PRODUCT_TYPE = "slope-aspect-orientation"; + final static String SLOPE_BAND_NAME = "slope"; + final static String ASPECT_BAND_NAME = "aspect"; + final static String ORIENTATION_BAND_NAME = "orientation"; + private final static String SLOPE_BAND_DESCRIPTION = "Slope of each pixel as angle"; + private final static String ASPECT_BAND_DESCRIPTION = + "Aspect of each pixel as angle between raster -Y direction and steepest slope, clockwise"; + private final static String ORIENTATION_BAND_DESCRIPTION = + "Orientation of each pixel as angle between east and raster X direction, clockwise"; + private final static String SLOPE_BAND_UNIT = "rad [0..pi/2]"; + private final static String ASPECT_BAND_UNIT = "rad [-pi..pi]"; + private final static String ORIENTATION_BAND_UNIT = "rad [-pi..pi]"; + + @Override + public void initialize() throws OperatorException { + + if (!(l1bProduct.containsBand(IdepixMerisConstants.MERIS_4RP_ALTITUDE_BAND_NAME))) { + throw new OperatorException("Slope/Aspect/Orientation requires altitude band at original resolution. " + + "Use MERIS 4RP input product"); + } + + ensureSingleRasterSize(l1bProduct); + GeoCoding sourceGeoCoding = l1bProduct.getSceneGeoCoding(); + if (sourceGeoCoding == null) { + throw new OperatorException("Source product has no geo-coding"); + } + + spatialResolution = SlopeAspectOrientationUtils.computeSpatialResolution(l1bProduct, sourceGeoCoding); + + elevationBand = l1bProduct.getBand(ELEVATION_BAND_NAME); + if (elevationBand == null) { + throw new OperatorException("Elevation band required to compute slope or aspect"); + } + latitudeTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_LATITUDE_BAND_NAME); + longitudeTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_LONGITUDE_BAND_NAME); + + Product targetProduct = createTargetProduct(); + + if (IdepixMerisUtils.isFullResolution(l1bProduct) || IdepixMerisUtils.isReducedResolution(l1bProduct)) { + IdepixMerisViewAngleInterpolationOp viewAngleInterpolationOp = new IdepixMerisViewAngleInterpolationOp(); + viewAngleInterpolationOp.setParameterDefaultValues(); + viewAngleInterpolationOp.setSourceProduct(l1bProduct); + Product viewAngleInterpolationProduct = viewAngleInterpolationOp.getTargetProduct(); + + viewZenithBand = viewAngleInterpolationProduct.getBand(IdepixMerisConstants.MERIS_VIEW_ZENITH_INTERPOLATED_BAND_NAME); + viewAzimuthBand = viewAngleInterpolationProduct.getBand(IdepixMerisConstants.MERIS_VIEW_AZIMUTH_INTERPOLATED_BAND_NAME); + ProductUtils.copyBand(IdepixMerisConstants.MERIS_VIEW_ZENITH_INTERPOLATED_BAND_NAME, + viewAngleInterpolationProduct, targetProduct, true); + ProductUtils.copyBand(IdepixMerisConstants.MERIS_VIEW_AZIMUTH_INTERPOLATED_BAND_NAME, + viewAngleInterpolationProduct, targetProduct, true); + } else { + viewZenithTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_VIEW_ZENITH_BAND_NAME); + viewAzimuthTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_VIEW_AZIMUTH_BAND_NAME); + } + + sunAzimuthTpg = l1bProduct.getTiePointGrid(IdepixMerisConstants.MERIS_SUN_AZIMUTH_BAND_NAME); + + setTargetProduct(targetProduct); + } + + private Band createBand(Product targetProduct, String bandName, String description, String unit) { + Band band = targetProduct.addBand(bandName, ProductData.TYPE_FLOAT32); + band.setDescription(description); + band.setUnit(unit); + band.setNoDataValue(-9999.); + band.setNoDataValueUsed(true); + return band; + } + + @Override + public void computeTileStack(Map targetTiles, Rectangle targetRectangle, ProgressMonitor pm) + throws OperatorException { + final Rectangle sourceRectangle = getSourceRectangle(targetRectangle); + final BorderExtender borderExtender = BorderExtender.createInstance(BorderExtender.BORDER_COPY); + final Tile latitudeTile = getSourceTile(latitudeTpg, sourceRectangle, borderExtender); + final Tile longitudeTile = getSourceTile(longitudeTpg, sourceRectangle, borderExtender); + final Tile elevationTile = getSourceTile(elevationBand, sourceRectangle, borderExtender); + Tile viewZenithAngleTile; + Tile viewAzimuthAngleTile; + if (IdepixMerisUtils.isFullResolution(l1bProduct) || IdepixMerisUtils.isReducedResolution(l1bProduct)) { + viewZenithAngleTile = getSourceTile(viewZenithBand, sourceRectangle, borderExtender); + viewAzimuthAngleTile = getSourceTile(viewAzimuthBand, sourceRectangle, borderExtender); + } else { + viewZenithAngleTile = getSourceTile(viewZenithTpg, sourceRectangle, borderExtender); + viewAzimuthAngleTile = getSourceTile(viewAzimuthTpg, sourceRectangle, borderExtender); + } + final Tile sunAzimuthAngleTile = getSourceTile(sunAzimuthTpg, sourceRectangle, borderExtender); + + final Tile slopeTile = targetTiles.get(slopeBand); + final Tile aspectTile = targetTiles.get(aspectBand); + final Tile orientationTile = targetTiles.get(orientationBand); + for (int y = targetRectangle.y; y < targetRectangle.y + targetRectangle.height; y++) { + for (int x = targetRectangle.x; x < targetRectangle.x + targetRectangle.width; x++) { + slopeTile.setSample(x, y, Float.NaN); + aspectTile.setSample(x, y, Float.NaN); + orientationTile.setSample(x, y, Float.NaN); + final float[] elevationDataMacropixel = SlopeAspectOrientationUtils.get3x3MacropixelData(elevationTile, y, x); + if (SlopeAspectOrientationUtils.is3x3ElevationDataValid(elevationDataMacropixel)) { + final float vza = viewZenithAngleTile.getSampleFloat(x, y); + final float vaa = viewAzimuthAngleTile.getSampleFloat(x, y); + final float saa = sunAzimuthAngleTile.getSampleFloat(x, y); + if (x == 2783 && y == 642) { + System.out.println("x, y = " + x + ", " + y); // small subset, shadow + } + final float[] latitudeDataMacropixel = + SlopeAspectOrientationUtils.get3x3MacropixelData(latitudeTile, y, x); + final float[] longitudeDataMacropixel = + SlopeAspectOrientationUtils.get3x3MacropixelData(longitudeTile, y, x); + final float orientation = SlopeAspectOrientationUtils.computeOrientation3x3Box(latitudeDataMacropixel, + longitudeDataMacropixel); + orientationTile.setSample(x, y, orientation); + final float[] slopeAspect = SlopeAspectOrientationUtils.computeSlopeAspect3x3(elevationDataMacropixel, + orientation, vza, vaa, saa, spatialResolution); + slopeTile.setSample(x, y, slopeAspect[0]); + aspectTile.setSample(x, y, slopeAspect[1]); + } + } + } + } + + private static Rectangle getSourceRectangle(Rectangle targetRectangle) { + return new Rectangle(targetRectangle.x - 1, targetRectangle.y - 1, + targetRectangle.width + 2, targetRectangle.height + 2); + } + + private Product createTargetProduct() { + final int sceneWidth = l1bProduct.getSceneRasterWidth(); + final int sceneHeight = l1bProduct.getSceneRasterHeight(); + Product targetProduct = new Product(TARGET_PRODUCT_NAME, TARGET_PRODUCT_TYPE, sceneWidth, sceneHeight); + ProductUtils.copyGeoCoding(l1bProduct, targetProduct); + targetProduct.setStartTime(l1bProduct.getStartTime()); + targetProduct.setEndTime(l1bProduct.getEndTime()); + + slopeBand = createBand(targetProduct, SLOPE_BAND_NAME, SLOPE_BAND_DESCRIPTION, SLOPE_BAND_UNIT); + aspectBand = createBand(targetProduct, ASPECT_BAND_NAME, ASPECT_BAND_DESCRIPTION, ASPECT_BAND_UNIT); + orientationBand = createBand(targetProduct, ORIENTATION_BAND_NAME, ORIENTATION_BAND_DESCRIPTION, ORIENTATION_BAND_UNIT); + + return targetProduct; + } + + + public static class Spi extends OperatorSpi { + public Spi() { + super(IdepixMerisSlopeAspectOrientationOp.class); + } + } +} diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java index cf95b947..23241192 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java @@ -1,5 +1,6 @@ package org.esa.snap.idepix.meris; +import org.apache.commons.math3.fitting.PolynomialFitter; import org.esa.s3tbx.processor.rad2refl.Rad2ReflConstants; import org.esa.s3tbx.processor.rad2refl.Rad2ReflOp; import org.esa.s3tbx.processor.rad2refl.Sensor; @@ -11,6 +12,7 @@ import org.esa.snap.core.util.BitSetter; import org.esa.snap.core.util.ProductUtils; +import org.esa.snap.core.util.math.MathUtils; import org.esa.snap.idepix.core.IdepixConstants; import org.esa.snap.idepix.core.IdepixFlagCoding; @@ -33,8 +35,13 @@ class IdepixMerisUtils { static FlagCoding createMerisFlagCoding() { FlagCoding flagCoding = IdepixFlagCoding.createDefaultFlagCoding(IdepixConstants.CLASSIF_BAND_NAME); - flagCoding.addFlag("IDEPIX_GLINT_RISK", BitSetter.setFlag(0, IdepixMerisConstants.IDEPIX_GLINT_RISK), - IdepixMerisConstants.IDEPIX_GLINT_RISK_DESCR_TEXT); + flagCoding.addFlag("IDEPIX_MOUNTAIN_SHADOW", BitSetter.setFlag(0, + IdepixMerisConstants.IDEPIX_MOUNTAIN_SHADOW), + IdepixMerisConstants.IDEPIX_MOUNTAIN_SHADOW_DESCR_TEXT); + + flagCoding.addFlag("IDEPIX_GLINT_RISK", BitSetter.setFlag(0, + IdepixMerisConstants.IDEPIX_GLINT_RISK), + IdepixMerisConstants.IDEPIX_GLINT_RISK_DESCR_TEXT); return flagCoding; } @@ -52,9 +59,14 @@ static void setupMerisClassifBitmask(Product classifProduct) { Mask mask; Random r = new Random(1234567); + mask = Mask.BandMathsType.create("IDEPIX_MOUNTAIN_SHADOW", IdepixMerisConstants.IDEPIX_MOUNTAIN_SHADOW_DESCR_TEXT, w, h, + "pixel_classif_flags.IDEPIX_MOUNTAIN_SHADOW", + IdepixFlagCoding.getRandomColour(r), 0.5f); + classifProduct.getMaskGroup().add(index++, mask); + mask = Mask.BandMathsType.create("IDEPIX_GLINT_RISK", IdepixMerisConstants.IDEPIX_GLINT_RISK_DESCR_TEXT, w, h, - "pixel_classif_flags.IDEPIX_GLINT_RISK", - IdepixFlagCoding.getRandomColour(r), 0.5f); + "pixel_classif_flags.IDEPIX_GLINT_RISK", + IdepixFlagCoding.getRandomColour(r), 0.5f); classifProduct.getMaskGroup().add(index, mask); } @@ -84,4 +96,65 @@ static Product computeCloudTopPressureProduct(Product sourceProduct) { return GPF.createProduct("Meris.CloudTopPressureOp", GPF.NO_PARAMS, sourceProduct); } + static double computeApparentSaa(double sza, double saa, double oza, double oaa, double lat) { + final double szaRad = sza * MathUtils.DTOR; + final double ozaRad = oza * MathUtils.DTOR; + + double deltaPhi; + if (oaa < 0.0) { + deltaPhi = 360.0 - Math.abs(oaa) - saa; + } else { + deltaPhi = saa - oaa; + } + final double deltaPhiRad = deltaPhi * MathUtils.DTOR; + final double numerator = Math.tan(szaRad) - Math.tan(ozaRad) * Math.cos(deltaPhiRad); + final double denominator = Math.sqrt(Math.tan(ozaRad) * Math.tan(ozaRad) + Math.tan(szaRad) * Math.tan(szaRad) - + 2.0 * Math.tan(szaRad) * Math.tan(ozaRad) * Math.cos(deltaPhiRad)); + + double delta = Math.acos(numerator / denominator); +// Sun in the North (Southern hemisphere), change sign! + if (saa > 270. || saa < 90){ + delta = -1.0 * delta; + } + if (oaa < 0.0) { + return saa - delta * MathUtils.RTOD; + } else { + return saa + delta * MathUtils.RTOD; + } + } + + static boolean isFullResolution(Product sourceProduct) { + // less strict to allow subsets: + return sourceProduct.getProductType().contains("_FR"); + } + + static boolean isReducedResolution(Product sourceProduct) { + // less strict to allow subsets: + return sourceProduct.getProductType().contains("_RR"); + } + static float[] interpolateViewAngles(PolynomialFitter curveFitter1, PolynomialFitter curveFitter2, + float[] viewAngleOrig, int[] nx, int nxChange) { + float[] viewAngleInterpol = viewAngleOrig.clone(); + + curveFitter1.clearObservations(); + for (int x = nx[0]; x < nx[1]; x++) { + curveFitter1.addObservedPoint(x * 1.0f, viewAngleOrig[x]); + } + final double[] fit1 = curveFitter1.fit(IdepixMerisConstants.POLYNOM_FIT_INITIAL); + + curveFitter2.clearObservations(); + for (int x = nx[2]; x < nx[3]; x++) { + curveFitter2.addObservedPoint(x * 1.0f, viewAngleOrig[x]); + } + final double[] fit2 = curveFitter2.fit(IdepixMerisConstants.POLYNOM_FIT_INITIAL); + + for (int x = nx[1]; x < nxChange; x++) { + viewAngleInterpol[x] = (float) (fit1[0] + fit1[1] * x + fit1[2] * x * x); + } + for (int x = nxChange; x < nx[2]; x++) { + viewAngleInterpol[x] = (float) (fit2[0] + fit2[1] * x + fit2[2] * x * x); + } + + return viewAngleInterpol; + } } diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisViewAngleInterpolationOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisViewAngleInterpolationOp.java new file mode 100644 index 00000000..15bb5954 --- /dev/null +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisViewAngleInterpolationOp.java @@ -0,0 +1,189 @@ +package org.esa.snap.idepix.meris; + +import com.bc.ceres.core.ProgressMonitor; +import org.apache.commons.math3.fitting.PolynomialFitter; +import org.apache.commons.math3.optim.nonlinear.vector.jacobian.LevenbergMarquardtOptimizer; +import org.esa.snap.core.datamodel.Band; +import org.esa.snap.core.datamodel.Product; +import org.esa.snap.core.datamodel.ProductData; +import org.esa.snap.core.datamodel.TiePointGrid; +import org.esa.snap.core.gpf.Operator; +import org.esa.snap.core.gpf.OperatorException; +import org.esa.snap.core.gpf.OperatorSpi; +import org.esa.snap.core.gpf.Tile; +import org.esa.snap.core.gpf.annotations.OperatorMetadata; +import org.esa.snap.core.gpf.annotations.SourceProduct; +import org.esa.snap.core.util.ProductUtils; +import org.esa.snap.idepix.core.AlgorithmSelector; +import org.esa.snap.idepix.core.IdepixConstants; +import org.esa.snap.idepix.core.util.IdepixIO; + +import java.awt.*; +import java.util.Map; + +/** + * Performs interpolation at view zenith and azimuth discontinuities for MERIS L1b input products. + * + * @author olafd + */ +@OperatorMetadata(alias = "MerisViewAngleInterpolation", version = "0.6", + authors = "Olaf Danne, Dagmar Müller (Brockmann Consult)", + internal = true, + category = "Optical/Preprocessing/Masking/IdePix (Clouds, Land, Water, ...)", + copyright = "Copyright (C) 2023 by Brockmann Consult", + description = "Performs interpolation at view zenith and azimuth discontinuities for MERIS L1b input products.") +public class IdepixMerisViewAngleInterpolationOp extends Operator { + + @SourceProduct(description = "Input product", + label = "MERIS L1b product") + private Product sourceProduct; + + private int nxChange = -1; + private int[] nx_vza; + private int[] nx_vaa; + + private Band vzaInterpolBand; + private Band vaaInterpolBand; + + + @Override + public void initialize() throws OperatorException { + + final boolean inputProductIsValid = IdepixIO.validateInputProduct(sourceProduct, AlgorithmSelector.MERIS); + if (!inputProductIsValid) { + throw new OperatorException(IdepixConstants.INPUT_INCONSISTENCY_ERROR_MESSAGE); + } + + setInterpolationIntervals(); + + Product targetProduct = createTargetProduct(); + + setTargetProduct(targetProduct); + } + + @Override + public void computeTileStack(Map targetTiles, Rectangle targetRectangle, ProgressMonitor pm) throws OperatorException { + + try { + final Tile vzaTile = + getSourceTile(sourceProduct.getTiePointGrid(IdepixMerisConstants.MERIS_VIEW_ZENITH_BAND_NAME), + targetRectangle); + final Tile vaaTile = + getSourceTile(sourceProduct.getTiePointGrid(IdepixMerisConstants.MERIS_VIEW_AZIMUTH_BAND_NAME), + targetRectangle); + + final Tile vzaInterpolTile = targetTiles.get(vzaInterpolBand); + final Tile vaaInterpolTile = targetTiles.get(vaaInterpolBand); + + float[] vzaOrigLine = new float[targetRectangle.width]; + float[] vaaOrigLine = new float[targetRectangle.width]; + + PolynomialFitter curveFitter1 = new PolynomialFitter(new LevenbergMarquardtOptimizer()); + PolynomialFitter curveFitter2 = new PolynomialFitter(new LevenbergMarquardtOptimizer()); + + for (int y = targetRectangle.y; y < targetRectangle.y + targetRectangle.height; y++) { + checkForCancellation(); + + for (int x = 0; x < targetRectangle.width; x++) { + vzaOrigLine[x] = vzaTile.getSampleFloat(x, y); + vaaOrigLine[x] = vaaTile.getSampleFloat(x, y); + } + + if (nxChange != -1 && nx_vza[1] > 0 && nx_vza[2] < targetRectangle.width && + nx_vaa[1] > 0 && nx_vaa[2] < targetRectangle.width) { + // we need a sufficient product width to do interpolation... + float[] vzaInterpolLine = IdepixMerisUtils.interpolateViewAngles(curveFitter1, curveFitter2, + vzaOrigLine, nx_vza, nxChange); + float[] vaaInterpolLine = IdepixMerisUtils.interpolateViewAngles(curveFitter1, curveFitter2, + vaaOrigLine, nx_vaa, nxChange); + for (int x = 0; x < targetRectangle.width; x++) { + vzaInterpolTile.setSample(x, y, vzaInterpolLine[x]); + vaaInterpolTile.setSample(x, y, vaaInterpolLine[x]); + } + } else { + for (int x = 0; x < targetRectangle.width; x++) { + vzaInterpolTile.setSample(x, y, vzaOrigLine[x]); + vaaInterpolTile.setSample(x, y, vaaOrigLine[x]); + } + } + } + } catch (Exception e) { + throw new OperatorException(e); + } + } + + private Product createTargetProduct() { + final int w = sourceProduct.getSceneRasterWidth(); + final int h = sourceProduct.getSceneRasterHeight(); + Product targetProduct = new Product(sourceProduct.getName(), sourceProduct.getProductType(), w, h); + targetProduct.setPreferredTileSize(w, 16); // we need tiles over whole source width + + ProductUtils.copyMetadata(sourceProduct, targetProduct); + ProductUtils.copyGeoCoding(sourceProduct, targetProduct); + targetProduct.setStartTime(sourceProduct.getStartTime()); + targetProduct.setEndTime(sourceProduct.getEndTime()); + + vzaInterpolBand = createInterpolatedBand(targetProduct, IdepixMerisConstants.MERIS_VIEW_ZENITH_INTERPOLATED_BAND_NAME, + "Interpolated OZA"); + vaaInterpolBand = createInterpolatedBand(targetProduct, IdepixMerisConstants.MERIS_VIEW_AZIMUTH_INTERPOLATED_BAND_NAME, + "Interpolated OAA"); + + return targetProduct; + } + + private Band createInterpolatedBand(Product targetProduct, String bandName, String description) { + Band band = targetProduct.addBand(bandName, ProductData.TYPE_FLOAT32); + band.setDescription(description); + band.setUnit("deg"); + band.setNoDataValue(-9999.); + band.setNoDataValueUsed(true); + return band; + } + + private void setInterpolationIntervals() { + final int productWidth = sourceProduct.getSceneRasterWidth(); + final boolean isFullResolution = IdepixMerisUtils.isFullResolution(sourceProduct); + if (isFullResolution && productWidth == IdepixMerisConstants.MERIS_FR_FULL_PRODUCT_WIDTH) { + // full width, full resolution + nxChange = IdepixMerisConstants.MERIS_FR_DEFAULT_NX_CHANGE; + nx_vza = IdepixMerisConstants.MERIS_DEFAULT_FR_NX_VZA; + nx_vaa = IdepixMerisConstants.MERIS_DEFAULT_FR_NX_VAA; + } else if (!isFullResolution && productWidth == IdepixMerisConstants.MERIS_RR_FULL_PRODUCT_WIDTH) { + // full width, reduced resolution + nxChange = IdepixMerisConstants.MERIS_RR_DEFAULT_NX_CHANGE; + nx_vza = IdepixMerisConstants.MERIS_DEFAULT_RR_NX_VZA; + nx_vaa = IdepixMerisConstants.MERIS_DEFAULT_RR_NX_VAA; + } else { + // subset: find discontinuity + final TiePointGrid vaaTpg = sourceProduct.getTiePointGrid(IdepixMerisConstants.MERIS_VIEW_AZIMUTH_BAND_NAME); + final float[] vaaProfile = (float[]) vaaTpg.getRasterData().getElems(); + for (int i = 0; i < productWidth -2; i++) { + if (vaaProfile[i+1] < 0.0 && vaaProfile[i] > 0.0) { + nxChange = i+2; + break; + } + } + + if (nxChange != -1) { + nx_vza = new int[4]; + nx_vaa = new int[4]; + nx_vza[0] = (int) (0.97*nxChange); + nx_vza[1] = (int) (0.99*nxChange); + nx_vza[2] = Math.min(productWidth, (int) (1.01*nxChange)); + nx_vza[3] = Math.min(productWidth, (int) (1.03*nxChange)); + nx_vaa[0] = (int) (0.55*nxChange); + nx_vaa[1] = (int) (0.94*nxChange); + nx_vaa[2] = Math.min(productWidth, (int) (1.08*nxChange)); + nx_vaa[3] = productWidth; + } + + } + } + + public static class Spi extends OperatorSpi { + + public Spi() { + super(IdepixMerisViewAngleInterpolationOp.class); + } + } +} \ No newline at end of file diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapter.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapter.java deleted file mode 100644 index 410a050c..00000000 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapter.java +++ /dev/null @@ -1,406 +0,0 @@ -package org.esa.snap.idepix.meris.reprocessing; - -import com.bc.ceres.core.ProgressMonitor; -import com.bc.ceres.glevel.MultiLevelImage; -import org.esa.snap.core.datamodel.*; -import org.esa.snap.core.util.BitSetter; -import org.esa.snap.core.util.ProductUtils; -import org.esa.snap.dataio.envisat.EnvisatConstants; - -import javax.media.jai.RenderedOp; -import javax.media.jai.operator.MeanDescriptor; -import java.awt.*; -import java.awt.image.renderable.ParameterBlock; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -/** - * Class providing the adaptation of a MERIS L1b product from fourth to third reprocessing version. - * - * @author olafd - */ -public class Meris3rd4thReprocessingAdapter implements ReprocessingAdapter { - - private final Map qualityToL1FlagMap = new HashMap<>(); - - public Meris3rd4thReprocessingAdapter() { - setupQualityToL1FlagMap(); - } - - @Override - public Product convertToLowerVersion(Product inputProduct) { - Product thirdReproProduct = new Product(inputProduct.getName(), - inputProduct.getProductType(), - inputProduct.getSceneRasterWidth(), - inputProduct.getSceneRasterHeight()); - - ProductUtils.copyGeoCoding(inputProduct, thirdReproProduct); - adaptProductInformationToThirdRepro(inputProduct, thirdReproProduct); - - // adapt band names: - // ## M%02d_radiance --> radiance_%d - // ## detector_index --> detector_index - adaptBandNamesToThirdRepro(inputProduct, thirdReproProduct); - - // adapt tie point grids: - // TP_latitude --> latitude - // TP_longitude --> longitude - // TP_altitude --> dem_alt // todo: clarify this! - // n.a. --> dem_rough // todo: clarify this! - // n.a. --> lat_corr // todo: clarify this! - // n.a. --> lon_corr // todo: clarify this! - // SZA --> sun_zenith - // OZA --> view_zenith - // SAA --> sun_azimuth - // OAA --> view_azimuth - // horiz_wind_vector_1 --> zonal_wind - // horiz_wind_vector_2 --> merid_wind - // sea_level_pressure --> atm_press - // total_ozone --> ozone , convert units - // humidity_pressure_level_14 --> rel_hum // relative humidity at 850 hPa - adaptTiePointGridsToThirdRepro(inputProduct, thirdReproProduct); - - // adapt flag band and coding: - // quality_flags --> l1_flags - // ## cosmetic (2^24) --> COSMETIC (1) - // ## duplicated (2^23) --> DUPLICATED (2) - // ## sun_glint_risk (2^22) --> GLINT_RISK (4) - // ## dubious (2^21) --> SUSPECT (8) - // ## land (2^31) --> LAND_OCEAN (16) - // ## bright (2^27) --> BRIGHT (32) - // ## coastline (2^30) --> COASTLINE (64) - // ## invalid (2^2^25) --> INVALID (128) - try { - adaptFlagBandToThirdRepro(inputProduct, thirdReproProduct); - } catch (IOException e) { - e.printStackTrace(); - } - - // adapt metadata: - // Idepix MERIS does not use any product metadata - // --> for the moment, copy just a few elements from element metadataSection: - Meris3rd4thReprocessingMetadata.fillMetadataInThirdRepro(inputProduct, thirdReproProduct); - - return thirdReproProduct; - } - - @Override - public Product convertToHigherVersion(Product inputProduct) { - // todo: implement - return null; - } - - /* package local for testing */ - int convertQualityToL1FlagValue(long qualityFlagValue) { - int l1FlagValue = 0; - for (int i = 0; i < Integer.SIZE; i++) { - if (qualityToL1FlagMap.containsKey(i) && BitSetter.isFlagSet(qualityFlagValue, i)) { - l1FlagValue += qualityToL1FlagMap.get(i); - } - } - return l1FlagValue; - } - - /* package local for testing */ - static float getMeanSolarFluxFrom4thReprocessing(Band solarFluxBand) { - final MultiLevelImage sourceImage = solarFluxBand.getSourceImage(); - ParameterBlock pb = new ParameterBlock(); - pb.addSource(sourceImage);// The source image - pb.add(null); // null ROI means whole image - pb.add(1); // check every pixel horizontally - pb.add(1); // check every pixel vertically - - // Perform the mean operation on the source image. - final RenderedOp meanImage = MeanDescriptor.create(sourceImage, null, 1, 1, null); - double[] mean = (double[]) meanImage.getProperty("mean"); - - return (float) mean[0]; - } - - private void adaptProductInformationToThirdRepro(Product inputProduct, Product thirdReproProduct) { - // we often need the start/stop times for downstream processors... - thirdReproProduct.setStartTime(inputProduct.getStartTime()); - thirdReproProduct.setEndTime(inputProduct.getEndTime()); - // todo: discuss what else we want to set back to 3RP here - thirdReproProduct.setPreferredTileSize(inputProduct.getPreferredTileSize()); - - if (isMerisRR(inputProduct)) { - // we often need the 3RP product type for downstream processors (e.g. for loading MER Auxdata) - thirdReproProduct.setProductType("MER_RR__1P"); - } else if (isMerisFR(inputProduct)) { - thirdReproProduct.setProductType("MER_FRG_1P"); - } - } - - private boolean isMerisRR(Product inputProduct) { - return inputProduct.getProductType().startsWith("ME_1_R"); - } - - private boolean isMerisFR(Product inputProduct) { - return inputProduct.getProductType().startsWith("ME_1_F"); - } - - private void adaptFlagBandToThirdRepro(Product inputProduct, Product thirdReproProduct) throws IOException { - - Band l1FlagBand = createL1bFlagBand(thirdReproProduct); - setupL1FlagsBitmask(thirdReproProduct); - - final Band qualityFlagBand = inputProduct.getBand("quality_flags"); - final int width = inputProduct.getSceneRasterWidth(); - final int height = inputProduct.getSceneRasterHeight(); - int[] qualityFlagData = new int[width * height]; - int[] l1FlagData = new int[width * height]; - qualityFlagBand.readPixels(0, 0, width, height, qualityFlagData, ProgressMonitor.NULL); - for (int i = 0; i < width * height; i++) { - l1FlagData[i] = convertQualityToL1FlagValue(qualityFlagData[i]); - } - - l1FlagBand.ensureRasterData(); - l1FlagBand.setPixels(0, 0, width, height, l1FlagData); - l1FlagBand.setSourceImage(l1FlagBand.getSourceImage()); - thirdReproProduct.addBand(l1FlagBand); - } - - private void adaptTiePointGridsToThirdRepro(Product inputProduct, Product thirdReproProduct) { - // adapt tie point grids: - // TP_latitude --> latitude - // TP_longitude --> longitude - // TP_altitude --> dem_alt // todo: clarify this! - // n.a. --> dem_rough // todo: clarify this! - // n.a. --> lat_corr // todo: clarify this! - // n.a. --> lon_corr // todo: clarify this! - // SZA --> sun_zenith - // OZA --> view_zenith - // SAA --> sun_azimuth - // OAA --> view_azimuth - // horizontal_wind_vector_1 --> zonal_wind - // horizontal_wind_vector_2 --> merid_wind - // sea_level_pressure --> atm_press - // total_ozone --> ozone // todo: check units! - // humidity_pressure_level_14 --> rel_hum // relative humidity at 850 hPa - - if (!thirdReproProduct.containsTiePointGrid("TP_latitude")) { - ProductUtils.copyTiePointGrid("TP_latitude", inputProduct, thirdReproProduct); - } - final TiePointGrid latitudeTargetTPG = thirdReproProduct.getTiePointGrid("TP_latitude"); - latitudeTargetTPG.setName("latitude"); - latitudeTargetTPG.setDescription("Latitude of the tie points (WGS-84), positive N"); - latitudeTargetTPG.setUnit("deg"); - - if (!thirdReproProduct.containsTiePointGrid("TP_longitude")) { - ProductUtils.copyTiePointGrid("TP_longitude", inputProduct, thirdReproProduct); - } - final TiePointGrid longitudeTargetTPG = thirdReproProduct.getTiePointGrid("TP_longitude"); - longitudeTargetTPG.setName("longitude"); - longitudeTargetTPG.setDescription("Longitude of the tie points (WGS-84), Greenwich origin, positive E"); - longitudeTargetTPG.setUnit("deg"); - - final TiePointGrid altitudeTargetTPG = - ProductUtils.copyTiePointGrid("TP_altitude", inputProduct, thirdReproProduct); - altitudeTargetTPG.setName("dem_alt"); - altitudeTargetTPG.setDescription("Digital elevation model altitude"); - - final TiePointGrid szaTargetTPG = - ProductUtils.copyTiePointGrid("SZA", inputProduct, thirdReproProduct); - szaTargetTPG.setName("sun_zenith"); - szaTargetTPG.setDescription("Viewing zenith angles"); - szaTargetTPG.setUnit("deg"); - - final TiePointGrid saaTargetTPG = - ProductUtils.copyTiePointGrid("SAA", inputProduct, thirdReproProduct); - saaTargetTPG.setName("sun_azimuth"); - saaTargetTPG.setDescription("Sun azimuth angles"); - saaTargetTPG.setUnit("deg"); - // SAA: 4th --> 3rd: if (saa <= 0.0) then saa += 360.0 - final float[] saaTiePoints = saaTargetTPG.getTiePoints(); - for (int i = 0; i < saaTiePoints.length; i++) { - if (saaTiePoints[i] <= 0.0) { - saaTiePoints[i] += 360.0; - } - } - - final TiePointGrid vzaTargetTPG = - ProductUtils.copyTiePointGrid("OZA", inputProduct, thirdReproProduct); - vzaTargetTPG.setName("view_zenith"); - - final TiePointGrid vaaTargetTPG = - ProductUtils.copyTiePointGrid("OAA", inputProduct, thirdReproProduct); - vaaTargetTPG.setName("view_azimuth"); - vaaTargetTPG.setDescription("Viewing azimuth angles"); - vaaTargetTPG.setUnit("deg"); - // OAA: 4th --> 3rd: if (oaa <= 0.0) then oaa += 360.0 - final float[] vaaTiePoints = vaaTargetTPG.getTiePoints(); - for (int i = 0; i < vaaTiePoints.length; i++) { - if (vaaTiePoints[i] <= 0.0) { - vaaTiePoints[i] += 360.0; - } - } - - final TiePointGrid zonalWindTPG = - ProductUtils.copyTiePointGrid("horizontal_wind_vector_1", inputProduct, thirdReproProduct); - zonalWindTPG.setName("zonal_wind"); - zonalWindTPG.setDescription("Zonal wind"); - - final TiePointGrid meridionalWindTPG = - ProductUtils.copyTiePointGrid("horizontal_wind_vector_2", inputProduct, thirdReproProduct); - meridionalWindTPG.setName("merid_wind"); - meridionalWindTPG.setDescription("Meridional wind"); - - final TiePointGrid atmPressTPG = - ProductUtils.copyTiePointGrid("sea_level_pressure", inputProduct, thirdReproProduct); - atmPressTPG.setName("atm_press"); - - final TiePointGrid ozoneTPG = - ProductUtils.copyTiePointGrid("total_ozone", inputProduct, thirdReproProduct); - ozoneTPG.setName("ozone"); - ozoneTPG.setDescription("Total ozone"); - ozoneTPG.setUnit("DU"); - double conversionFactor = 1.0 / 2.1415e-5; // convert from kg/m2 to DU, https://sacs.aeronomie.be/info/dobson.php - final float[] ozoneTiePoints = ozoneTPG.getTiePoints(); - for (int i = 0; i < ozoneTiePoints.length; i++) { - ozoneTiePoints[i] *= conversionFactor; - } - - // relative humidity: take humidity_pressure_level_14 which - // corresponds to rel_hum in 3RP, which is at 850hPa according to MERIS Product Handbook v2.1, page 52, - // https://earth.esa.int/pub/ESA_DOC/ENVISAT/MERIS/meris.ProductHandbook.2_1.pdf) - final TiePointGrid relHumTPG = - ProductUtils.copyTiePointGrid("humidity_pressure_level_14", inputProduct, thirdReproProduct); - relHumTPG.setName("rel_hum"); - relHumTPG.setDescription("Relative humidity"); - } - - private void adaptBandNamesToThirdRepro(Product inputProduct, Product thirdReproProduct) { - // ## detector_index --> detector_index - // ## M%02d_radiance --> radiance_%d - Band detectorIndexTargetBand = - ProductUtils.copyBand(EnvisatConstants.MERIS_DETECTOR_INDEX_DS_NAME, - inputProduct, thirdReproProduct, true); - final Band detectorIndexSourceBand = inputProduct.getBand(EnvisatConstants.MERIS_DETECTOR_INDEX_DS_NAME); - detectorIndexTargetBand.setDescription(detectorIndexSourceBand.getDescription()); - detectorIndexTargetBand.setNoDataValueUsed(false); - detectorIndexTargetBand.setNoDataValue(0.0); - detectorIndexTargetBand.setValidPixelExpression(null); - - for (int i = 0; i < EnvisatConstants.MERIS_L1B_NUM_SPECTRAL_BANDS; i++) { - final String inputBandName = "M" + String.format("%02d", i + 1) + "_radiance"; - final String thirdReproBandName = EnvisatConstants.MERIS_L1B_SPECTRAL_BAND_NAMES[i]; - final Band inputRadianceBand = inputProduct.getBand(inputBandName); - final Band targetRadianceBand = ProductUtils.copyBand(inputBandName, inputProduct, - thirdReproBandName, thirdReproProduct, true); - copyGeneralBandProperties(inputRadianceBand, targetRadianceBand); - copySpectralBandProperties(inputRadianceBand, targetRadianceBand); - - // set solar flux: - final Band solarFluxBand = inputProduct.getBand("solar_flux_band_" + (i + 1)); - // todo: solar flux is not needed for Idepix, but later for general adapter. Discuss how to set. - // For the moment, compute the mean of the corresponding solar flux band image. - // Later, implement GK suggestion - targetRadianceBand.setSolarFlux(getMeanSolarFluxFrom4thReprocessing(solarFluxBand)); - - // set valid pixel expression: - targetRadianceBand.setValidPixelExpression("!l1_flags.INVALID"); - } - } - - private static void copyGeneralBandProperties(Band sourceBand, Band targetBand) { - targetBand.setUnit(sourceBand.getUnit()); - } - - private static void copySpectralBandProperties(Band sourceBand, Band targetBand) { - targetBand.setDescription("TOA radiance band " + sourceBand.getSpectralBandIndex()); - targetBand.setSpectralBandIndex(sourceBand.getSpectralBandIndex()); - targetBand.setSpectralWavelength(sourceBand.getSpectralWavelength()); - targetBand.setSpectralBandwidth(sourceBand.getSpectralBandwidth()); - targetBand.setNoDataValueUsed(false); - targetBand.setNoDataValue(0.0); - } - - private static Band createL1bFlagBand(Product thirdReproProduct) { - - Band l1FlagBand = new Band("l1_flags", ProductData.TYPE_INT32, - thirdReproProduct.getSceneRasterWidth(), thirdReproProduct.getSceneRasterHeight()); - l1FlagBand.setDescription("Level 1b classification and quality flags"); - - FlagCoding l1FlagCoding = new FlagCoding("l1_flags"); - l1FlagCoding.addFlag("COSMETIC", BitSetter.setFlag(0, 0), "Pixel is cosmetic"); - l1FlagCoding.addFlag("DUPLICATED", BitSetter.setFlag(0, 1), "Pixel has been duplicated (filled in)"); - l1FlagCoding.addFlag("GLINT_RISK", BitSetter.setFlag(0, 2), "Pixel has glint risk"); - l1FlagCoding.addFlag("SUSPECT", BitSetter.setFlag(0, 3), "Pixel is suspect"); - l1FlagCoding.addFlag("LAND_OCEAN", BitSetter.setFlag(0, 4), "Pixel is over land, not ocean"); - l1FlagCoding.addFlag("BRIGHT", BitSetter.setFlag(0, 5), "Pixel is bright"); - l1FlagCoding.addFlag("COASTLINE", BitSetter.setFlag(0, 6), "Pixel is part of a coastline"); - l1FlagCoding.addFlag("INVALID", BitSetter.setFlag(0, 7), "Pixel is invalid"); - - l1FlagBand.setSampleCoding(l1FlagCoding); - thirdReproProduct.getFlagCodingGroup().add(l1FlagCoding); - - return l1FlagBand; - } - - private void setupQualityToL1FlagMap() { - // quality_flags --> l1_flags - // ## cosmetic (2^24) --> COSMETIC (1) - // ## duplicated (2^23) --> DUPLICATED (2) - // ## sun_glint_risk (2^22) --> GLINT_RISK (4) - // ## dubious (2^21) --> SUSPECT (8) - // ## land (2^31) --> LAND_OCEAN (16) - // ## bright (2^27) --> BRIGHT (32) - // ## coastline (2^30) --> COASTLINE (64) - // ## invalid (2^25) --> INVALID (128) - qualityToL1FlagMap.put(24, 1); - qualityToL1FlagMap.put(23, 2); - qualityToL1FlagMap.put(22, 4); - qualityToL1FlagMap.put(21, 8); - qualityToL1FlagMap.put(31, 16); - qualityToL1FlagMap.put(27, 32); - qualityToL1FlagMap.put(30, 64); - qualityToL1FlagMap.put(25, 128); - } - - private static void setupL1FlagsBitmask(Product thirdReproProduct) { - - int index = 0; - int w = thirdReproProduct.getSceneRasterWidth(); - int h = thirdReproProduct.getSceneRasterHeight(); - Mask mask; - - mask = Mask.BandMathsType.create("cosmetic", "Pixel is cosmetic", w, h, - "l1_flags.COSMETIC", new Color(204, 153, 255), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("duplicated", "Pixel has been duplicated (filled in)", w, h, - "l1_flags.DUPLICATED", new Color(255, 200, 0), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("glint_risk", "Pixel has glint risk", w, h, - "l1_flags.GLINT_RISK", new Color(255, 0, 255), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("suspect", "Pixel is suspect", w, h, - "l1_flags.SUSPECT", new Color(204, 102, 255), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("land", "Pixel is over land, not ocean", w, h, - "l1_flags.LAND_OCEAN", new Color(51, 153, 0), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("water", "Pixel is over ocean, not land", w, h, - "not l1_flags.LAND_OCEAN", new Color(153, 153, 255), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("bright", "Pixel is bright", w, h, - "l1_flags.BRIGHT", new Color(255, 255, 0), 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("coastline", "Pixel is part of a coastline", w, h, - "l1_flags.COASTLINE", Color.GREEN, 0.5f); - thirdReproProduct.getMaskGroup().add(index++, mask); - - mask = Mask.BandMathsType.create("invalid", "Pixel is invaid", w, h, - "l1_flags.INVALID", Color.RED, 0.5f); - thirdReproProduct.getMaskGroup().add(index, mask); - } - -} diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadata.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadata.java deleted file mode 100644 index 24c6f4f2..00000000 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadata.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.esa.snap.idepix.meris.reprocessing; - -import org.esa.snap.core.datamodel.MetadataAttribute; -import org.esa.snap.core.datamodel.MetadataElement; -import org.esa.snap.core.datamodel.Product; - -/** - * Class providing methods for metadata extraction/conversion between 3RP and 4RP. - * - * @author olafd - */ -public class Meris3rd4thReprocessingMetadata { - - /** - * Transfers selected metadata from 4RP into 3RP product. - * - * @param fourthReproProduct - MERIS L1b 4RP product - * @param thirdReproProduct - MERIS L1b 3RP product - */ - public static void fillMetadataInThirdRepro(Product fourthReproProduct, Product thirdReproProduct) { - final MetadataElement inputProductMetadataRoot = fourthReproProduct.getMetadataRoot(); - final MetadataElement thirdReproProductMetadataRoot = thirdReproProduct.getMetadataRoot(); - if (thirdReproProductMetadataRoot != null && inputProductMetadataRoot != null) { - final MetadataElement mphElement = new MetadataElement("MPH"); - thirdReproProductMetadataRoot.addElement(mphElement); - final MetadataElement manifestElement = inputProductMetadataRoot.getElement("Manifest"); - if (manifestElement != null) { - final MetadataElement metadataSectionMetadataElement = manifestElement.getElement("metadataSection"); - if (metadataSectionMetadataElement != null) { - // MPH --> PRODUCT - // Manifest --> metadataSection --> generalProductInformation --> productName - final MetadataElement generalProductInformationMetadataElement = - metadataSectionMetadataElement.getElement("generalProductInformation"); - if (generalProductInformationMetadataElement != null) { - final MetadataAttribute productNameAttr = - generalProductInformationMetadataElement.getAttribute("productName"); - if (productNameAttr != null) { - mphElement.addAttribute(new MetadataAttribute("PRODUCT", productNameAttr.getData(), true)); - } - } - - // MPH --> SENSING_START - // Manifest --> metadataSection --> acquisitionPeriod --> startTime - // MPH --> SENSING_STOP - // Manifest --> metadataSection --> acquisitionPeriod --> stopTime - final MetadataElement acquisitionPeriodMetadataElement = - metadataSectionMetadataElement.getElement("acquisitionPeriod"); - if (acquisitionPeriodMetadataElement != null) { - final MetadataAttribute startTimeAttr = - acquisitionPeriodMetadataElement.getAttribute("startTime"); - if (startTimeAttr != null) { - mphElement.addAttribute(new MetadataAttribute("SENSING_START", startTimeAttr.getData(), true)); - } - final MetadataAttribute stopTimeAttr = - acquisitionPeriodMetadataElement.getAttribute("stopTime"); - if (stopTimeAttr != null) { - mphElement.addAttribute(new MetadataAttribute("SENSING_STOP", stopTimeAttr.getData(), true)); - } - } - - // MPH --> CYCLE - // Manifest --> metadataSection --> orbitReference --> cycleNumber - // MPH --> REL_ORBIT - // Manifest --> metadataSection --> orbitReference --> relativeOrbitNumber --> relativeOrbitNumber - // MPH --> ABS_ORBIT - // Manifest --> metadataSection --> orbitReference --> orbitNumber --> orbitNumber - final MetadataElement orbitReferenceMetadataElement = - metadataSectionMetadataElement.getElement("orbitReference"); - if (orbitReferenceMetadataElement != null) { - final MetadataAttribute cycleNumberAttr = - orbitReferenceMetadataElement.getAttribute("cycleNumber"); - if (cycleNumberAttr != null) { - mphElement.addAttribute(new MetadataAttribute("CYCLE", cycleNumberAttr.getData(), true)); - } - - final MetadataElement relativeOrbitNumberMetadataElement = - orbitReferenceMetadataElement.getElement("relativeOrbitNumber"); - if (relativeOrbitNumberMetadataElement != null) { - final MetadataAttribute relativeOrbitNumberAttr = - relativeOrbitNumberMetadataElement.getAttribute("relativeOrbitNumber"); - if (relativeOrbitNumberAttr != null) { - mphElement.addAttribute(new MetadataAttribute("REL_ORBIT", relativeOrbitNumberAttr.getData(), true)); - } - } - - final MetadataElement orbitNumberMetadataElement = - orbitReferenceMetadataElement.getElement("orbitNumber"); - if (orbitNumberMetadataElement != null) { - final MetadataAttribute orbitNumberAttr = - orbitNumberMetadataElement.getAttribute("orbitNumber"); - if (orbitNumberAttr != null) { - mphElement.addAttribute(new MetadataAttribute("ABS_ORBIT", orbitNumberAttr.getData(), true)); - } - } - } - } - // todo: discuss what else is needed? - } - } - } -} diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/ReprocessingAdapter.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/ReprocessingAdapter.java deleted file mode 100644 index 83134f7d..00000000 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/reprocessing/ReprocessingAdapter.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.esa.snap.idepix.meris.reprocessing; - -import org.esa.snap.core.datamodel.Product; - -/** - * Interface for adaptation of reprocessed L1 source data (here i.e. MERIS 3rd <--> 4th reprocessing) - * todo: move this to a more general place in SNAP - * - */ -public interface ReprocessingAdapter { - - /** - * Provides the conversion to lower version product for given higher version input product - * - * @param inputProduct - the higher version input product - * @return Product lowerVersionProduct - */ - Product convertToLowerVersion(Product inputProduct); - - /** - * Provides the conversion to higher version product for given lower version input product - * - * @param inputProduct - the lower version input product - * @return Product higherVersionProduct - */ - Product convertToHigherVersion(Product inputProduct); -} diff --git a/idepix-meris/src/main/javahelp/org/esa/snap/idepix/meris/docs/images/MerisParameters.png b/idepix-meris/src/main/javahelp/org/esa/snap/idepix/meris/docs/images/MerisParameters.png index 16c6e9abcbfc92b312865d01ffc5c2105f85ecdf..62e89979a22b4de7716636fd019c075fdd8970ec 100644 GIT binary patch literal 32494 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 literal 28161 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 Processing Parameters under certain conditions. The default value is 4.6.

+

+ Compute mountain shadow:
+ If set, a mountain shadow is computed and added to the pixel classification. The default value is 'true'. +

+ +

+ Extent of mountain shadow:
+ A value between zero and 1 defining how extensive the cloud shadow is flagged. A higher value means more shadow is flagged. + The best value is dependent on the current scene. As default 0.9 is set. +

+

Compute cloud shadow:
If set, a cloud shadow is computed and added to the pixel classification. The default value is 'true'. diff --git a/idepix-meris/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi b/idepix-meris/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi index 0924dcd5..7941b126 100644 --- a/idepix-meris/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi +++ b/idepix-meris/src/main/resources/META-INF/services/org.esa.snap.core.gpf.OperatorSpi @@ -2,4 +2,6 @@ org.esa.snap.idepix.meris.IdepixMerisOp$Spi org.esa.snap.idepix.meris.IdepixMerisLandClassificationOp$Spi org.esa.snap.idepix.meris.IdepixMerisWaterClassificationOp$Spi org.esa.snap.idepix.meris.IdepixMerisMergeLandWaterOp$Spi -org.esa.snap.idepix.meris.IdepixMerisPostProcessOp$Spi \ No newline at end of file +org.esa.snap.idepix.meris.IdepixMerisPostProcessOp$Spi +org.esa.snap.idepix.meris.IdepixMerisMountainShadowOp$Spi +org.esa.snap.idepix.meris.IdepixMerisSlopeAspectOrientationOp$Spi \ No newline at end of file diff --git a/idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapterTest.java b/idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapterTest.java deleted file mode 100644 index fc9e4835..00000000 --- a/idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingAdapterTest.java +++ /dev/null @@ -1,270 +0,0 @@ -package org.esa.snap.idepix.meris.reprocessing; - -import com.bc.ceres.core.ProgressMonitor; -import org.esa.snap.core.datamodel.*; -import org.esa.snap.dataio.envisat.EnvisatConstants; -import org.junit.Before; -import org.junit.Test; - -import java.io.IOException; - -import static org.junit.Assert.*; - -public class Meris3rd4thReprocessingAdapterTest { - - private Product fourthReproTestProduct; - private Meris3rd4thReprocessingAdapter adapter; - private Product thirdReproProduct; - - @Before - public void setUp() { - createFourthReproTestProduct(); - adapter = new Meris3rd4thReprocessingAdapter(); - thirdReproProduct = adapter.convertToLowerVersion(fourthReproTestProduct); - } - - @Test - public void testConvertQualityToL1FlagValue() { - // mapping quality_flags --> l1_flags: - // ## cosmetic (2^24) --> COSMETIC (1) - // ## duplicated (2^23) --> DUPLICATED (2) - // ## sun_glint_risk (2^22) --> GLINT_RISK (4) - // ## dubious (2^21) --> SUSPECT (8) - // ## land (2^31) --> LAND_OCEAN (16) - // ## bright (2^27) --> BRIGHT (32) - // ## coastline (2^30) --> COASTLINE (64) - // ## invalid (2^25) --> INVALID (128) - - // quality flag: pixel is cosmetic, dubious, coastline - // NOTE: quality_flag is uint32 in the 4RP product, we treat it as a long here - long qualityFlagValue = (long) (Math.pow(2.0, 24) + Math.pow(2.0, 21) + Math.pow(2.0, 30)); - int expectedL1FlagValue = 1 + 8 + 64; - int l1FlagValue = adapter.convertQualityToL1FlagValue(qualityFlagValue); - assertEquals(expectedL1FlagValue, l1FlagValue); - - // quality flag: pixel is duplicated, sun glint risk, land, bright - qualityFlagValue = (long) (Math.pow(2.0, 23) + Math.pow(2.0, 22) + Math.pow(2.0, 31) + Math.pow(2.0, 27)); - expectedL1FlagValue = 2 + 4 + 16 + 32; - l1FlagValue = adapter.convertQualityToL1FlagValue(qualityFlagValue); - assertEquals(expectedL1FlagValue, l1FlagValue); - - // quality flag: pixel is invalid - qualityFlagValue = (long) (Math.pow(2.0, 25)); - expectedL1FlagValue = 128; - l1FlagValue = adapter.convertQualityToL1FlagValue(qualityFlagValue); - assertEquals(expectedL1FlagValue, l1FlagValue); - } - - @Test - public void testConvertToLowerVersion_spectralBands() throws IOException { - for (int i = 0; i < EnvisatConstants.MERIS_L1B_NUM_SPECTRAL_BANDS; i++) { - // spectral bands - final String fourthReproSpectralBandName = "M" + String.format("%02d", i + 1) + "_radiance"; - final String thirdReproSpectralBandName = EnvisatConstants.MERIS_L1B_SPECTRAL_BAND_NAMES[i]; - final Band fourthReproSpectralBand = fourthReproTestProduct.getBand(fourthReproSpectralBandName); - final Band thirdReproSpectralBand = thirdReproProduct.getBand(thirdReproSpectralBandName); - assertNotNull(thirdReproSpectralBand); - float[] fourthReproSpectralData = new float[3 * 2]; - float[] thirdReproSpectralData = new float[3 * 2]; - fourthReproSpectralBand.readPixels(0, 0, 3, 2, fourthReproSpectralData, ProgressMonitor.NULL); - thirdReproSpectralBand.readPixels(0, 0, 3, 2, thirdReproSpectralData, ProgressMonitor.NULL); - assertArrayEquals(fourthReproSpectralData, thirdReproSpectralData, 0.0f); - - // solar fluxes - final String fourthReproSolarFluxBandName = "solar_flux_band_" + (i + 1); - final Band fourthReproSolarFluxSpectralBand = fourthReproTestProduct.getBand(fourthReproSolarFluxBandName); - assertNotNull(fourthReproSolarFluxSpectralBand); - float[] fourthReproSolarFluxData = new float[3 * 2]; - fourthReproSolarFluxSpectralBand.readPixels(0, 0, 3, 2, fourthReproSolarFluxData, ProgressMonitor.NULL); - float expectedMeanSolarFlux = (fourthReproSolarFluxData[0] + fourthReproSolarFluxData[1] + - fourthReproSolarFluxData[2] + fourthReproSolarFluxData[3] + - fourthReproSolarFluxData[4] + fourthReproSolarFluxData[5]) / 6.0f; - float computedMeanSolarFlux = - Meris3rd4thReprocessingAdapter.getMeanSolarFluxFrom4thReprocessing(fourthReproSolarFluxSpectralBand); - assertEquals(expectedMeanSolarFlux, computedMeanSolarFlux, 1.E-6f); - assertEquals(expectedMeanSolarFlux, thirdReproSpectralBand.getSolarFlux(), 1.E-6f); - } - } - - @Test - public void testConvertToLowerVersion_detectorIndex() throws IOException { - // detector index - final Band fourthReproDetectorIndexBand = fourthReproTestProduct.getBand("detector_index"); - final Band thirdReproDetectorIndexBand = thirdReproProduct.getBand("detector_index"); - assertNotNull(thirdReproDetectorIndexBand); - int[] fourthReproDetectorIndexData = new int[3 * 2]; - int[] thirdReproDetectorIndexData = new int[3 * 2]; - fourthReproDetectorIndexBand.readPixels(0, 0, 3, 2, fourthReproDetectorIndexData, ProgressMonitor.NULL); - thirdReproDetectorIndexBand.readPixels(0, 0, 3, 2, thirdReproDetectorIndexData, ProgressMonitor.NULL); - assertArrayEquals(fourthReproDetectorIndexData, thirdReproDetectorIndexData); - } - - @Test - public void testConvertToLowerVersion_flagBands() throws IOException { - // l1b flag - final Band fourthReproQualityFlagBand = fourthReproTestProduct.getBand("quality_flags"); - final Band thirdReproL1bFlagBand = thirdReproProduct.getBand("l1_flags"); - assertNotNull(thirdReproL1bFlagBand); - int[] fourthReproQualityFlagData = new int[3 * 2]; - int[] thirdReproL1bFlagData = new int[3 * 2]; - fourthReproQualityFlagBand.readPixels(0, 0, 3, 2, fourthReproQualityFlagData, ProgressMonitor.NULL); - thirdReproL1bFlagBand.readPixels(0, 0, 3, 2, thirdReproL1bFlagData, ProgressMonitor.NULL); - assertNotNull(thirdReproL1bFlagData); - - int[] expectedFourthReproQualityFlagData = new int[]{ - (int) (Math.pow(2.0, 24) + Math.pow(2.0, 21) + Math.pow(2.0, 30)), - (int) (Math.pow(2.0, 22) + Math.pow(2.0, 23) + Math.pow(2.0, 27)), - (int) (Math.pow(2.0, 22) + Math.pow(2.0, 23) + Math.pow(2.0, 24)), - (int) (Math.pow(2.0, 24) + Math.pow(2.0, 30)), - (int) (Math.pow(2.0, 24) + Math.pow(2.0, 27) + Math.pow(2.0, 30)), - (int) (Math.pow(2.0, 25)), - }; - assertArrayEquals(expectedFourthReproQualityFlagData, fourthReproQualityFlagData); - int[] expectedThirdReproL1bFlagData = new int[]{ - 1+8+64, - 2+4+32, - 1+2+4, - 1+64, - 1+32+64, - 128, - }; - assertArrayEquals(expectedThirdReproL1bFlagData, thirdReproL1bFlagData); - } - - private void createFourthReproTestProduct() { - fourthReproTestProduct = new Product("ME_1_RRG_4RP_test", "ME_1_RRG", 3, 2); - - // add bands - for (int i = 0; i < EnvisatConstants.MERIS_L1B_NUM_SPECTRAL_BANDS; i++) { - final String fourthReproSpectralBandName = "M" + String.format("%02d", i + 1) + "_radiance"; - Band fourthReproSpectralBand = new Band(fourthReproSpectralBandName, ProductData.TYPE_FLOAT32, 3, 2); - fourthReproSpectralBand.ensureRasterData(); - float[] testFloat32s = new float[]{(i + 1) * 1.f, (i + 1) * 2.f, (i + 1) * 3.f, - (i + 1) * 4.f, (i + 1) * 5.f, (i + 1) * 6.f}; - fourthReproSpectralBand.setPixels(0, 0, 3, 2, testFloat32s); - fourthReproSpectralBand.setSourceImage(fourthReproSpectralBand.getSourceImage()); - fourthReproTestProduct.addBand(fourthReproSpectralBand); - - Band solarFluxBand = new Band("solar_flux_band_" + (i + 1), ProductData.TYPE_FLOAT32, 3, 2); - solarFluxBand.ensureRasterData(); - float[] testFloat32sSolarFlux = new float[]{1400 + (i + 1) * 1.f, 1400 + (i + 1) * 2.f, 1400 + (i + 1) * 3.f, - 1400 + (i + 1) * 4.f, 1400 + (i + 1) * 5.f, 1400 + (i + 1) * 6.f}; - solarFluxBand.setPixels(0, 0, 3, 2, testFloat32sSolarFlux); - solarFluxBand.setSourceImage(solarFluxBand.getSourceImage()); - fourthReproTestProduct.addBand(solarFluxBand); - } - - Band detectorIndexBand = new Band("detector_index", ProductData.TYPE_INT16, 3, 2); - detectorIndexBand.ensureRasterData(); - int[] testInt16sDetectorIndex = new int[]{111, 222, 333, 444, 555, 666}; - detectorIndexBand.setPixels(0, 0, 3, 2, testInt16sDetectorIndex); - detectorIndexBand.setSourceImage(detectorIndexBand.getSourceImage()); - fourthReproTestProduct.addBand(detectorIndexBand); - - Band qualityFlagBand = new Band("quality_flags", ProductData.TYPE_UINT32, 3, 2); - qualityFlagBand.ensureRasterData(); - int[] testInt16sQualityFlag = new int[]{ - (int) (Math.pow(2.0, 24) + Math.pow(2.0, 21) + Math.pow(2.0, 30)), - (int) (Math.pow(2.0, 22) + Math.pow(2.0, 23) + Math.pow(2.0, 27)), - (int) (Math.pow(2.0, 22) + Math.pow(2.0, 23) + Math.pow(2.0, 24)), - (int) (Math.pow(2.0, 24) + Math.pow(2.0, 30)), - (int) (Math.pow(2.0, 24) + Math.pow(2.0, 27) + Math.pow(2.0, 30)), - (int) (Math.pow(2.0, 25)), - }; - qualityFlagBand.setPixels(0, 0, 3, 2, testInt16sQualityFlag); - qualityFlagBand.setSourceImage(qualityFlagBand.getSourceImage()); - fourthReproTestProduct.addBand(qualityFlagBand); - - // add tie point grids: - // TP_latitude --> latitude - // TP_longitude --> longitude - // TP_altitude --> dem_alt - // SZA --> sun_zenith - // OZA --> view_zenith - // SAA --> sun_azimuth - // OAA --> view_azimuth - // horizontal_wind_vector_1 --> zonal_wind - // horizontal_wind_vector_2 --> merid_wind - // sea_level_pressure --> atm_press - // total_ozone --> ozone - // humidity_pressure_level_14 --> rel_hum // relative humidity at 850 hPa - - float[] tpLatTestData = new float[]{31.f, 32.f, 33.f, 27.f, 28.f, 29.f}; - TiePointGrid tpLatTpg = new TiePointGrid("TP_latitude", 3, 2, 0, 0, 1, 1, tpLatTestData); - fourthReproTestProduct.addTiePointGrid(tpLatTpg); - - float[] tpLonTestData = new float[]{51.f, 52.f, 33.f, 51.f, 52.f, 53.f}; - TiePointGrid tpLonTpg = new TiePointGrid("TP_longitude", 3, 2, 0, 0, 1, 1, tpLonTestData); - fourthReproTestProduct.addTiePointGrid(tpLonTpg); - - float[] tpAltTestData = new float[]{234.f, 567.f, 789.f, 1133.f, 1244.f, 1355.f}; - TiePointGrid tpAltTpg = new TiePointGrid("TP_altitude", 3, 2, 0, 0, 1, 1, tpAltTestData); - fourthReproTestProduct.addTiePointGrid(tpAltTpg); - - float[] szaTestData = new float[]{41.f, 42.f, 43.f, 47.f, 48.f, 49.f}; - TiePointGrid szaTpg = new TiePointGrid("SZA", 3, 2, 0, 0, 1, 1, szaTestData); - fourthReproTestProduct.addTiePointGrid(szaTpg); - - float[] saaTestData = new float[]{-14.f, -24.f, -34.f, 74.f, 84.f, 89.f}; - TiePointGrid saaTpg = new TiePointGrid("SAA", 3, 2, 0, 0, 1, 1, saaTestData); - fourthReproTestProduct.addTiePointGrid(saaTpg); - - float[] ozaTestData = new float[]{11.f, 22.f, 33.f, 12.f, 23.f, 34.f}; - TiePointGrid ozaTpg = new TiePointGrid("OZA", 3, 2, 0, 0, 1, 1, ozaTestData); - fourthReproTestProduct.addTiePointGrid(ozaTpg); - - float[] oaaTestData = new float[]{-55.f, -56.f, -57.f, 58.f, 59.f, 60.f}; - TiePointGrid oaaTpg = new TiePointGrid("OAA", 3, 2, 0, 0, 1, 1, oaaTestData); - fourthReproTestProduct.addTiePointGrid(oaaTpg); - - float[] zonalWindTestData = new float[]{10.f, 11.f, 12.f, 16.f, 17.f, 18.f}; - TiePointGrid zonalWindTpg = new TiePointGrid("horizontal_wind_vector_1", 3, 2, 0, 0, 1, 1, zonalWindTestData); - fourthReproTestProduct.addTiePointGrid(zonalWindTpg); - - float[] meridionalWindTestData = new float[]{5.f, 6.f, 7.f, 2.f, 3.f, 4.f}; - TiePointGrid meridionalWindTpg = new TiePointGrid("horizontal_wind_vector_2", 3, 2, 0, 0, 1, 1, meridionalWindTestData); - fourthReproTestProduct.addTiePointGrid(meridionalWindTpg); - - float[] slpTestData = new float[]{1005.f, 1006.f, 1008.f, 1002.f, 1003.f, 1004.f}; - TiePointGrid slpTpg = new TiePointGrid("sea_level_pressure", 3, 2, 0, 0, 1, 1, slpTestData); - fourthReproTestProduct.addTiePointGrid(slpTpg); - - float[] ozoneTestData = new float[]{0.005f, 0.0055f, 0.006f, 0.0065f, 0.007f, 0.0075f}; - TiePointGrid ozoneTpg = new TiePointGrid("total_ozone", 3, 2, 0, 0, 1, 1, ozoneTestData); - fourthReproTestProduct.addTiePointGrid(ozoneTpg); - - float[] relHumTestData = new float[]{2.34f, 13.7f, 11.5f, 33.4f, 35.8f, 39.2f}; - TiePointGrid relHumTpg = new TiePointGrid("humidity_pressure_level_14", 3, 2, 0, 0, 1, 1, relHumTestData); - fourthReproTestProduct.addTiePointGrid(relHumTpg); - - fourthReproTestProduct.setAutoGrouping("M*_radiance:solar_flux"); - - // metadata - final MetadataElement manifestElement = new MetadataElement("Manifest"); - fourthReproTestProduct.getMetadataRoot().addElement(manifestElement); - final MetadataElement metadataSectionElement = new MetadataElement("metadataSection"); - manifestElement.addElement(metadataSectionElement); - final MetadataElement generalProductInformationElement = new MetadataElement("generalProductInformation"); - metadataSectionElement.addElement(generalProductInformationElement); - generalProductInformationElement.addAttribute(new MetadataAttribute("productName", - ProductData.createInstance(fourthReproTestProduct.getName()), true)); - final MetadataElement acquisitionPeriodElement = new MetadataElement("acquisitionPeriod"); - metadataSectionElement.addElement(acquisitionPeriodElement); - acquisitionPeriodElement.addAttribute(new MetadataAttribute("startTime", - ProductData.createInstance("2011-07-02T14:08:01.955726Z"), true)); - acquisitionPeriodElement.addAttribute(new MetadataAttribute("stopTime", - ProductData.createInstance("2011-07-02T14:51:57.552001Z"), true)); - final MetadataElement orbitReferenceElement = new MetadataElement("orbitReference"); - orbitReferenceElement.addAttribute(new MetadataAttribute("cycleNumber", - ProductData.createInstance(new int[]{104}), true)); - final MetadataElement orbitNumberElement = new MetadataElement("orbitNumber"); - orbitNumberElement.addAttribute(new MetadataAttribute("orbitNumber", - ProductData.createInstance(new int[]{48832}), true)); - orbitReferenceElement.addElement(orbitNumberElement); - final MetadataElement relativeOrbitNumberElement = new MetadataElement("relativeOrbitNumber"); - relativeOrbitNumberElement.addAttribute(new MetadataAttribute("relativeOrbitNumber", - ProductData.createInstance(new int[]{111}), true)); - orbitReferenceElement.addElement(relativeOrbitNumberElement); - metadataSectionElement.addElement(orbitReferenceElement); - } -} diff --git a/idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadataTest.java b/idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadataTest.java deleted file mode 100644 index 28d4539b..00000000 --- a/idepix-meris/src/test/java/org/esa/snap/idepix/meris/reprocessing/Meris3rd4thReprocessingMetadataTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.esa.snap.idepix.meris.reprocessing; - -import org.esa.snap.core.datamodel.MetadataAttribute; -import org.esa.snap.core.datamodel.MetadataElement; -import org.esa.snap.core.datamodel.Product; -import org.esa.snap.core.datamodel.ProductData; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class Meris3rd4thReprocessingMetadataTest { - - private Product fourthReproTestProduct; - - @Before - public void setUp() { - createFourthReproTestProduct_metadataOnly(); - } - - @Test - public void testFillMetadataInThirdRepro() { - final Product thirdReproProduct = new Product("ME_1_RRG_3RP_test", "ME_1_RRG", 3, 2); - Meris3rd4thReprocessingMetadata.fillMetadataInThirdRepro(fourthReproTestProduct, thirdReproProduct); - - final MetadataElement thirdReproProductMetadataRoot = thirdReproProduct.getMetadataRoot(); - assertNotNull(thirdReproProductMetadataRoot); - - final MetadataElement mphElement = thirdReproProductMetadataRoot.getElement("MPH"); - assertNotNull(mphElement); - - final MetadataAttribute productAttr = mphElement.getAttribute("PRODUCT"); - assertNotNull(productAttr); - assertNotNull(productAttr.getData()); - assertEquals(fourthReproTestProduct.getName(), productAttr.getData().getElemString()); - - final MetadataAttribute sensingStartAttr = mphElement.getAttribute("SENSING_START"); - assertNotNull(sensingStartAttr); - assertNotNull(sensingStartAttr.getData()); - assertEquals("2011-07-02T14:08:01.955726Z", sensingStartAttr.getData().getElemString()); - - final MetadataAttribute sensingStopAttr = mphElement.getAttribute("SENSING_STOP"); - assertNotNull(sensingStopAttr); - assertNotNull(sensingStopAttr.getData()); - assertEquals("2011-07-02T14:51:57.552001Z", sensingStopAttr.getData().getElemString()); - - final MetadataAttribute cycleAttr = mphElement.getAttribute("CYCLE"); - assertNotNull(cycleAttr); - assertNotNull(cycleAttr.getData()); - assertEquals(104, cycleAttr.getData().getElemInt()); - - final MetadataAttribute relOrbitAttr = mphElement.getAttribute("REL_ORBIT"); - assertNotNull(relOrbitAttr); - assertNotNull(relOrbitAttr.getData()); - assertEquals(111, relOrbitAttr.getData().getElemInt()); - - final MetadataAttribute absOrbitAttr = mphElement.getAttribute("ABS_ORBIT"); - assertNotNull(absOrbitAttr); - assertNotNull(absOrbitAttr.getData()); - assertEquals(48832, absOrbitAttr.getData().getElemInt()); - } - - private void createFourthReproTestProduct_metadataOnly() { - fourthReproTestProduct = new Product("ME_1_RRG_4RP_test", "ME_1_RRG", 3, 2); - - // add metadata - final MetadataElement manifestElement = new MetadataElement("Manifest"); - fourthReproTestProduct.getMetadataRoot().addElement(manifestElement); - final MetadataElement metadataSectionElement = new MetadataElement("metadataSection"); - manifestElement.addElement(metadataSectionElement); - final MetadataElement generalProductInformationElement = new MetadataElement("generalProductInformation"); - metadataSectionElement.addElement(generalProductInformationElement); - generalProductInformationElement.addAttribute(new MetadataAttribute("productName", - ProductData.createInstance(fourthReproTestProduct.getName()), true)); - final MetadataElement acquisitionPeriodElement = new MetadataElement("acquisitionPeriod"); - metadataSectionElement.addElement(acquisitionPeriodElement); - acquisitionPeriodElement.addAttribute(new MetadataAttribute("startTime", - ProductData.createInstance("2011-07-02T14:08:01.955726Z"), true)); - acquisitionPeriodElement.addAttribute(new MetadataAttribute("stopTime", - ProductData.createInstance("2011-07-02T14:51:57.552001Z"), true)); - final MetadataElement orbitReferenceElement = new MetadataElement("orbitReference"); - orbitReferenceElement.addAttribute(new MetadataAttribute("cycleNumber", - ProductData.createInstance(new int[]{104}), true)); - final MetadataElement orbitNumberElement = new MetadataElement("orbitNumber"); - orbitNumberElement.addAttribute(new MetadataAttribute("orbitNumber", - ProductData.createInstance(new int[]{48832}), true)); - orbitReferenceElement.addElement(orbitNumberElement); - final MetadataElement relativeOrbitNumberElement = new MetadataElement("relativeOrbitNumber"); - relativeOrbitNumberElement.addAttribute(new MetadataAttribute("relativeOrbitNumber", - ProductData.createInstance(new int[]{111}), true)); - orbitReferenceElement.addElement(relativeOrbitNumberElement); - metadataSectionElement.addElement(orbitReferenceElement); - } -} diff --git a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOp.java b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOp.java index 5df76810..981e1622 100644 --- a/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOp.java +++ b/idepix-olci/src/main/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOp.java @@ -9,19 +9,17 @@ import org.esa.snap.core.gpf.annotations.OperatorMetadata; import org.esa.snap.core.gpf.annotations.SourceProduct; import org.esa.snap.core.util.ProductUtils; -import org.esa.snap.core.util.math.MathUtils; -import org.opengis.referencing.operation.MathTransform; +import org.esa.snap.idepix.core.util.SlopeAspectOrientationUtils; import javax.media.jai.BorderExtender; import java.awt.*; -import java.awt.geom.AffineTransform; import java.util.Map; /** * Computes Slope, Aspect and Orientation for a Sentinel-3 OLCI product. * See theory e.g. at - * https://www.e-education.psu.edu/geog480/node/490, or - * https://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-analyst-toolbox/how-hillshade-works.htm + * ..., or + * ... * * @author Tonio Fincke, Olaf Danne, Dagmar Mueller */ @@ -38,9 +36,6 @@ public class IdepixOlciSlopeAspectOrientationOp extends Operator { @SourceProduct(alias = "l1b") private Product l1bProduct; - private final static float EARTH_MIN_ELEVATION = -428.0f; // at shoreline of Dead Sea - private final static float EARTH_MAX_ELEVATION = 8848.0f; // Mt. Everest - private static final String ELEVATION_BAND_NAME = IdepixOlciConstants.OLCI_ALTITUDE_BAND_NAME; private double spatialResolution; @@ -77,16 +72,9 @@ public void initialize() throws OperatorException { if (sourceGeoCoding == null) { throw new OperatorException("Source product has no geo-coding"); } - if (sourceGeoCoding instanceof CrsGeoCoding) { - final MathTransform i2m = sourceGeoCoding.getImageToMapTransform(); - if (i2m instanceof AffineTransform) { - spatialResolution = ((AffineTransform) i2m).getScaleX(); - } else { - spatialResolution = computeSpatialResolution(l1bProduct, sourceGeoCoding); - } - } else { - spatialResolution = computeSpatialResolution(l1bProduct, sourceGeoCoding); - } + + spatialResolution = SlopeAspectOrientationUtils.computeSpatialResolution(l1bProduct, sourceGeoCoding); + elevationBand = l1bProduct.getBand(ELEVATION_BAND_NAME); if (elevationBand == null) { throw new OperatorException("Elevation band required to compute slope or aspect"); @@ -154,16 +142,20 @@ public void computeTileStack(Map targetTiles, Rectangle targetRectan slopeTile.setSample(x, y, Float.NaN); aspectTile.setSample(x, y, Float.NaN); orientationTile.setSample(x, y, Float.NaN); - final float[] elevationDataMacropixel = get3x3MacropixelData(elevationTile, y, x); - if (is3x3ElevationDataValid(elevationDataMacropixel)) { + final float[] elevationDataMacropixel = SlopeAspectOrientationUtils.get3x3MacropixelData(elevationTile, y, x); + if (SlopeAspectOrientationUtils.is3x3ElevationDataValid(elevationDataMacropixel)) { final float vza = viewZenithAngleTile.getSampleFloat(x, y); final float vaa = viewAzimuthAngleTile.getSampleFloat(x, y); final float saa = sunAzimuthAngleTile.getSampleFloat(x, y); - final float[] latitudeDataMacropixel = get3x3MacropixelData(latitudeTile, y, x); - final float[] longitudeDataMacropixel = get3x3MacropixelData(longitudeTile, y, x); - final float orientation = computeOrientation(latitudeDataMacropixel, longitudeDataMacropixel); + final float[] latitudeDataMacropixel = + SlopeAspectOrientationUtils.get3x3MacropixelData(latitudeTile, y, x); + final float[] longitudeDataMacropixel = + SlopeAspectOrientationUtils.get3x3MacropixelData(longitudeTile, y, x); + final float orientation = SlopeAspectOrientationUtils.computeOrientation3x3Box(latitudeDataMacropixel, + longitudeDataMacropixel); orientationTile.setSample(x, y, orientation); - final float[] slopeAspect = computeSlopeAspect(elevationDataMacropixel, orientation, vza, vaa, saa, + final float[] slopeAspect = SlopeAspectOrientationUtils.computeSlopeAspect3x3(elevationDataMacropixel, + orientation, vza, vaa, saa, spatialResolution); slopeTile.setSample(x, y, slopeAspect[0]); aspectTile.setSample(x, y, slopeAspect[1]); @@ -172,150 +164,6 @@ public void computeTileStack(Map targetTiles, Rectangle targetRectan } } - private float[] get3x3MacropixelData(Tile sourceTile, int y, int x) { - float[] macropixelData = new float[9]; - macropixelData[0] = sourceTile.getSampleFloat(x - 1, y - 1); - macropixelData[1] = sourceTile.getSampleFloat(x, y - 1); - macropixelData[2] = sourceTile.getSampleFloat(x + 1, y - 1); - macropixelData[3] = sourceTile.getSampleFloat(x - 1, y); - macropixelData[4] = sourceTile.getSampleFloat(x, y); - macropixelData[5] = sourceTile.getSampleFloat(x + 1, y); - macropixelData[6] = sourceTile.getSampleFloat(x - 1, y + 1); - macropixelData[7] = sourceTile.getSampleFloat(x, y + 1); - macropixelData[8] = sourceTile.getSampleFloat(x + 1, y + 1); - - return macropixelData; - } - - /** - * Checks if 3x3 box of elevations around reference pixel is valid - * (i.e. not NaN and all values inside real values possible on Earth) - * - * @param elevationData - 3x3 box of elevations - * @return boolean - */ - static boolean is3x3ElevationDataValid(float[] elevationData) { - for (final float elev : elevationData) { - if (elev == 0.0f || Float.isNaN(elev) || elev < EARTH_MIN_ELEVATION || elev > EARTH_MAX_ELEVATION) { - return false; - } - } - return true; - } - - /* package local for testing */ - static float[] computeSlopeAspect(float[] elev, float orientation, float vza, float vaa, float saa, double spatialResolution) { - //DM: geometric correction of resolution necessary for observations not in nadir view. - // generates steeper slopes. Needs vza, vaa, angle between y-pixel axis and north direction (orientation). - //DM: orientation in rad! - float b = (elev[2] + 2 * elev[5] + elev[8] - elev[0] - 2 * elev[3] - elev[6]) / 8f; //direction x - float c = (elev[0] + 2 * elev[1] + elev[2] - elev[6] - 2 * elev[7] - elev[8]) / 8f; //direction y - double vaa_orientation = (360.0 - (vaa + orientation / MathUtils.DTOR)) * MathUtils.DTOR; - double spatialRes = spatialResolution / Math.cos(vza * MathUtils.DTOR); - float slope = (float) Math.atan(Math.sqrt(Math.pow(b / (spatialRes * Math.sin(vaa_orientation)), 2) + - Math.pow(c / (spatialRes * Math.cos(vaa_orientation)), 2))); - float aspect = (float) Math.atan2(-b, -c); - if (saa > 270. || saa < 90) { //Sun from North (mostly southern hemisphere) - aspect -= Math.PI; - } - if (aspect < 0.0f) { - // map from [-180, 180] into [0, 360], see e.g. https://www.e-education.psu.edu/geog480/node/490 - aspect += 2.0 * Math.PI; - } - if (slope <= 0.0) { - aspect = Float.NaN; - } - - return new float[]{slope, aspect}; - } - - /* package local for testing */ - static float computeOrientation(float[] latData, float[] lonData) { - final float lat1 = latData[3]; - final float lat2 = latData[5]; - final float lon1 = lonData[3]; - final float lon2 = lonData[5]; - - return computeOrientation(lat1, lat2, lon1, lon2); - } - - /** - * Provides orientation ('bearing') between two points. - * See theory e.g. at - * https://www.igismap.com/formula-to-find-bearing-or-heading-angle-between-two-points-latitude-longitude/ - * - * @param lat1 - first latitude - * @param lat2 - second latitude - * @param lon1 - first longitude - * @param lon2 - second longitude - * - * @return float - */ - static float computeOrientation(float lat1, float lat2, float lon1, float lon2) { -// final float lat1Rad = lat1 * MathUtils.DTOR_F; -// final float deltaLon = lon2 - lon1; - // we use this formula, as in S2-MSI: -// return (float) Math.atan2(-(lat2 - lat1), deltaLon * Math.cos(lat1Rad)); - - // DM: formulas from theory, see above - double X = Math.cos(lat2 * MathUtils.DTOR) * Math.sin((lon2 - lon1) * MathUtils.DTOR); - double Y = Math.cos(lat1 * MathUtils.DTOR) * Math.sin(lat2 * MathUtils.DTOR) - Math.sin(lat1 * MathUtils.DTOR) * X; - return (float) (Math.atan2(X, Y)); - - } - - /** - * Computes product spatial resolution from great circle distances at the product edges. - * To be used as fallback if we have no CRS geocoding. - * - * @param l1bProduct - the source product - * @param sourceGeoCoding - the source scene geocoding - * @return spatial resolution in metres - */ - static double computeSpatialResolution(Product l1bProduct, GeoCoding sourceGeoCoding) { - final double width = l1bProduct.getSceneRasterWidth(); - final double height = l1bProduct.getSceneRasterHeight(); - - final GeoPos leftPos = sourceGeoCoding.getGeoPos(new PixelPos(0, height / 2), null); - final GeoPos rightPos = sourceGeoCoding.getGeoPos(new PixelPos(width - 1, height / 2), null); - final double distance1 = - computeDistance(leftPos.getLat(), leftPos.getLon(), rightPos.getLat(), rightPos.getLon()); - - final GeoPos upperPos = sourceGeoCoding.getGeoPos(new PixelPos(width / 2, 0), null); - final GeoPos lowerPos = sourceGeoCoding.getGeoPos(new PixelPos(width / 2, height - 1), null); - final double distance2 = - computeDistance(upperPos.getLat(), upperPos.getLon(), lowerPos.getLat(), lowerPos.getLon()); - - final double distance = 0.5 * (distance1 + distance2); - - return 1000.0 * distance / (width - 1); - } - - /** - * Calculate the great-circle distance between two points on Earth using Haversine formula. - * See e.g. https://www.movable-type.co.uk/scripts/latlong.html - * - * @param lat1 - first point latitude - * @param lon1 - first point longitude - * @param lat2 - second point latitude - * @param lon2 - second point longitude - * @return distance in km - */ - static double computeDistance(double lat1, double lon1, double lat2, double lon2) { - final double deltaLatR = (lat1 - lat2) * MathUtils.DTOR; - final double deltaLonR = (lon1 - lon2) * MathUtils.DTOR; - - final double a = Math.sin(deltaLatR / 2.0) * Math.sin(deltaLatR / 2.0) + - Math.cos(lat1 * MathUtils.DTOR) * Math.cos(lat2 * MathUtils.DTOR) * - Math.sin(deltaLonR / 2.0) * Math.sin(deltaLonR / 2.0); - - final double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - final double R = 6371.0; // Earth radius in km - - return R * c; - } - private static Rectangle getSourceRectangle(Rectangle targetRectangle) { return new Rectangle(targetRectangle.x - 1, targetRectangle.y - 1, targetRectangle.width + 2, targetRectangle.height + 2); diff --git a/idepix-olci/src/test/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOpTest.java b/idepix-olci/src/test/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOpTest.java deleted file mode 100644 index 0411b8f5..00000000 --- a/idepix-olci/src/test/java/org/esa/snap/idepix/olci/IdepixOlciSlopeAspectOrientationOpTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.esa.snap.idepix.olci; - -import org.junit.Ignore; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * @author Tonio Fincke - */ -public class IdepixOlciSlopeAspectOrientationOpTest { - -// @Test -// public void computeSlopeAndAspect() { -// // flat plane over 3x3 box -// float[] altitude = new float[]{ -// 0.0f, 0.0f, 0.0f, -// 0.0f, 0.0f, 0.0f, -// 0.0f, 0.0f, 0.0f}; -// float[] slopeAndAspect = IdepixOlciSlopeAspectOrientationOp.computeSlopeAspect(altitude, 300); -// assertEquals(slopeAndAspect[0], 0.0f, 1e-7); -// assertEquals(slopeAndAspect[1], Float.NaN, 1e-8); -// -// // ratio 1:1 (45deg) inclined plane over 3x3 box, ascending to the north -// altitude = new float[]{ -// 600.0f, 600.0f, 600.0f, -// 300.0f, 300.0f, 300.0f, -// 0.0f, 0.0f, 0.0f}; -// slopeAndAspect = IdepixOlciSlopeAspectOrientationOp.computeSlopeAspect(altitude, 300); -// assertEquals(slopeAndAspect[0], Math.PI / 4, 1e-7); -// assertEquals(slopeAndAspect[1], Math.PI, 1e-6); -// -// // ratio 1:1 (45deg) inclined plane over 3x3 box, ascending to the west -// altitude = new float[]{ -// 600.0f, 300.0f, 0.0f, -// 600.0f, 300.0f, 0.0f, -// 600.0f, 300.0f, 0.0f}; -// slopeAndAspect = IdepixOlciSlopeAspectOrientationOp.computeSlopeAspect(altitude, 300); -// assertEquals(slopeAndAspect[0], Math.PI / 4, 1e-7); -// assertEquals(slopeAndAspect[1], Math.PI / 2, 1e-6); -// -// // ratio 1:2 inclined plane over 3x3 box, ascending to the east -// altitude = new float[]{ -// 0.0f, 300.0f, 600.0f, -// 0.0f, 300.0f, 600.0f, -// 0.0f, 300.0f, 600.0f}; -// slopeAndAspect = IdepixOlciSlopeAspectOrientationOp.computeSlopeAspect(altitude, 600); -// assertEquals(slopeAndAspect[0], Math.atan(0.5), 1e-7); -// assertEquals(slopeAndAspect[1], Math.PI * 3.0 / 2.0, 1e-6); -// } - - @Test - @Ignore - public void testComputeOrientation() { - - // todo: does not work after DM's latest changes, check why - // east-west 3x3 box, orientaton is 0 deg - float[] latitudes = new float[]{50.0f, 50.0f, 50.0f, - 50.1f, 50.1f, 50.1f, - 50.2f, 50.2f, 50.2f}; - float[] longitudes = new float[]{10.0f, 10.1f, 10.2f, - 10.0f, 10.1f, 10.2f, - 10.0f, 10.1f, 10.2f}; - float orientation = IdepixOlciSlopeAspectOrientationOp.computeOrientation(latitudes, longitudes); - assertEquals(0.0f, orientation, 1e-8); - - // 3x3 box tilted 90deg clockwise --> orientation is -90 deg - latitudes = new float[]{50.0f, 50.1f, 50.2f, - 50.0f, 50.1f, 50.2f, - 50.0f, 50.1f, 50.2f}; - longitudes = new float[]{10.2f, 10.2f, 10.2f, - 10.1f, 10.1f, 10.1f, - 10.0f, 10.0f, 10.0f}; - orientation = IdepixOlciSlopeAspectOrientationOp.computeOrientation(latitudes, longitudes); - assertEquals(-Math.PI / 2.0f, orientation, 1e-6); - - // 3x3 box tilted 45deg counterclockwise --> orientation is 45 deg - latitudes = new float[]{-0.1f, -0.05f, 0.0f, - -0.05f, 0.0f, 0.05f, - 0.0f, 0.05f, 0.1f}; - longitudes = new float[]{10.0f, 10.05f, 10.1f, - 10.05f, 10.1f, 10.15f, - 10.1f, 10.15f, 10.2f}; - orientation = IdepixOlciSlopeAspectOrientationOp.computeOrientation(latitudes, longitudes); - assertEquals(-Math.PI / 4.0f, orientation, 1e-3); - } -} \ No newline at end of file From 18b7c83d111561c56a736ff2e240f81ad7a7bda8 Mon Sep 17 00:00:00 2001 From: martin-boettcher Date: Wed, 5 Apr 2023 22:04:35 +0200 Subject: [PATCH 12/12] Idepix MERIS optimised use of trigonometric functions --- .../snap/idepix/core/util/IdepixUtils.java | 9 +++ .../meris/IdepixMerisMountainShadowOp.java | 20 +++--- .../IdepixMerisSlopeAspectOrientationOp.java | 6 +- .../snap/idepix/meris/IdepixMerisUtils.java | 31 +++++---- .../IdepixMerisWaterClassificationOp.java | 25 +++++++- .../IdepixMerisWaterClassificationOpTest.java | 63 +++++++++++++++++++ pom.xml | 2 +- 7 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 idepix-meris/src/test/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOpTest.java diff --git a/idepix-core/src/main/java/org/esa/snap/idepix/core/util/IdepixUtils.java b/idepix-core/src/main/java/org/esa/snap/idepix/core/util/IdepixUtils.java index 5246628e..2b3a31f9 100644 --- a/idepix-core/src/main/java/org/esa/snap/idepix/core/util/IdepixUtils.java +++ b/idepix-core/src/main/java/org/esa/snap/idepix/core/util/IdepixUtils.java @@ -147,6 +147,15 @@ public static GeoPos getGeoPos(GeoCoding geoCoding, int x, int y) { * @return the azimuth difference [degree] */ public static double computeAzimuthDifference(final double vaa, final double saa) { + final double delta = (720.0 + vaa - saa) % 360.0; + if (delta > 180.0) { + return 360.0 - delta; + } else { + return delta; + } + } + + public static double computeAzimuthDifference1(final double vaa, final double saa) { return MathUtils.RTOD * Math.acos(Math.cos(MathUtils.DTOR * (vaa - saa))); } } diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java index 394b55a6..42758eeb 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisMountainShadowOp.java @@ -103,19 +103,19 @@ protected void computePixel(int x, int y, Sample[] sourceSamples, WritableSample final GeoPos geoPos = l1bProduct.getSceneGeoCoding().getGeoPos(pixelPos, null); final double saaApparent = IdepixMerisUtils.computeApparentSaa(sza, saa, oza, oaa, geoPos.getLat()); - if (x == 2783 && y == 642) { - System.out.println("x, y = " + x + ", " + y); // small subset, shadow - } - if (x == 2783 && y == 2181) { - System.out.println("x, y = " + x + ", " + y); // large subset, no shadow - } +// if (x == 2783 && y == 642) { +// System.out.println("x, y = " + x + ", " + y); // small subset, shadow +// } +// if (x == 2783 && y == 2181) { +// System.out.println("x, y = " + x + ", " + y); // large subset, no shadow +// } final boolean isMountainShadow = isMountainShadow(sza, (float) saaApparent, slope, aspect, orientation, mntShadowExtent); - if (isMountainShadow) { - System.out.println("x, y, slope, aspect, orientation = " + - x + ", " + y + ", " + slope + ", " + aspect + ", " + orientation); - } +// if (isMountainShadow) { +// System.out.println("x, y, slope, aspect, orientation = " + +// x + ", " + y + ", " + slope + ", " + aspect + ", " + orientation); +// } targetSamples[MOUNTAIN_SHADOW_FLAG_BAND_INDEX].set(isMountainShadow); } } diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java index d6974fd1..59d8ddb9 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisSlopeAspectOrientationOp.java @@ -154,9 +154,9 @@ public void computeTileStack(Map targetTiles, Rectangle targetRectan final float vza = viewZenithAngleTile.getSampleFloat(x, y); final float vaa = viewAzimuthAngleTile.getSampleFloat(x, y); final float saa = sunAzimuthAngleTile.getSampleFloat(x, y); - if (x == 2783 && y == 642) { - System.out.println("x, y = " + x + ", " + y); // small subset, shadow - } +// if (x == 2783 && y == 642) { +// System.out.println("x, y = " + x + ", " + y); // small subset, shadow +// } final float[] latitudeDataMacropixel = SlopeAspectOrientationUtils.get3x3MacropixelData(latitudeTile, y, x); final float[] longitudeDataMacropixel = diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java index 23241192..f578ada0 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisUtils.java @@ -97,24 +97,23 @@ static Product computeCloudTopPressureProduct(Product sourceProduct) { } static double computeApparentSaa(double sza, double saa, double oza, double oaa, double lat) { - final double szaRad = sza * MathUtils.DTOR; - final double ozaRad = oza * MathUtils.DTOR; - - double deltaPhi; - if (oaa < 0.0) { - deltaPhi = 360.0 - Math.abs(oaa) - saa; - } else { - deltaPhi = saa - oaa; - } - final double deltaPhiRad = deltaPhi * MathUtils.DTOR; - final double numerator = Math.tan(szaRad) - Math.tan(ozaRad) * Math.cos(deltaPhiRad); - final double denominator = Math.sqrt(Math.tan(ozaRad) * Math.tan(ozaRad) + Math.tan(szaRad) * Math.tan(szaRad) - - 2.0 * Math.tan(szaRad) * Math.tan(ozaRad) * Math.cos(deltaPhiRad)); - +// final double deltaPhi; +// if (oaa < 0.0) { +// deltaPhi = 360.0 - Math.abs(oaa) - saa; +// } else { +// deltaPhi = saa - oaa; +// } +// final double cosDeltaPhi = Math.cos(deltaPhi * MathUtils.DTOR); + final double tanSza = Math.tan(sza * MathUtils.DTOR); + final double tanOza = Math.tan(oza * MathUtils.DTOR); + final double cosDeltaPhi = Math.cos((saa - oaa) * MathUtils.DTOR); + final double numerator = tanSza - tanOza * cosDeltaPhi; + final double denominator = Math.sqrt(tanOza * tanOza + tanSza * tanSza - + 2.0 * tanSza * tanOza * cosDeltaPhi); double delta = Math.acos(numerator / denominator); -// Sun in the North (Southern hemisphere), change sign! + // Sun in the North (Southern hemisphere), change sign! if (saa > 270. || saa < 90){ - delta = -1.0 * delta; + delta = -delta; } if (oaa < 0.0) { return saa - delta * MathUtils.RTOD; diff --git a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOp.java b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOp.java index 94aaa25c..8c82ba0a 100644 --- a/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOp.java +++ b/idepix-meris/src/main/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOp.java @@ -333,9 +333,27 @@ private double computeChiW(int x, int y, Tile winduTile, Tile windvTile, Tile sa return MathUtils.RTOD * (Math.acos(Math.cos(arg))); } + static double computeChiW1(double windU, double windV, double saa) { + final double phiw = azimuth(windU, windV); + /* and "scattering" angle */ + final double arg = MathUtils.DTOR * (saa - phiw); + return MathUtils.RTOD * (Math.acos(Math.cos(arg))); + } + + static double computeChiW2(double windU, double windV, double saa) { + final double phiw = MathUtils.RTOD * Math.atan2(windU, windV); + final double delta = (720.0 + saa - phiw) % 360.0; + if (delta > 180.0) { + return 360.0 - delta; + } else { + return delta; + } + } + private double computeRhoGlint(int x, int y, Tile winduTile, Tile windvTile, Tile szaTile, Tile vzaTile, Tile saaTile, Tile vaaTile) { - final double chiw = computeChiW(x, y, winduTile, windvTile, saaTile); + //final double chiw = computeChiW(x, y, winduTile, windvTile, saaTile); + final double chiw = computeChiW2(winduTile.getSampleFloat(x, y), windvTile.getSampleFloat(x, y), saaTile.getSampleFloat(x, y)); final float vaa = vaaTile.getSampleFloat(x, y); final float saa = saaTile.getSampleFloat(x, y); final double deltaAzimuth = (float) IdepixUtils.computeAzimuthDifference(vaa, saa); @@ -390,7 +408,7 @@ private GeoPos getGeoPos(int x, int y) { return geoPos; } - private double azimuth(double x, double y) { + static double azimuth1(double x, double y) { if (y > 0.0) { // DPM #2.6.5.1.1-1 return (MathUtils.RTOD * Math.atan(x / y)); @@ -402,6 +420,9 @@ private double azimuth(double x, double y) { return (x >= 0.0 ? 90.0 : 270.0); } } + static double azimuth(double y, double x) { + return MathUtils.RTOD * Math.atan2(y, x); + } public static class Spi extends OperatorSpi { public Spi() { diff --git a/idepix-meris/src/test/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOpTest.java b/idepix-meris/src/test/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOpTest.java new file mode 100644 index 00000000..dc141db8 --- /dev/null +++ b/idepix-meris/src/test/java/org/esa/snap/idepix/meris/IdepixMerisWaterClassificationOpTest.java @@ -0,0 +1,63 @@ +package org.esa.snap.idepix.meris; + +import junit.framework.TestCase; +import org.esa.snap.core.util.math.MathUtils; +import org.esa.snap.idepix.core.util.IdepixUtils; + +/** + * TODO add API doc + * + * @author Martin Boettcher + */ +public class IdepixMerisWaterClassificationOpTest extends TestCase { + public void testAtan() { + double ator = Math.PI / 180.0; + for (double y = -10.0; y <= 10.0; y += 1.0) { + for (double x = -10.0; y <= 10.0; y += 1.0) { + double v1 = IdepixMerisWaterClassificationOp.azimuth1(x * ator, y * ator); + double v2 = IdepixMerisWaterClassificationOp.azimuth(x * ator, y * ator); + assertEquals(x+","+y, v1, v2, 1.0e-5); + } + } + } + + public void testChiw() { + double ator = Math.PI / 180.0; + for (double u = -180.0; u <= 180.0; u += 45.0) { + for (double v = -180.0; v <= 180.0; v += 45.0) { + for (double s = -180.0; s <= 180.0; s += 15.0) { + double w1 = IdepixMerisWaterClassificationOp.computeChiW1(u * ator, v * ator, s); + double w2 = IdepixMerisWaterClassificationOp.computeChiW2(u * ator, v * ator, s); + assertEquals(u + "," + v + "," + s, w1, w2, 1.0e-5); + } + } + } + } + + public void testCos() { + for (double oaa = -180.0; oaa <= 360.0; oaa += 45.0) { + for (double saa = -180.0; saa <= 360.0; saa += 45.0) { + final double deltaPhi; + if (oaa < 0.0) { + deltaPhi = 360.0 - Math.abs(oaa) - saa; + } else { + deltaPhi = saa - oaa; + } + double v1 = Math.cos(MathUtils.DTOR * deltaPhi); + double v2 = Math.cos(MathUtils.DTOR * (saa - oaa)); + assertEquals(oaa + "," + saa, v1, v2, 1e-5); + } + } + } + + public void testAzimuthDifference() { + for (double oaa = -180.0; oaa <= 360.0; oaa += 45.0) { + for (double saa = -180.0; saa <= 360.0; saa += 45.0) { + double v1 = IdepixUtils.computeAzimuthDifference1(oaa, saa); + double v2 = IdepixUtils.computeAzimuthDifference(oaa, saa); + assertEquals(oaa + "," + saa, v1, v2, 1e-5); + } + } + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 49185f57..9513df00 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ UTF-8 - 9.0.5-SNAPSHOT + 9.0.6-SNAPSHOT RELEASE82 2.0.05