Exercise 3 - Cloud Masking Operator

Skip to end of metadata
Go to start of metadata

Learning Targets

  • Introduction to GPF (Graph Processing Framework)
  • Develop a GPF operator
  • Run the GPF operator
  • Convert the cloud masking code
  • Become acquainted with the GPF API classes
    • Operator and OperatorSpi
    • Tile
    • GPF
    • OperatorSpiRegistry

Introduction to GPF

A data processing program, such as the cloud masking tool from exercise 2, can roughly be separated into 3 steps

  1. Initialisation
    1. Retrieving and setting parameters
    2. Opening source product
    3. Configuration of target product
  2. Computation
    1. Retrieving source pixels
    2. Computing target pixels
    3. Writing target pixels
  3. Finalisation
    1. Disposal of resources
    2. Closing file I/O

GPF stands for the Graph Processing Framework which was introduced in BEAM 4.1. The main driver for the development of this framework was the duplication of these 3 steps in many tools and the inability to use the output of one processor as input to another without the implied file I/O.

The GPF offers to users

  • A standard command-line for all operators
  • Chaining of processing nodes without intermediate file I/O
  • Definition of operator graphs and configuration via XML
  • Processing of large raster datasets via tiling
  • Exchange of operator implementations (algorithms)
  • Standard operators such as Merge, Reproject, Mosaic...

The GPF API simplifies processor development. For development of new operators, GPF offers

  • A very simple programming model
  • Parameter injection into operator instances
  • Generate default GUI for operator classes
  • Generate documentation from operator classes

For using existing operators in code, GPF offers

  • Programmatic definition of processing graphs
  • Ability to invoke operators via name
  • A registry which dynamically loads new operators from JARs (plugins)

Develop a GPF operator

Add GPF to BEAM library

The GPF API is a self-standing BEAM module which needs to be added to the classpath.

IntelliJ IDEA
  • Select from the main menu File/Project Structure...
  • Select *Global Libraries
  • Select BEAM-4.6 library
  • Click Add Classes... and select the JARs ceres-binding and beam-gpf from %BEAM_HOME%/modules
  • Click OK

Create a new project Ex3 as described in the previous exercise and attach the BEAM-4.6 global library to the project.

  • Select from the main menu File/Project Structure...
  • Select Modules
  • Switch to the tab Dependencies
  • Click Add...
  • Choose Global Library...
  • Select BEAM-4.6
  • Click OK
Eclipse
  • In the main menu, click Window/Preferences...
  • Select preferences page Java/Build Path/User Libraries
  • Select BEAM-4.6 library
  • Click Add JARs... and select the JARs ceres-binding and beam-gpf from %BEAM_HOME%/modules
  • Attach the BEAM javadocs and source code to the beam-gpf JAR as described in exercise 1
  • Click OK
    Create a new project Ex3 as described in the previous exercise and attach the BEAM-4.6 UserLibrary to the classpath.

Create the CloudMaskOp class (stub)

Create a new class CloudMaskOp which extends abstract org.esa.beam.framework.gpf.Operator class. In the editor, note the red underline below the CloudMaskOp identifier.

IntelliJ IDEA

Place cursor there and press ALT+ENTER (Quick Fix) to generate stubs for the abstract initialize() method.
In the editor, place the cursor on Operator and press CTRL+Q. The javadoc explains which of the two tile computation methods shall be implemented. In the editor, after the initialize() method, press CTRL+O and select to override computeTile(...).

Eclipse

Place cursor there and press CTRL+1 (Quick Fix) to generate stubs for the abstract initialize() method.

In the editor, place the cursor on Operator. The javadoc explains which of the two tile computation methods shall be implemented. In the editor, after the initialize() method, type comp, press CTRL+Space and select to override computeTile(...).

Now you now should have:

CloudMaskOp.java
import org.esa.beam.framework.datamodel.Band;
import org.esa.beam.framework.gpf.Operator;
import org.esa.beam.framework.gpf.OperatorException;
import org.esa.beam.framework.gpf.Tile;

import com.bc.ceres.core.ProgressMonitor;

public class CloudMaskOp extends Operator {

    @Override
    public void initialize() throws OperatorException {
        // TODO Auto-generated method stub
    }

    @Override
    public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm)
            throws OperatorException {
        // TODO Auto-generated method stub
        super.computeTile(targetBand, targetTile, pm);
    }
}

Now the operator template is complete and you can begin to add flesh to the bones. In the initialize() method you will have to place the code which creates the target product. The code is very similar to the initialization code from the previous exercise with the exception that

  1. the source product is already known by operator it retrieved using the Operator.getSourceProduct(id) method using an arbitrary identifier,
  2. the target product is unknown by the operator and is make known by using the Operator.setTargetProduct(tp) method.

For the time being, the computeTile(...) shall only print out the tiles requested by the framework. We will place the actual cloud mask computation here later.

The methods shall now be implemented as follows:

CloudMaskOp.java
import org.esa.beam.framework.gpf.Operator;
// ...

public class CloudMaskOp extends Operator {

    @Override
    public void initialize() throws OperatorException {
        Product sourceProduct = getSourceProduct("aatsrToa");
        int rasterWidth = sourceProduct.getSceneRasterWidth();
        int rasterHeight = sourceProduct.getSceneRasterHeight();
        Product targetProduct = new Product("ATS_CLM_2P", "ATS_CLM_2P",
                                            rasterWidth, rasterHeight);
        ProductUtils.copyTiePointGrids(sourceProduct, targetProduct);
        ProductUtils.copyGeoCoding(sourceProduct, targetProduct);
        Band btempNadirTargetBand = ProductUtils.copyBand("btemp_nadir_1200",
                                                          sourceProduct, targetProduct);
        Band btempFwardTargetBand = ProductUtils.copyBand("btemp_fward_1200",
                                                          sourceProduct, targetProduct);
        Band cloudMaskNadirTargetBand = new Band("cloud_mask_nadir",
                                                 ProductData.TYPE_UINT8,
                                                 rasterWidth, rasterHeight);
        Band cloudMaskFwardTargetBand = new Band("cloud_mask_fward",
                                                 ProductData.TYPE_UINT8,
                                                 rasterWidth, rasterHeight);
        targetProduct.addBand(cloudMaskNadirTargetBand);
        targetProduct.addBand(cloudMaskFwardTargetBand);
        setTargetProduct(targetProduct);
    }

    @Override
    public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm)
            throws OperatorException {
        Rectangle rectangle = targetTile.getRectangle();
        System.out.println("band=" + targetBand.getName() + ", rectangle=" + rectangle);
    }
}

Run the CloudMaskOp class

The BEAM Graph Processing Tool

GPF operators are invoked using the BEAM Graph Processing Tool gpt, which is located in %BEAM4_HOME%/bin. Try using it, e.g. let it print its usage by typing gpt -h in a command shell:

> gpt -h
Usage:
  gpt <op>|<graph-file> [options] [<source-file-1> <source-file-2> ...]

...

In order to invoke gpt via the IDE, you have to call the main method of the GPF class org.esa.beam.framework.gpf.main.Main. Set up the following Run configuration, type Java Application:

Name: CloudMaskOp
Main class: org.esa.beam.framework.gpf.main.Main
Program arguments: CloudMaskOp -SaatsrToa=C:\Downloads\ATS_TOA_1CNPDK20030504_111142_000000772016_00080_06146_0157.N1
VM arguments: -Xmx512M

For the program arguments, please refer to the usage of gpt. The -S specifies a source product, later we will also use the -P option which specifies a processing parameter.

However, if you run the configuration, you should get the follwing error:

Error: Operator SPI not found for operator [CloudMaskOp]

This weird error message wants to inform you that an important support class is still missing. It is the operator Service Provider Interface (OperatorSpi in the UML class diagram above). The operator SPI is

  • required to run the operator code,
  • used to make the operator known to the GPF and therefore
  • used to dynamically extend its capabilities (plug-in).

With the SPI, our operator implementation can be called via the gpt and can then also be used as a processing node in an operator graph (via XML or programmatically).
For more information about Service Provider Interface see Wikipedia.

Publish the operator to the GPF

Two steps are required to register the operator in the GPF:

  1. Add an operator SPI class for the operator class
  2. Expose the operator SPI class in a service provider configuration file

It is a good practice to specify the SPI class as a static nested class of the operator class. The operator alias is provided as an annotation the the operator class. Apply the following changes to your existing CloudMaskOp source code:

In the operator class add an SPI class as follows:

CloudMaskOp.java
import org.esa.beam.framework.gpf.Operator;
import org.esa.beam.framework.gpf.OperatorSpi;
// ...

public class CloudMaskOp extends Operator {

    // ...

    public static class Spi extends OperatorSpi {
        public Spi() {
            super(CloudMaskOp.class);
        }
    }
}

Now create the directory META-INF/services in the source folder of the project. Add a service provider configuration file named org.esa.beam.framework.gpf.OperatorSpi.

IntelliJ IDEA

To do this, right-click the src folder of project Ex3 and use the New/Package command.
Ignore the warning that the name is not a valid package name.

Eclipse

To do this, right-click the src folder of project Ex3 and use the New/Package and New/Folder commands.

Open the file in the editor an enter the fully qualified class name (= class name prefixed by dot separated package path) of the operator SPI:

META-INF/services/org.esa.beam.framework.gpf.OperatorSpi
CloudMaskOp$Spi

Note the $-sign in the class name, which is the standard separator for nested classes in Java. You can place any number of different SPIs in this file, one in each line.

Tip
Within the GPF, the role of the OperatorSpi is also to provide a factory for Operator instances. Therefore it provides two createOperator factory methods which can be overridden in order to change the default behaviour.

When run again, we should get the following output from method computeTile():

band=btemp_nadir_1200, rectangle=java.awt.Rectangle[x=0,y=0,width=512,height=512]
band=btemp_fward_1200, rectangle=java.awt.Rectangle[x=0,y=0,width=512,height=512]
band=cloud_mask_nadir, rectangle=java.awt.Rectangle[x=0,y=0,width=512,height=512]
band=cloud_mask_fward, rectangle=java.awt.Rectangle[x=0,y=0,width=512,height=512]

The computeTile method is called four times, for each of the target bands. The product used in this example has a raster size of 512 x 512 pixels which is the default tile size used by GPF. For larger resolutions than 512 x 512, the GPF would have created more tile processing requests, one for each tile and band.

Tip
Try setting the default tile size of the target product, e.g.
targetProduct.setPreferredTileSize(50, 50);
and run the operator again.

Implement the algorithm of CloudMaskOp

When implementing the computeTile method you will have to access the source and target bands we currently access in the initialize method. When sharing variables across methods, it is convenient and common to use class fields.

IntelliJ IDEA

To do so, place the cursor on the local variable btempNadirTargetBand in the initialize method, press CTRL+ALT+F to convert the variable to a field.

Eclipse

To do so, place the cursor on the local variable btempNadirTargetBand in the initialize method, press CTRL+1 and select Convert local variable to field.

Do so with the other target bands so that you get the following fields:

private Band cloudMaskNadirTargetBand;
private Band cloudMaskFwardTargetBand;
private Band btempNadirTargetBand;
private Band btempFwardTargetBand;

We will also need the two source bands. Get them from the source product, create local variables and then convert them to fields:

private Band btempNadirSourceBand;
private Band btempFwardSourceBand;
// ...

public void initialize() throws OperatorException {
    Product sourceProduct = getSourceProduct("aatsrToa");
    btempNadirSourceBand = sourceProduct.getBand("btemp_nadir_1200");
    btempFwardSourceBand = sourceProduct.getBand("btemp_fward_1200");
    // ...
}

We will also need the parameter btempThreshold as field of the class:

private double btempThreshold = 280.0; // K
// ...

Now the computTile method can be implemented as follows:

@Override
    public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm)
            throws OperatorException {
        Rectangle rectangle = targetTile.getRectangle();
        Tile btempNadirSourceTile = getSourceTile(btempNadirSourceBand,
                                                  rectangle, pm);
        Tile btempFwardSourceTile = getSourceTile(btempFwardSourceBand,
                                                  rectangle, pm);
        if (targetBand == cloudMaskNadirTargetBand) {
            for (int y = targetTile.getMinY(); y <= targetTile.getMaxY(); y++) {
                for (int x = targetTile.getMinX(); x <= targetTile.getMaxX(); x++) {
                    double btemp = btempNadirSourceTile.getSampleDouble(x, y);
                    targetTile.setSample(x, y, btemp < btempThreshold ? 1 : 0);
                }
            }
        } else if (targetBand == cloudMaskFwardTargetBand) {
            for (int y = targetTile.getMinY(); y <= targetTile.getMaxY(); y++) {
                for (int x = targetTile.getMinX(); x <= targetTile.getMaxX(); x++) {
                    double btemp = btempFwardSourceTile.getSampleDouble(x, y);
                    targetTile.setSample(x, y, btemp < btempThreshold ? 1 : 0);
                }
            }
        } else if (targetBand == btempNadirTargetBand) {
            targetTile.setRawSamples(btempNadirSourceTile.getRawSamples());
        } else if (targetBand == btempFwardTargetBand) {
            targetTile.setRawSamples(btempFwardSourceTile.getRawSamples());
        }
    }

An alternative to the implementing the computeTile method is the computeTileStack method. computeTile successively computes a tile for all target bands, while computeTileStack computes the tiles for all target bands all at once. For some algorithms all results are computed in one step, for example the output vector of a neural network, while other algorithms can compute their outputs independently, for example filters or map projections. If operators are invoked within VISAT, the computeTile method has runtime advantages, because most often, users display single bands. If operators are invoked from gpt, a target product gets written to disk and in this case the computeTileStack method will be preferably called, if possible.

Here is the equivalent implementation for the computeTileStack method:

@Override
    public void computeTileStack(Map<Band, Tile> targetTiles,
                                 Rectangle targetRectangle,
                                 ProgressMonitor pm) throws OperatorException {

        Tile btempNadirSourceTile = getSourceTile(btempNadirSourceBand,
                                                  targetRectangle, pm);
        Tile btempFwardSourceTile = getSourceTile(btempFwardSourceBand,
                                                  targetRectangle, pm);

        Tile btempNadirTargetTile = targetTiles.get(btempNadirTargetBand);
        Tile btempFwardTargetTile = targetTiles.get(btempFwardTargetBand);
        Tile cloudMaskNadirTargetTile = targetTiles.get(cloudMaskNadirTargetBand);
        Tile cloudMaskFwardTargetTile = targetTiles.get(cloudMaskFwardTargetBand);

        int x1 = targetRectangle.x;
        int x2 = x1 + targetRectangle.width - 1;
        int y1 = targetRectangle.y;
        int y2 = y1 + targetRectangle.height - 1;

        for (int y = y1; y <= y2; y++) {
            for (int x = x1; x <= x2; x++) {
                double btempNadir = btempNadirSourceTile.getSampleDouble(x, y);
                double btempFward = btempFwardSourceTile.getSampleDouble(x, y);
                btempNadirTargetTile.setSample(x, y, btempNadir);
                btempFwardTargetTile.setSample(x, y, btempFward);
                cloudMaskNadirTargetTile.setSample(x, y, btempNadir < btempThreshold);
                cloudMaskFwardTargetTile.setSample(x, y, btempFward < btempThreshold);
            }
        }
    }

Operator Annotations

Operator metadata

In order to specify an operator alias and other metadata, use the OperatorMetadata annotation:

@OperatorMetadata(alias="MaskClouds", description="Creates an AATSR TOA cloud mask")
public class CloudMaskOp extends Operator {
    // ...
}

Processing parameters

Specify operator processing parameters as fields using the Parameter annotation:

// ...
@Parameter(defaultValue="270.0", unit="K", description="Temperature threshold")
private double btempThreshold;
// ...

Source products

Specify operator source products as fields using the SourceProduct annotation. Write

@SourceProduct(alias="aatsrToa")
private Product sourceProduct;

instead of using local variables

public void initialize() throws OperatorException {
    Product sourceProduct = getSourceProduct("aatsrToa");
    // ...
}

Target product

Specify the operator target product as field using the TargetProduct annotation. Write

@TargetProduct
private Product targetProduct;

instead of using a local variable

public void initialize() throws OperatorException {
    // ...
    Product targetProduct = new Product(...);
    // ...
    setTargetProduct(targetProduct);
}

The @TargetProduct annotation has no properties.

Integration with gpt

  1. Create JAR as described in last exercise: cloud-mask-op.jar
  2. Copy JAR to %BEAM4_HOME%/lib
  3. Run gpt -h
  4. Note MaskClouds now in list of available operators
  5. Run gpt MaskClouds -h
  6. Note generated operator documentation:
    > gpt -h MaskClouds
    Usage:
      gpt MaskClouds [options]
    
    Description:
      Creates an AATSR TOA cloud mask
    
    Source Options:
      -SaatsrToa=<file>    Sets source 'aatsrToa' to <filepath>.
                           This is a mandatory source.
    
    Parameter Options:
      -PbtempThreshold=<double>    Temperature threshold
                                   Default value is '270.0'.
    
    Graph XML Format:
      <graph>
        <id>someGraphId</id>
        <node>
          <id>someNodeId</id>
          <operator>MaskClouds</operator>
          <sources>
            <aatsrToa>${aatsrToa}</aatsrToa>
          </sources>
          <parameters>
            <btempThreshold>double</btempThreshold>
          </parameters>
        </node>
      </graph>
    

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Jan 19, 2010

    Marco Peters says:

    TODO - Explain the class diagram and add sequence diagram in section Introductio...

    TODO - Explain the class diagram and add sequence diagram in section Introduction to GPF.
    TODO - In section Introduction to GPF explain also the programming model and tile computation request.
    TODO - explain the code of computTile and computTileStack in section Implement the algorithm of CloudMaskOp.
    TODO - Explain role of annotations for GPF in section Operator Annotations.
    TODO - Explain all @OperatorMetadata properties in section Operator metadata.
    TODO - Explain all @Parameter properties in section Processing parameters.
    TODO - Explain all @SourceProduct properties and mention @SourceProducts in section Source products.