/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2015, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.renderer.lite.gridcoverage2d; import it.geosolutions.jaiext.lookup.LookupTable; import it.geosolutions.jaiext.lookup.LookupTableFactory; import it.geosolutions.jaiext.piecewise.DefaultPiecewiseTransform1DElement; import it.geosolutions.jaiext.piecewise.PiecewiseTransform1D; import it.geosolutions.jaiext.range.NoDataContainer; import it.geosolutions.jaiext.range.Range; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.RenderedImage; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.media.jai.ImageLayout; import javax.media.jai.JAI; import javax.media.jai.ROI; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.factory.Hints; import org.geotools.image.ImageWorker; import org.geotools.renderer.i18n.ErrorKeys; import org.geotools.renderer.i18n.Errors; import org.geotools.renderer.i18n.Vocabulary; import org.geotools.renderer.i18n.VocabularyKeys; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.styling.AbstractContrastMethodStrategy; import org.geotools.styling.ContrastEnhancement; import org.geotools.styling.ExponentialContrastMethodStrategy; import org.geotools.styling.HistogramContrastMethodStrategy; import org.geotools.styling.LogarithmicContrastMethodStrategy; import org.geotools.styling.NormalizeContrastMethodStrategy; import org.geotools.styling.StyleVisitor; import org.geotools.util.SimpleInternationalString; import org.opengis.coverage.grid.GridCoverage; import org.opengis.filter.expression.Expression; import org.opengis.style.ContrastMethod; import org.opengis.util.InternationalString; /** * This implementations of {@link CoverageProcessingNode} takes care of the * {@link ContrastEnhancement} element of the SLD 1.0 spec. * * @author Simone Giannecchini, GeoSolutions * @authod Daniele Romagnoli, GeoSolutions * */ class ContrastEnhancementNode extends StyleVisitorCoverageProcessingNodeAdapter implements StyleVisitor, CoverageProcessingNode { /* * (non-Javadoc) * @see CoverageProcessingNode#getName() */ public InternationalString getName() { return Vocabulary.formatInternational(VocabularyKeys.CONTRAST_ENHANCEMENT); } /** * Specified the supported Histogram Enhancement algorithms. * * @todo in the future this should be pluggable. */ private final static Set<String> SUPPORTED_HE_ALGORITHMS; /** * This are the different types f histogram equalization that we support for * the moment. MOre should be added soon. * */ static { //load the contrast enhancement operations final HashSet<String> heAlg = new HashSet<String>(2, 1.0f); heAlg.add("NORMALIZE"); heAlg.add("HISTOGRAM"); heAlg.add("LOGARITHMIC"); heAlg.add("EXPONENTIAL"); SUPPORTED_HE_ALGORITHMS = Collections.unmodifiableSet(heAlg); } /** ContrastMethod */ AbstractContrastMethodStrategy contrastEnhancementMethod = null; /** Enhancement type to use. */ private String type = null; /** * Value we'll use for the gamma correction operation. */ private double gammaValue = Double.NaN; /* * (non-Javadoc) * * @see org.geotools.renderer.lite.gridcoverage2d.StyleVisitorAdapter#visit(org.geotools.styling.ContrastEnhancement) */ public void visit(final ContrastEnhancement ce) { // ///////////////////////////////////////////////////////////////////// // // Do nothing if we don't have a valid ContrastEnhancement element. This // would protect us against bad SLDs // // ///////////////////////////////////////////////////////////////////// if (ce == null){ return; } // ///////////////////////////////////////////////////////////////////// // // TYPE of the operation to perform // // ///////////////////////////////////////////////////////////////////// ContrastMethod contrastMethod = ce.getMethod(); if (contrastMethod != null) { final String type = contrastMethod.name(); if (type != null && !type.equalsIgnoreCase("None")) { this.type = type.toUpperCase(); if (!SUPPORTED_HE_ALGORITHMS.contains(type.toUpperCase())) throw new IllegalArgumentException(Errors.format(ErrorKeys.OPERATION_NOT_FOUND_$1, type.toUpperCase())); this.contrastEnhancementMethod = parseContrastEnhancementMethod(contrastMethod, ce.getOptions()); } } // ///////////////////////////////////////////////////////////////////// // // GAMMA // // ///////////////////////////////////////////////////////////////////// final Expression gamma = ce.getGammaValue(); if (gamma != null) { final Number number = gamma.evaluate(null, Double.class); if (number != null) { gammaValue = number.doubleValue(); // check the gamma value if (gammaValue < 0) throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "Gamma", number)); if (Double.isNaN(gammaValue) || Double.isInfinite(gammaValue)) throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "Gamma", number)); } } } private AbstractContrastMethodStrategy parseContrastEnhancementMethod( ContrastMethod method, Map<String, Expression> options) { String name = method.name().toUpperCase(); AbstractContrastMethodStrategy ceMethod = null; if ("NORMALIZE".equals(name)) { Expression algorithm = options.get(AbstractContrastMethodStrategy.ALGORITHM); ceMethod = new NormalizeContrastMethodStrategy(); if (algorithm != null) { ceMethod.setAlgorithm(algorithm); } } else if ("LOGARITHMIC".equalsIgnoreCase(name)) { ceMethod = new LogarithmicContrastMethodStrategy(); } else if ("EXPONENTIAL".equalsIgnoreCase(name)) { ceMethod = new ExponentialContrastMethodStrategy(); } else if ("HISTOGRAM".equalsIgnoreCase(name)) { ceMethod = new HistogramContrastMethodStrategy(); } else { throw new IllegalArgumentException( Errors.format(ErrorKeys.UNSUPPORTED_METHOD_$1, method)); } ceMethod.setOptions(options); return ceMethod; } /** * Default constructor */ public ContrastEnhancementNode() { this(null); } /** * Constructor for a {@link ContrastEnhancementNode} which allows to specify * a {@link Hints} instance to control internal factory machinery. * * @param hints * {@link Hints} instance to control internal factory machinery. */ public ContrastEnhancementNode(final Hints hints) { super( 1, hints, SimpleInternationalString.wrap("ContrastEnhancementNode"), SimpleInternationalString .wrap("Node which applies ContrastEnhancement following SLD 1.0 spec.")); } /* * (non-Javadoc) * * @see org.geotools.renderer.lite.gridcoverage2d.StyleVisitorCoverageProcessingNodeAdapter#execute() */ @SuppressWarnings("unchecked") protected GridCoverage2D execute() { final Hints hints = getHints(); // ///////////////////////////////////////////////////////////////////// // // Get the sources and see what we got to do. Note that if we have more // than once source we'll use only the first one but we'll // // ///////////////////////////////////////////////////////////////////// final List<CoverageProcessingNode> sources = this.getSources(); if (sources != null && !sources.isEmpty()) { final GridCoverage2D source = (GridCoverage2D) getSource(0).getOutput(); GridCoverageRendererUtilities.ensureSourceNotNull(source, this.getName().toString()); GridCoverage2D output; if ((!Double.isNaN(gammaValue) && !Double.isInfinite(gammaValue) && !(Math.abs(gammaValue -1)<1E-6))|| (type != null && type.length() > 0)) { // ///////////////////////////////////////////////////////////////////// // // We have a valid gamma value, let's go ahead. // // ///////////////////////////////////////////////////////////////////// final RenderedImage sourceImage = source.getRenderedImage(); // ///////////////////////////////////////////////////////////////////// // // PREPARATION // // ///////////////////////////////////////////////////////////////////// // // // // Get the ROI and NoData from the input coverageS // //// ROI roi = CoverageUtilities.getROIProperty(source); NoDataContainer noDataContainer = CoverageUtilities.getNoDataProperty(source); Range nodata = noDataContainer != null ? noDataContainer.getAsRange() : null; // // // // Get the source image and if necessary convert it to use a // ComponentColorModel. This way we are sure we will have a // visible image most part of the time. // // // //// // // @todo TODO HACK we need to convert to byte the image when going to // apply HISTOGRAM anyway // //// ImageWorker worker; if(type!=null&&type.equalsIgnoreCase("HISTOGRAM")) { worker = new ImageWorker(sourceImage) .setROI(roi).setNoData(nodata) .setRenderingHints(hints) .forceComponentColorModel() .rescaleToBytes(); } else { worker = new ImageWorker(sourceImage) .setROI(roi).setNoData(nodata) .setRenderingHints(hints) .forceComponentColorModel(); } final int numbands = worker.getNumBands(); // // // // Save the alpha band if present, in order to put it back // later in the loop. We are not going to use it anyway for // the IHS conversion. // // // RenderedImage alphaBand = null; if (numbands % 2 == 0) { // get the alpha band alphaBand = new ImageWorker(worker.getRenderedImage()) .setRenderingHints(hints).retainLastBand() .getRenderedImage(); // strip the alpha band from the original image worker .setRenderingHints(hints).retainBands(numbands - 1); } // // // // Get the single band to work on, which might be the // intensity for RGB(A) or the GRAY channel for Gray(A) // // // ImageWorker intensityWorker; RenderedImage hChannel = null; RenderedImage sChannel = null; final boolean intensity; RenderedImage IHS = null; if (numbands > 1) { // convert the prepared image to IHS colorspace to work // on I band IHS = worker .setRenderingHints(hints).forceColorSpaceIHS() .getRenderedImage(); // get the various singular bands intensityWorker = worker.setRenderingHints(hints).retainFirstBand(); sChannel = new ImageWorker(IHS).setRenderingHints(hints) .retainLastBand().getRenderedImage(); hChannel = new ImageWorker(IHS).setRenderingHints(hints) .retainBands(new int[] { 1 }).getRenderedImage(); intensity = true; } else { // // // // we have only one band we don't need to go to IHS // // // intensityWorker = worker; intensity = false; } // ///////////////////////////////////////////////////////////////////// // // HISTOGRAM ENHANCEMENT // // // // ///////////////////////////////////////////////////////////////////// performContrastEnhancement(intensityWorker, hints); // ///////////////////////////////////////////////////////////////////// // // GAMMA CORRECTION // // Lookup for building the actual lut that caches the gamma // correction function's values. // // ///////////////////////////////////////////////////////////////////// performGammaCorrection(intensityWorker,hints); // ///////////////////////////////////////////////////////////////////// // // POSTPROCESSING // // Take care of the intermediated image we left back. This // means, handle the fact that we might have gone to IHS and // the alpha band. // // ///////////////////////////////////////////////////////////////////// if (intensity) { // // // // IHS --> RGB // // Let's merge the modified IHS image. The message on // the mailing list (see comments for this class) // mentioned that it is required to pass a RenderingHing // with a ImageLayout with the IHS color // model. // // // final ImageLayout imageLayout = new ImageLayout(); imageLayout.setColorModel(IHS.getColorModel()); imageLayout.setSampleModel(IHS.getSampleModel()); final RenderingHints rendHints = new RenderingHints(Collections.EMPTY_MAP); rendHints.add(hints); rendHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT,imageLayout)); // merge and go to rgb again intensityWorker.setRenderingHints(rendHints).addBands(new RenderedImage[]{hChannel, sChannel}, false, null); intensityWorker.setRenderingHints(hints).forceColorSpaceRGB(); } // // // // ALPHA BAND // // Let's merge the alpha band with the image we have rebuilt. // // // if (alphaBand != null) { final ColorModel cm = new ComponentColorModel( numbands >= 3 ? ColorSpace .getInstance(ColorSpace.CS_sRGB) : ColorSpace .getInstance(ColorSpace.CS_GRAY), numbands >= 3 ? new int[] { 8, 8, 8, 8 } : new int[] { 8, 8 }, true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); final ImageLayout imageLayout = new ImageLayout(); imageLayout.setColorModel(cm); imageLayout.setSampleModel(cm.createCompatibleSampleModel(intensityWorker.getRenderedImage().getWidth(), intensityWorker.getRenderedImage().getHeight())); // merge and go to rgb intensityWorker .setRenderingHints(hints) .setRenderingHint(JAI.KEY_IMAGE_LAYOUT,imageLayout) .addBand(alphaBand, false, true, null); } // ///////////////////////////////////////////////////////////////////// // // OUTPUT // // ///////////////////////////////////////////////////////////////////// final int numSourceBands=source.getNumSampleDimensions(); final RenderedImage finalImage = intensityWorker.getRenderedImage(); final int numActualBands= finalImage.getSampleModel().getNumBands(); final GridCoverageFactory factory = getCoverageFactory(); final HashMap<Object,Object> props = new HashMap<Object,Object>(); if(source.getProperties() != null) { props.putAll(source.getProperties()); } // Setting ROI and NODATA if(intensityWorker.getNoData() != null){ props.put(NoDataContainer.GC_NODATA, new NoDataContainer(intensityWorker.getNoData())); } if(intensityWorker.getROI() != null){ props.put("GC_ROI", intensityWorker.getROI()); } if(numActualBands==numSourceBands) { final String name = "ce_coverage" + source.getName(); output = factory.create( name, finalImage, (GridGeometry2D)source.getGridGeometry(), source.getSampleDimensions(), new GridCoverage[]{source}, props); } else { // replicate input bands final GridSampleDimension sd[]= new GridSampleDimension[numActualBands]; for(int i=0;i<numActualBands;i++) sd[i]=(GridSampleDimension) source.getSampleDimension(0); output = factory.create( "ce_coverage"+source.getName().toString(), finalImage, (GridGeometry2D)source.getGridGeometry(), sd, new GridCoverage[]{source}, props); } } else // ///////////////////////////////////////////////////////////////////// // // We do not have a valid gamma value, let's try with a // conservative approach that is, let's forward the source // coverage intact. // // ///////////////////////////////////////////////////////////////////// output = source; return output; } throw new IllegalStateException(Errors.format( ErrorKeys.SOURCE_CANT_BE_NULL_$1, this.getName().toString())); } /** * Performs a contrast enhancement operation on the input image. Note that not all the contrast enhancement operations have been implemented in a * way that is generic enough o handle all data types. * * @param inputImage the input {@link RenderedImage} to work on. * @param hints {@link Hints} to control the contrast enhancement process. * @return a {@link RenderedImage} on which a contrast enhancement has been performed. */ private RenderedImage performContrastEnhancement(ImageWorker inputWorker, final Hints hints) { inputWorker.setRenderingHints(hints); if (contrastEnhancementMethod != null) { RenderedImage inputImage = inputWorker.getRenderedImage(); assert inputImage.getSampleModel().getNumBands() == 1 : inputImage; ContrastEnhancementType ceType = ContrastEnhancementType.getType(contrastEnhancementMethod); return ceType.process(inputWorker, hints, contrastEnhancementMethod.getParameters()); } return inputWorker.getRenderedImage(); } /** * Performs a gamma correction operation on the input image. * * @param inputImage the input {@link RenderedImage} to work on. * @param hints {@link Hints} to control the contrast enhancement process. * @return a {@link RenderedImage} on which a gamma correction has been performed. */ private RenderedImage performGammaCorrection(ImageWorker worker, final Hints hints) { worker.setRenderingHints(hints); // note that we should work on a single band RenderedImage inputImage = worker.getRenderedOperation(); assert inputImage.getSampleModel().getNumBands() == 1 : inputImage; final int dataType = inputImage.getSampleModel().getDataType(); RenderedImage result = inputImage; if (!Double.isNaN(gammaValue) && Math.abs(gammaValue - 1.0) > 1E-6) { if (dataType == DataBuffer.TYPE_BYTE) { // // // // Byte case, use lookup to optimize // // // final byte[] lut = new byte[256]; for (int i = 1; i < lut.length; i++) { lut[i] = (byte) (255.0 * Math.pow(i / 255.0, gammaValue) + 0.5d); } // apply the operation now LookupTable table = LookupTableFactory.create(lut, dataType); worker.lookup(table); } else { // // Generic case // // // STEP 1 do the extrema // final double[] minimum = worker.getMinimums(); final double[] maximum = worker.getMaximums(); // // STEP 2 do the gamma correction by using generic piecewise // final PiecewiseTransform1D<DefaultPiecewiseTransform1DElement> transform = ContrastEnhancementType .generateGammaCorrectedPiecewise(minimum[0], maximum[0], gammaValue); worker.piecewise(transform, Integer.valueOf(0)); } } result = worker.getRenderedImage(); assert result.getSampleModel().getNumBands() == 1 : result; return result; } }