|
| 1 | +package org.esa.snap.idepix.slstr; |
| 2 | + |
| 3 | +import com.bc.ceres.core.ProgressMonitor; |
| 4 | +import org.esa.snap.idepix.core.IdepixConstants; |
| 5 | +import org.esa.snap.idepix.core.util.IdepixIO; |
| 6 | +import org.esa.snap.idepix.core.util.IdepixUtils; |
| 7 | +import org.esa.snap.idepix.core.util.SchillerNeuralNetWrapper; |
| 8 | +import eu.esa.opt.processor.rad2refl.Rad2ReflConstants; |
| 9 | +import org.esa.snap.core.datamodel.*; |
| 10 | +import org.esa.snap.core.gpf.Operator; |
| 11 | +import org.esa.snap.core.gpf.OperatorException; |
| 12 | +import org.esa.snap.core.gpf.OperatorSpi; |
| 13 | +import org.esa.snap.core.gpf.Tile; |
| 14 | +import org.esa.snap.core.gpf.annotations.OperatorMetadata; |
| 15 | +import org.esa.snap.core.gpf.annotations.Parameter; |
| 16 | +import org.esa.snap.core.gpf.annotations.SourceProduct; |
| 17 | +import org.esa.snap.core.gpf.annotations.TargetProduct; |
| 18 | +import org.esa.snap.core.util.ProductUtils; |
| 19 | + |
| 20 | +import java.awt.*; |
| 21 | +import java.io.InputStream; |
| 22 | +import java.util.Map; |
| 23 | + |
| 24 | +/** |
| 25 | + * OLCI/SLSTR synergy pixel classification operator. |
| 26 | + * |
| 27 | + * @author olafd |
| 28 | + */ |
| 29 | +@OperatorMetadata(alias = "Idepix.Slstr.Classification", |
| 30 | + version = "3.0", |
| 31 | + internal = true, |
| 32 | + authors = "Olaf Danne", |
| 33 | + copyright = "(c) 2025 by Brockmann Consult", |
| 34 | + description = "IdePix pixel classification operator for SLSTR synergy.") |
| 35 | +public class SlstrClassificationOp extends Operator { |
| 36 | + |
| 37 | + @Parameter(defaultValue = "false", |
| 38 | + label = " Write NN value to the target product.", |
| 39 | + description = " If applied, write Schiller NN value to the target product ") |
| 40 | + private boolean outputSchillerNNValue; |
| 41 | + |
| 42 | + @SourceProduct(alias = "l1b", description = "The source product.") |
| 43 | + Product sourceProduct; |
| 44 | + |
| 45 | + @SourceProduct(alias = "reflOlci") |
| 46 | + private Product olciRad2reflProduct; |
| 47 | + |
| 48 | + @SourceProduct(alias = "waterMask") |
| 49 | + private Product waterMaskProduct; |
| 50 | + |
| 51 | + |
| 52 | + @TargetProduct(description = "The target product.") |
| 53 | + Product targetProduct; |
| 54 | + |
| 55 | + Band cloudFlagBand; |
| 56 | + |
| 57 | + private Band[] olciReflBands; |
| 58 | + |
| 59 | + private Band landWaterBand; |
| 60 | + |
| 61 | + public static final String OLCISLSTR_ALL_NET_NAME = "11x10x4x3x2_207.9.net"; |
| 62 | + |
| 63 | + private static final double THRESH_LAND_MINBRIGHT1 = 0.3; |
| 64 | + private static final double THRESH_LAND_MINBRIGHT2 = 0.25; // test OD 20170411 |
| 65 | + |
| 66 | + ThreadLocal<SchillerNeuralNetWrapper> olciSlstrAllNeuralNet; |
| 67 | + private SlstrCloudNNInterpreter nnInterpreter; |
| 68 | + |
| 69 | + |
| 70 | + @Override |
| 71 | + public void initialize() throws OperatorException { |
| 72 | + setBands(); |
| 73 | + nnInterpreter = SlstrCloudNNInterpreter.create(); |
| 74 | + readSchillerNeuralNets(); |
| 75 | + createTargetProduct(); |
| 76 | + } |
| 77 | + |
| 78 | + private void readSchillerNeuralNets() { |
| 79 | + InputStream olciAllIS = getClass().getResourceAsStream(OLCISLSTR_ALL_NET_NAME); |
| 80 | + olciSlstrAllNeuralNet = SchillerNeuralNetWrapper.create(olciAllIS); |
| 81 | + } |
| 82 | + |
| 83 | + public void setBands() { |
| 84 | + // e.g. Oa07_reflectance |
| 85 | + olciReflBands = new Band[Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length]; |
| 86 | + for (int i = 0; i < Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length; i++) { |
| 87 | + final int suffixStart = Rad2ReflConstants.OLCI_REFL_BAND_NAMES[i].indexOf("_"); |
| 88 | + final String reflBandname = Rad2ReflConstants.OLCI_REFL_BAND_NAMES[i].substring(0, suffixStart); |
| 89 | + olciReflBands[i] = olciRad2reflProduct.getBand(reflBandname + "_reflectance"); |
| 90 | + } |
| 91 | + |
| 92 | + landWaterBand = waterMaskProduct.getBand("land_water_fraction"); |
| 93 | + } |
| 94 | + |
| 95 | + void createTargetProduct() throws OperatorException { |
| 96 | + int sceneWidth = sourceProduct.getSceneRasterWidth(); |
| 97 | + int sceneHeight = sourceProduct.getSceneRasterHeight(); |
| 98 | + |
| 99 | + targetProduct = new Product(sourceProduct.getName(), sourceProduct.getProductType(), sceneWidth, sceneHeight); |
| 100 | + |
| 101 | + // shall be the only target band!! |
| 102 | + cloudFlagBand = targetProduct.addBand(IdepixConstants.CLASSIF_BAND_NAME, ProductData.TYPE_INT16); |
| 103 | + FlagCoding flagCoding = SlstrUtils.createOlciFlagCoding(); |
| 104 | + cloudFlagBand.setSampleCoding(flagCoding); |
| 105 | + targetProduct.getFlagCodingGroup().add(flagCoding); |
| 106 | + |
| 107 | + ProductUtils.copyTiePointGrids(sourceProduct, targetProduct); |
| 108 | + |
| 109 | + ProductUtils.copyGeoCoding(sourceProduct, targetProduct); |
| 110 | + targetProduct.setStartTime(sourceProduct.getStartTime()); |
| 111 | + targetProduct.setEndTime(sourceProduct.getEndTime()); |
| 112 | +// ProductUtils.copyMetadata(sourceProduct, targetProduct); |
| 113 | + |
| 114 | + if (outputSchillerNNValue) { |
| 115 | + final Band nnValueBand = targetProduct.addBand(IdepixConstants.NN_OUTPUT_BAND_NAME, ProductData.TYPE_FLOAT32); |
| 116 | + nnValueBand.setNoDataValue(0.0f); |
| 117 | + nnValueBand.setNoDataValueUsed(true); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + |
| 122 | + @Override |
| 123 | + public void computeTileStack(Map<Band, Tile> targetTiles, Rectangle rectangle, ProgressMonitor pm) throws OperatorException { |
| 124 | + final Tile waterFractionTile = getSourceTile(landWaterBand, rectangle); |
| 125 | + |
| 126 | + final Band olciQualityFlagBand = sourceProduct.getBand(SlstrConstants.OLCI_QUALITY_FLAGS_BAND_NAME); |
| 127 | + final Tile olciQualityFlagTile = getSourceTile(olciQualityFlagBand, rectangle); |
| 128 | + |
| 129 | + final Band slstrCloudAnFlagBand = sourceProduct.getBand(SlstrConstants.SLSTR_CLOUD_AN_FLAG_BAND_NAME); |
| 130 | + final Tile slstrCloudAnFlagTile = getSourceTile(slstrCloudAnFlagBand, rectangle); |
| 131 | + |
| 132 | + Tile[] olciReflectanceTiles = new Tile[Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length]; |
| 133 | + float[] olciReflectance = new float[Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length]; |
| 134 | + for (int i = 0; i < Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length; i++) { |
| 135 | + olciReflectanceTiles[i] = getSourceTile(olciReflBands[i], rectangle); |
| 136 | + } |
| 137 | + |
| 138 | + final Band cloudFlagTargetBand = targetProduct.getBand(IdepixConstants.CLASSIF_BAND_NAME); |
| 139 | + final Tile cloudFlagTargetTile = targetTiles.get(cloudFlagTargetBand); |
| 140 | + |
| 141 | + Band nnTargetBand; |
| 142 | + Tile nnTargetTile = null; |
| 143 | + if (outputSchillerNNValue) { |
| 144 | + nnTargetBand = targetProduct.getBand(IdepixConstants.NN_OUTPUT_BAND_NAME); |
| 145 | + nnTargetTile = targetTiles.get(nnTargetBand); |
| 146 | + } |
| 147 | + try { |
| 148 | + for (int y = rectangle.y; y < rectangle.y + rectangle.height; y++) { |
| 149 | + checkForCancellation(); |
| 150 | + for (int x = rectangle.x; x < rectangle.x + rectangle.width; x++) { |
| 151 | + initCloudFlag(targetTiles.get(cloudFlagTargetBand), y, x); |
| 152 | + final int waterFraction = waterFractionTile.getSampleInt(x, y); |
| 153 | + final boolean isL1bLand = olciQualityFlagTile.getSampleBit(x, y, SlstrConstants.L1_F_LAND); |
| 154 | + final boolean isLand = |
| 155 | + IdepixUtils.isLandPixel(x, y, sourceProduct.getSceneGeoCoding(), isL1bLand, waterFraction); |
| 156 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_LAND, isLand); |
| 157 | + |
| 158 | + for (int i = 0; i < Rad2ReflConstants.OLCI_REFL_BAND_NAMES.length; i++) { |
| 159 | + olciReflectance[i] = olciReflectanceTiles[i].getSampleFloat(x, y); |
| 160 | + } |
| 161 | + |
| 162 | + final boolean l1Invalid = olciQualityFlagTile.getSampleBit(x, y, SlstrConstants.L1_F_INVALID); |
| 163 | + boolean reflectancesValid = IdepixIO.areAllReflectancesValid(olciReflectance); |
| 164 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_INVALID, l1Invalid || !reflectancesValid); |
| 165 | + |
| 166 | + final boolean isSlstrCloudAn137Thresh = |
| 167 | + slstrCloudAnFlagTile.getSampleBit(x, y, SlstrConstants.CLOUD_AN_F_137_THRESH); |
| 168 | + final boolean isSlstrCloudAnGrossCloud = |
| 169 | + slstrCloudAnFlagTile.getSampleBit(x, y, SlstrConstants.CLOUD_AN_F_GROSS_CLOUD); |
| 170 | + |
| 171 | + if (reflectancesValid) { |
| 172 | + SchillerNeuralNetWrapper nnWrapper = olciSlstrAllNeuralNet.get(); |
| 173 | + double[] inputVector = nnWrapper.getInputVector(); |
| 174 | + // use OLCI net instead of OLCI/SLSTR net: |
| 175 | + for (int i = 0; i < inputVector.length; i++) { |
| 176 | + inputVector[i] = Math.sqrt(olciReflectance[i]); |
| 177 | + } |
| 178 | + |
| 179 | + final double nnOutput = nnWrapper.getNeuralNet().calc(inputVector)[0]; |
| 180 | + |
| 181 | + if (!cloudFlagTargetTile.getSampleBit(x, y, IdepixConstants.IDEPIX_INVALID)) { |
| 182 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_AMBIGUOUS, false); |
| 183 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_SURE, false); |
| 184 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD, false); |
| 185 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_SNOW_ICE, false); |
| 186 | + |
| 187 | + // CB 20170406: todo: needed here? |
| 188 | + final boolean cloudSure = olciReflectance[2] > THRESH_LAND_MINBRIGHT1 && |
| 189 | + nnInterpreter.isCloudSure(nnOutput); |
| 190 | + final boolean cloudAmbiguous = olciReflectance[2] > THRESH_LAND_MINBRIGHT2 && |
| 191 | + nnInterpreter.isCloudAmbiguous(nnOutput); |
| 192 | + |
| 193 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_AMBIGUOUS, cloudAmbiguous); |
| 194 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_SURE, cloudSure); |
| 195 | + // request RQ, GK, 20220111: |
| 196 | + final boolean isSynCloud = cloudAmbiguous || cloudSure || isSlstrCloudAn137Thresh || |
| 197 | + isSlstrCloudAnGrossCloud; |
| 198 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD, cloudAmbiguous || cloudSure); |
| 199 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD, isSynCloud); |
| 200 | + cloudFlagTargetTile.setSample(x, y, IdepixConstants.IDEPIX_SNOW_ICE, nnInterpreter.isSnowIce(nnOutput)); |
| 201 | + } |
| 202 | + |
| 203 | + if (nnTargetTile != null) { |
| 204 | + nnTargetTile.setSample(x, y, nnOutput); |
| 205 | + } |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | + } catch (Exception e) { |
| 210 | + throw new OperatorException("Failed to provide cloud screening:\n" + e.getMessage(), e); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + void initCloudFlag(Tile targetTile, int y, int x) { |
| 215 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD, false); |
| 216 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_SURE, false); |
| 217 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_AMBIGUOUS, false); |
| 218 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_SNOW_ICE, false); |
| 219 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_BUFFER, false); |
| 220 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_CLOUD_SHADOW, false); |
| 221 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_COASTLINE, false); |
| 222 | + targetTile.setSample(x, y, IdepixConstants.IDEPIX_LAND, true); // already checked |
| 223 | + } |
| 224 | + |
| 225 | + public static class Spi extends OperatorSpi { |
| 226 | + public Spi() { |
| 227 | + super(SlstrClassificationOp.class); |
| 228 | + } |
| 229 | + } |
| 230 | +} |
0 commit comments