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/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/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 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; + } + } + } 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..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 @@ -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,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); @@ -146,7 +151,10 @@ 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); + paramsBuffer.put("computeCloudBuffer", computeCloudBuffer); + 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/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/S2IdepixCloudPostProcess2Op.java b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java new file mode 100644 index 00000000..8defa29a --- /dev/null +++ b/idepix-s2msi/src/main/java/org/esa/snap/idepix/s2msi/operators/S2IdepixCloudPostProcess2Op.java @@ -0,0 +1,494 @@ +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.BitSetter; +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 coastal areas and 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 { + + @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; + private int contextSize; + private int cloudBufferSize; + + private int landWaterContextRadius; + private int urbanContextRadius = urbanContextSize / 2; + private int cdiStddevContextRadius = cdiStddevContextSize / 2; + private int contextRadius; + + private RectangleExtender pixelStateRectCalculator; + private RectangleExtender urbanRectCalculator; + private RectangleExtender cdiRectCalculator; + 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 { + + Product cloudBufferProduct = createTargetProduct(classifiedProduct, + classifiedProduct.getName(), + classifiedProduct.getProductType()); + 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"); + + // add previous state for debugging, comment out in operational version + //ProductUtils.copyBand("pixel_classif_flags", classifiedProduct, + // "flags_before_postprocessing", cloudBufferProduct, true); + + 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 { + + 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 + cloudBufferWidth, cdiStddevContextRadius + cloudBufferWidth); + final Rectangle contextExtendedRectangle = new Rectangle(targetRectangle); + contextExtendedRectangle.grow(contextRadius + cloudBufferWidth, contextRadius + cloudBufferWidth); + final Rectangle cloudBufferRectangle = cloudBufferRectCalculator.extend(targetRectangle); + + // 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, cloudBufferRectangle); + final Tile b11Tile = getSourceTile(b11Band, cloudBufferRectangle); + + // 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[][] 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 + // 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 = 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) && 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) && isClear(sourceFlagTile, y, x)) { + fillPatchInAccu(y, x, cloudBufferRectangle, urbanContextSize, clearNearbyAccu); + } + + // add stddev contributions to accu of sums, squares, counts, required for urban cloud distinction + if (cdiExtendedRectangle.contains(x, y)) { + collectCdiSumsAndSquares(y, x, b7Tile, b8Tile, b8aTile, cdiRectangle, cloudBufferRectangle, + m7, c7, m8, c8, n78); + } + + // 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; + if (cloudBufferRectangle.contains(xt, yt)) { + correctFlagsOfPixel(yt, xt, + landNearbyAccu, waterNearbyAccu, clearNearbyAccu, + m7, c7, m8, c8, n78, + sourceFlagTile, b2Tile, b8Tile, b11Tile, + cloudBufferRectangle, targetRectangle, + 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 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 cloudBufferRectangle, Rectangle targetRectangle, + 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); + } + } + // 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 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[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); + final float b11 = b11Tile.getSampleFloat(xt, yt); + final float idx1 = b2 / b11; + final float idx2 = b8 / b11; + //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); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_SURE, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); + } else { + // align cloud flag with combination of ambiguous and sure + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, isAmbigousOrSure(pixelClassifFlags)); + } + } + return pixelClassifFlags; + } + + 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 + if (isCloud(pixelClassifFlags) && isNotCirrusNotWater(pixelClassifFlags) && clearNearbyAccu[jt][it]) { + // 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 = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_AMBIGUOUS, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD_SURE, false); + pixelClassifFlags = BitSetter.setFlag(pixelClassifFlags, IDEPIX_CLOUD, false); + } + } + return pixelClassifFlags; + } + + 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 width, boolean[][] accu) { + // reduce patch to part overlapping with target image + 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; + 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); + } + } + } + } + + + 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 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 isValid(int pixelClassifFlags) { + return (pixelClassifFlags & 1 << IDEPIX_INVALID) == 0; + } + + private static boolean isAmbigousOrSure(int pixelClassifFlags) { + return (pixelClassifFlags & (1 << IDEPIX_CLOUD_AMBIGUOUS | 1 << IDEPIX_CLOUD_SURE)) != 0; + } + + 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); // seems JNI uses different formula + return n == 0 ? Double.NaN : n == 1 ? 0.0 : (c - m * m / n) / n; + } + + + public static class Spi extends OperatorSpi { + public Spi() { + super(S2IdepixCloudPostProcess2Op.class); + } + } +} 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++) { 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..9513df00 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ UTF-8 - 9.0.2 + 9.0.6-SNAPSHOT RELEASE82 2.0.05