GeoTools : JAI-EXT integration in GeoTools

Contact:

Nicola Lagomarsini, Daniele Romagnoli

Tracker:

 

Tagline:

 

Description

This proposal would like to integrate JAI-EXT project into GeoTools. JAI-EXT is an open-source project developed by GeoSolutions team for replacing the old JAI project with a new framework for high performance image processing. Currently most of the Geospatial data provides information about invalid pixel values called NoData. Those values should be handled correctly when reading or processing raster data. Geospatial processing softwares like GDAL are able to recognize NoData.

The main effort of the JAI-EXT project is support those NoData values in order to achieve better results when processing raster. Another important JAI-EXT feature is the ability to support external ROI object for masking image regions and reducing active computation area.
With the following proposal we would like to allow users to switch between JAI and JAI-EXT operations; this should be done by using the JAIExt class inside the JAI-EXT modules. This class will be able to do switching between JAI and JAI-EXT operations.

It should be pointed out that users should no more employ the OperationDescriptor implementations for executing an image operation; they should instead call the ImageWorker class or directly call the JAI.create() method with a ParameterBlock implementation (not ParameterBlockJAI). These recommendations are required because otherwise an Exception could be thrown when switching from JAI-EXT to JAI operation and continuing to use  ROI or NoData values.

More information about JAI-EXT may be found at the JAI-EXT Wiki page.

Implementation

This proposal provides various modifications on the GeoTools modules in order to replace all the OperationDescriptor calls into ImageWorker or direct JAI calls. Anyway most of the work has been done inside the gt-coverage and gt-render modules, since these two ones are the mostly involved into image processing.

Another section of the gt-coverage module which have been highly modified is the Coverage Operations one. In this section we have change the operation constructors in order to support switching between JAI and JAI-EXT registered operations. These modifications also involve the CoverageProcessor class in order to support update/removal of the Coverage Operations after switching JAI and JAI-EXT.

On the gt-render side,  the main changes involve the modifications to the CoverageProcessingNode implementation in order to support the JAI/JAI-EXT switching. 

It should be noticed that a few classes has been moved from GeoTools to JAI-EXT. These classes are:

  • ColorIndexer in the gt-coverage module;
  • GenericPiecewise in the gt-render module;
  • RasterClassifier in the gt-render module;
  • ArtifactsFilter in the gt-imagemosaic module.

Another interesting point is the move of the BandMerge operation from the gt-process-raster module to the gt-coverage one, also by removing its related classes, since they are all inside the JAI-EXT project.

Also this proposal will upgrade Guava dependency to 17.0 like for GeoServer and GeoWebCache.

Status

Voting has not started yet:

Tasks

This section is used to make sure your proposal is complete (did you remember documentation?) and has enough paid or volunteer time lined up to be a success

 

no progress

(tick)

done

(error)

impeded

(warning)

lack mandate/funds/time

(question)

volunteer needed

  1. API changed based on BEFORE / AFTER (tick)
  2. Update default implementation of the gt-coverage/gt-render modules in order to accept both JAI and JAI-EXT data (tick)
  3. Move JAI related code from GeoTools to JAI-EXT (tick)
  4. Update the user guide

API Changes

ImageWorker

AFTER:

/**
 * Creates a new uninitialized worker with RenderingHints for a {@linkplain #mosaic mosaic operation}
 */
public ImageWorker(RenderingHints hints) {
        setRenderingHints(hints);
}
 
/**
  * The NoData range to check nodata, or {@code null} if none.
  */
private Range nodata;

/**
  * Array of values used for indicating the background values
  */
private double[] destNoData;

/**
  * @return The current NoData Range.
  */
public final Range getNoData() {
       return nodata;
}

/**
  * @return The current destination nodata.
  */
public final double[] getDestinationNoData() {
      return destNoData;
}
 
/**
  * Set the <cite>NoData Range</cite> for checking NoData during computation.
  * 
  * @param nodata The new NoData Range.
  * @return This ImageWorker
  * 
  */
public final ImageWorker setnoData(final Range nodata) {
        this.nodata = nodata;
        if(nodata != null && image != null){
            PlanarImage img = getPlanarImage();
            img.setProperty(NoDataContainer.GC_NODATA, new NoDataContainer(nodata));
            image = img;
        } else if(image != null){
            PlanarImage img = getPlanarImage();
            Object property = img.getProperty(NoDataContainer.GC_NODATA);
			if(property != null && property != Image.UndefinedProperty){
	            img.removeProperty(NoDataContainer.GC_NODATA);
	            image = img;
            }
        }
        invalidateStatistics();
        return this;
}

/**
  * Set the <cite>NoData Range</cite> for checking NoData during computation.
  * 
  * @param nodata The new NoData Range.
  * @return This ImageWorker
  * 
  */
public final ImageWorker setDestinationNoData(final double[] destNoData) {
        this.destNoData = destNoData;
        invalidateStatistics();
        return this;
}
 
public final ImageWorker removeRenderingHints() {
        if (commonHints != null) {
            commonHints = null;
        }
         return this;
}
 
/**
  * Returns the mean value for each band.
  */
public double[] getMean() {...}
 
/**
  * Returns the histogram of an image.
  */
public Histogram getHistogram(int[] numBins, double[] lowValues, double[] highValues) {...}

These are only a part of the various methods added to the ImageWorker class. We added only the most important ones.

ImagingParameterDescriptors

BEFORE:

private static final Object[] AUTHORITIES = {
             "com.sun.media.jai", Citations.JAI,
             "org.geotools",      Citations.GEOTOOLS,
             "org.jaitools.media.jai",Citations.JAI
};

AFTER:

private static final Object[] AUTHORITIES = {
             "com.sun.media.jai", Citations.JAI,
             "org.geotools",      Citations.GEOTOOLS,
             "org.jaitools.media.jai",Citations.JAI,
             "it.geosolutions.jaiext",Citations.JAI
};


CoverageProcessor

BEFORE:

public CoverageProcessor() {
    	this(null);
}
 
public CoverageProcessor(final RenderingHints hints) {...}

AFTER:

protected CoverageProcessor() {
    	this(null);
}

protected CoverageProcessor(final RenderingHints hints) {...}
 
/**
  * This method is called when the user has registered another {@link OperationDescriptor} for an operation
  * and requires to update the various CoverageProcessors.
  */
public static synchronized void updateProcessors(){
        Set<Hints> keySet = processorsPool.keySet();
        for(Hints key : keySet){
            CoverageProcessor processor = processorsPool.get(key);
            processor.scanForPlugins();
        }
}
   
/**
  * This method is called when the user has registered another {@link OperationDescriptor} and must remove the old operation
  * instance from the processors.
  * 
  * @param operationName Name of the operation to remove
  */
public static synchronized void removeOperationFromProcessors(String operationName){
        List<String> operations = JAIExt.getJAINames(operationName); 
        Set<Hints> keySet = processorsPool.keySet();
        for (Hints key : keySet) {
            for (String opName : operations) {
                CoverageProcessor processor = processorsPool.get(key);
                Operation op = processor.getOperation(opName);
                if (op != null) {
                    processor.removeOperation(op);
                }
            }
        }
}  
 
/**
  * Removes the specified operation to this processor. This method is usually invoked at construction time before this processor is made accessible.
  * 
  * @param operation The operation to remove.
  */
protected void removeOperation(final Operation operation) {
            Utilities.ensureNonNull("operation", operation);
            synchronized (operations) {
    
                if (operations.isEmpty()) {
                    return;
                }
                operations.remove(operation.getName().trim());
            }
        }	

GridCoverage2DRIA

BEFORE:

public static GridCoverage2DRIA create(final GridCoverage2D src, final GridGeometry2D dst, final double nodata) {
         return create(src, dst, nodata, null);
}  

 
public static GridCoverage2DRIA create(GridCoverage2D src, GridGeometry2D dst,
            double nodata, Hints hints) {...}
 
public static GridCoverage2DRIA create(final GridCoverage2D src, final GridCoverage2D dst,
            final double nodata) {...}
 
protected GridCoverage2DRIA(final GridCoverage2D src, final GridGeometry2D dst,
             final Vector sources, final ImageLayout layout, final Map configuration,
             final boolean cobbleSources, final BorderExtender extender, final Interpolation interp,
             final double[] nodata) {...}

AFTER:

public static GridCoverage2DRIA create(final GridCoverage2D src, final GridGeometry2D dst, final double nodata) {
        return create(src, dst, nodata, null, null);
}
    
public static GridCoverage2DRIA create(final GridCoverage2D src, final GridCoverage2D dst,
            final double nodata) {
         return create(src, dst, nodata, null);
}
    
public static GridCoverage2DRIA create(GridCoverage2D src, GridGeometry2D dst,
            double nodata, Hints hints){
        return create(src, dst, nodata, hints, null);
}  

public static GridCoverage2DRIA create(GridCoverage2D src, GridGeometry2D dst,
            double nodata, Hints hints, ROI roi) {...}


public static GridCoverage2DRIA create(final GridCoverage2D src, final GridCoverage2D dst,
            final double nodata, ROI roi) {...}
 
protected GridCoverage2DRIA(final GridCoverage2D src, final GridGeometry2D dst,
             final Vector sources, final ImageLayout layout, final Map configuration,
             final boolean cobbleSources, final BorderExtender extender, final Interpolation interp,
             final double[] nodata, ROI roi) {...}
 
/**
  * This property generator computes the properties for the operation "GridCoverage2DRIA" dynamically.
  */
static class GridCoverage2DRIAPropertyGenerator extends PropertyGeneratorImpl {...}

OperationJAI

AFTER:

/**
  * Constructs a grid coverage operation backed by a JAI operation. The operation descriptor
  * must supports the {@code "rendered"} mode (which is the case for most JAI operations).
  *
  * @param operationName JAI operation name
  * @param operation The JAI operation descriptor.
  */
public OperationJAI(final String operationName, final OperationDescriptor operation) {
        this(operation, new ExtendedImagingParameterDescriptors(operationName, operation));
}
 
    
protected void handleJAIEXTParams(ParameterBlockJAI parameters, ParameterValueGroup parameters2) {
        return;
}
    
protected static Map<String, Object> handleROINoDataProperties(Map<String, Object> properties,
            ParameterBlockJAI parameters, GridCoverage2D sourceCoverage, String operationName,
            int roiIndex, int noDataIndex, int backgroundIndex) {
        Map<String, Object> prop = new HashMap<>();
        if (properties != null) {
            prop.putAll(properties);
        }
        // Setting the internal properties
        if (JAIExt.isJAIExtOperation(operationName)) {
            ROI roiParam = (ROI) parameters.getObjectParameter(roiIndex);
            CoverageUtilities.setROIProperty(properties, roiParam);

            Range noDataParam = (Range) parameters.getObjectParameter(noDataIndex);
            if (noDataParam != null || roiParam != null) {
                // NoData must be set
                // Background has been set?
                Object background = parameters.getObjectParameter(backgroundIndex);
                if (background != null) {
                    if (background instanceof double[] || background instanceof Number) {
                        CoverageUtilities.setNoDataProperty(properties, background);
                    } else {
                        // Undefined, set 0
                        
                    }
                }
            }
        }
        return prop;
}
    
    
protected static void handleROINoDataInternal(ParameterBlockJAI parameters,
            GridCoverage2D sourceCoverage, String operationName, int roiIndex, int noDataIndex){
        // Getting the internal ROI property
        ROI innerROI = CoverageUtilities.getROIProperty(sourceCoverage);  
        if(JAIExt.isJAIExtOperation(operationName)){
                ROI roiParam = (ROI) parameters.getObjectParameter(roiIndex);
                ROI newROI = null;
                if(innerROI == null ){
                        newROI = roiParam;
                } else {
                        newROI = roiParam != null ? innerROI.intersect(roiParam) : innerROI;
                }
                parameters.set(newROI, roiIndex);
        }
       
        
        NoDataContainer nodataProp = CoverageUtilities.getNoDataProperty(sourceCoverage);
        Range innerNodata = (Range) ((nodataProp != null) ? nodataProp.getAsRange() : null);  
        if(JAIExt.isJAIExtOperation(operationName)){
                Range noDataParam = (Range) parameters.getObjectParameter(noDataIndex);
                if(noDataParam == null ){
                        parameters.set(innerNodata, noDataIndex);
                }
        }
}
    
protected void extractSources(final ParameterValueGroup parameters,
            final Collection<GridCoverage2D> sources,
            final String[] sourceNames) throws ParameterNotFoundException,
            InvalidParameterValueException {
        Utilities.ensureNonNull("parameters", parameters);
        Utilities.ensureNonNull("sources", sources);
        Utilities.ensureNonNull("sourceNames", sourceNames);
        GridCoverage2D[] sourceArray = new GridCoverage2D[sourceNames.length];
        extractSources(parameters, sourceNames, sourceArray);
        sources.addAll(Arrays.asList(sourceArray));
}   

Crop

BEFORE:

/**
  * Constructs a default {@code "Crop"} operation.
  */
public Crop() {
		super(new DefaultParameterDescriptorGroup(Citations.GEOTOOLS,
 				"CoverageCrop", new ParameterDescriptor[] { SOURCE_0,
 						CROP_ENVELOPE, CROP_ROI,
 						ROI_OPTIMISATION_TOLERANCE,
						FORCE_MOSAIC}));

AFTER:

/**
  * Constructs a default {@code "Crop"} operation.
  */
public Crop() {
		super(new DefaultParameterDescriptorGroup(Citations.JAI,
 				"CoverageCrop", new ParameterDescriptor[] { SOURCE_0,
 						CROP_ENVELOPE, CROP_ROI,
 						ROI_OPTIMISATION_TOLERANCE,
						FORCE_MOSAIC, NODATA, DEST_NODATA}));
 
 
public static final String PARAMNAME_NODATA = "NoData";
public static final String PARAMNAME_DEST_NODATA = "destNoData";
 
	
/**
  * The parameter descriptor used to tell this operation to check NoData
  */
public static final ParameterDescriptor<Range> NODATA = new DefaultParameterDescriptor<Range>(
                        Citations.JAI, PARAMNAME_NODATA,
                        Range.class, // Value class
                        null, // Array of valid values
                        null,  // Default value
                        null,  // Minimal value
                        null,  // Maximal value
                        null, // Unit of measure
                        true); // Parameter is optional
        
/**
  * The parameter descriptor used to tell this operation to set destinationNoData
  */
public static final ParameterDescriptor<double[]> DEST_NODATA = new DefaultParameterDescriptor<double[]>(
                        Citations.JAI, PARAMNAME_DEST_NODATA,
                        double[].class, // Value class
                        null, // Array of valid values
                        null,  // Default value
                        null,  // Minimal value
                        null,  // Maximal value
                        null, // Unit of measure
                        true); // Parameter is optional 

 
 

CoverageUtilities

AFTER:

public static NoDataContainer getNoDataProperty(GridCoverage2D coverage){
        final Object noData = coverage.getProperty(NoDataContainer.GC_NODATA);
        if(noData != null){
            if(noData instanceof NoDataContainer){
                return (NoDataContainer) noData;
            }else if(noData instanceof Double){
                return new NoDataContainer((Double)noData);
            }
        }
        return null;
}
    
public static ROI getROIProperty(GridCoverage2D coverage){
        final Object roi = coverage.getProperty("GC_ROI");
        if(roi != null && roi instanceof ROI){
            return (ROI) roi;
        }
        return null;
}
    
public static void setNoDataProperty(Map<String, Object> properties, Object noData){
        if(noData == null || properties == null){
            return;
        }
        if(noData instanceof Range){
            properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((Range) noData));
        }else if(noData instanceof Double){
            properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((Double) noData));
        }else if(noData instanceof double[]){
            properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((double[]) noData));
        }else if(noData instanceof NoDataContainer){
            properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((NoDataContainer) noData));
        }
}
    
public static void setROIProperty(Map<String, Object> properties, ROI roi){
        if(roi == null || properties == null){
            return;
        }
        properties.put("GC_ROI", roi);
}

modules/library/coverage/pom.xml

AFTER:

<!-- JAIExt -->
<dependency>
		<groupId>it.geosolutions.jaiext.affine</groupId>
		<artifactId>jt-affine</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.algebra</groupId>
		<artifactId>jt-algebra</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.bandmerge</groupId>
		<artifactId>jt-bandmerge</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.bandselect</groupId>
		<artifactId>jt-bandselect</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.border</groupId>
		<artifactId>jt-border</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.buffer</groupId>
		<artifactId>jt-buffer</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.crop</groupId>
		<artifactId>jt-crop</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.lookup</groupId>
		<artifactId>jt-lookup</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.mosaic</groupId>
		<artifactId>jt-mosaic</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.nullop</groupId>
		<artifactId>jt-nullop</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.rescale</groupId>
		<artifactId>jt-rescale</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.stats</groupId>
		<artifactId>jt-stats</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.warp</groupId>
		<artifactId>jt-warp</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.zonal</groupId>
		<artifactId>jt-zonal</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.binarize</groupId>
		<artifactId>jt-binarize</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.format</groupId>
		<artifactId>jt-format</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.colorconvert</groupId>
		<artifactId>jt-colorconvert</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.errordiffusion</groupId>
		<artifactId>jt-errordiffusion</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.orderdither</groupId>
		<artifactId>jt-orderdither</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.colorindexer</groupId>
		<artifactId>jt-colorindexer</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.imagefunction</groupId>
		<artifactId>jt-imagefunction</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.piecewise</groupId>
		<artifactId>jt-piecewise</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.classifier</groupId>
		<artifactId>jt-classifier</artifactId>
</dependency>
<dependency>
		<groupId>it.geosolutions.jaiext.rlookup</groupId>
		<artifactId>jt-rlookup</artifactId>
</dependency>	
<dependency>
		<groupId>it.geosolutions.jaiext.vectorbin</groupId>
		<artifactId>jt-vectorbin</artifactId>
</dependency>

 

Documentation Changes

list the pages effected by this proposal