/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008 - 2015, Geomatys * * 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.geotoolkit.display2d.style.renderer; import com.vividsolutions.jts.geom.Geometry; import java.awt.Color; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.image.*; import java.awt.image.renderable.ParameterBlock; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.logging.Level; import javax.media.jai.ImageLayout; import javax.media.jai.JAI; import javax.media.jai.LookupTableJAI; import javax.media.jai.NullOpImage; import javax.media.jai.OpImage; import javax.media.jai.RenderedOp; import org.apache.sis.geometry.Envelope2D; import org.apache.sis.geometry.GeneralEnvelope; import org.geotoolkit.storage.coverage.CoverageReference; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.coverage.grid.*; import org.geotoolkit.coverage.io.CoverageStoreException; import org.geotoolkit.coverage.io.DisjointCoverageDomainException; import org.geotoolkit.coverage.io.GridCoverageReadParam; import org.geotoolkit.coverage.io.GridCoverageReader; import org.geotoolkit.data.query.Query; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display2d.GO2Utilities; import org.geotoolkit.display2d.canvas.RenderingContext2D; import org.geotoolkit.display2d.primitive.ProjectedCoverage; import org.geotoolkit.display2d.style.CachedRasterSymbolizer; import org.geotoolkit.display2d.style.CachedSymbolizer; import org.geotoolkit.filter.visitor.DefaultFilterVisitor; import org.apache.sis.geometry.Envelopes; import org.geotoolkit.geometry.jts.JTS; import org.geotoolkit.image.interpolation.Interpolation; import org.geotoolkit.image.interpolation.InterpolationCase; import org.geotoolkit.image.interpolation.Resample; import org.geotoolkit.image.interpolation.Rescaler; import org.geotoolkit.image.iterator.PixelIterator; import org.geotoolkit.image.iterator.PixelIteratorFactory; import org.geotoolkit.internal.referencing.CRSUtilities; import org.geotoolkit.map.CoverageMapLayer; import org.geotoolkit.map.DefaultCoverageMapLayer; import org.geotoolkit.map.ElevationModel; import org.geotoolkit.utility.parameter.ParametersExt; import org.geotoolkit.process.ProcessDescriptor; import org.geotoolkit.process.ProcessException; import org.geotoolkit.processing.coverage.shadedrelief.ShadedReliefDescriptor; import org.geotoolkit.processing.image.bandselect.BandSelectDescriptor; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.geotoolkit.referencing.operation.transform.EarthGravitationalModel; import org.apache.sis.referencing.operation.transform.LinearTransform; import org.geotoolkit.style.StyleConstants; import org.geotoolkit.style.function.Categorize; import org.geotoolkit.style.function.CompatibleColorModel; import org.geotoolkit.style.function.DefaultInterpolationPoint; import org.geotoolkit.style.function.Interpolate; import org.geotoolkit.style.function.InterpolationPoint; import org.geotoolkit.style.function.Jenks; import org.geotoolkit.style.function.Method; import org.geotoolkit.style.function.Mode; import org.geotoolkit.image.BufferedImages; import org.opengis.filter.Filter; import org.opengis.filter.FilterVisitor; import org.opengis.filter.PropertyIsEqualTo; import org.opengis.filter.expression.Function; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import org.opengis.geometry.Envelope; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import org.opengis.style.ChannelSelection; import org.opengis.style.ColorMap; import org.opengis.style.ContrastEnhancement; import org.opengis.style.ContrastMethod; import org.opengis.style.RasterSymbolizer; import org.opengis.style.SelectedChannelType; import org.opengis.style.ShadedRelief; import org.opengis.util.FactoryException; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.util.ArgumentChecks; import org.geotoolkit.coverage.Category; import org.geotoolkit.factory.FactoryFinder; import org.geotoolkit.factory.Hints; import org.geotoolkit.image.palette.PaletteFactory; import org.geotoolkit.internal.jdk8.JDK8; import org.geotoolkit.metadata.ImageStatistics; import org.geotoolkit.processing.coverage.statistics.StatisticOp; import org.geotoolkit.processing.coverage.statistics.Statistics; import org.geotoolkit.processing.image.dynamicrange.DynamicRangeStretchProcess; import org.geotoolkit.style.MutableStyleFactory; import static org.geotoolkit.style.StyleConstants.DEFAULT_CATEGORIZE_LOOKUP; import static org.geotoolkit.style.StyleConstants.DEFAULT_FALLBACK; import org.geotoolkit.style.function.DefaultInterpolate; import org.opengis.coverage.Coverage; import org.opengis.coverage.SampleDimension; import org.opengis.metadata.content.CoverageDescription; /** * Symbolizer renderer adapted for Raster. * * @author Johann Sorel (Geomatys) * @author Cédric Briançon (Geomatys) * @author Marechal remi (Geomatys) */ public class DefaultRasterSymbolizerRenderer extends AbstractCoverageSymbolizerRenderer<CachedRasterSymbolizer>{ /** * Style factory object use to generate in some case to interpret raster with no associated style. * * @see #applyColorMapStyle(CoverageReference, GridCoverage2D, RasterSymbolizer) */ public static final MutableStyleFactory SF = (MutableStyleFactory) FactoryFinder.getStyleFactory( new Hints(Hints.STYLE_FACTORY, MutableStyleFactory.class)); public DefaultRasterSymbolizerRenderer(final SymbolizerRendererService service, final CachedRasterSymbolizer symbol, final RenderingContext2D context){ super(service,symbol,context); } /** * {@inheritDoc } */ @Override public void portray(final ProjectedCoverage projectedCoverage) throws PortrayalException { try { GridCoverage2D dataCoverage = getObjectiveCoverage(projectedCoverage); GridCoverage2D elevationCoverage = getObjectiveElevationCoverage(projectedCoverage); final CoverageMapLayer coverageLayer = projectedCoverage.getLayer(); final CoverageReference ref = coverageLayer.getCoverageReference(); assert ref != null : "CoverageMapLayer.getCoverageReference() contract don't allow null pointeur."; if (dataCoverage == null) { //LOGGER.log(Level.WARNING, "RasterSymbolizer : Reprojected coverage is null."); return; } final RasterSymbolizer sourceSymbol = symbol.getSource(); //////////////////////////////////////////////////////////////////// // 2 - Select bands to style / display // //////////////////////////////////////////////////////////////////// //band select ---------------------------------------------------------- //works as a JAI operation final int nbDim = dataCoverage.getNumSampleDimensions(); if (nbDim > 1) { //we can change sample dimension only if we have more then one available. final ChannelSelection selections = sourceSymbol.getChannelSelection(); if (selections != null) { final SelectedChannelType channel = selections.getGrayChannel(); if (channel != null) { //single band selection final int[] indices = new int[]{ getBandIndice(channel.getChannelName(), dataCoverage) }; dataCoverage = selectBand(dataCoverage, indices); } else { final SelectedChannelType[] channels = selections.getRGBChannels(); final int[] selected = new int[]{ getBandIndice(channels[0].getChannelName(), dataCoverage), getBandIndice(channels[1].getChannelName(), dataCoverage), getBandIndice(channels[2].getChannelName(), dataCoverage) }; //@Workaround(library="JAI",version="1.0.x") //TODO when JAI has been rewritten, this test might not be necessary anymore //check if selection actually does something if (!(selected[0] == 0 && selected[1] == 1 && selected[2] == 2) || nbDim != 3) { dataCoverage = selectBand(dataCoverage, selected); } } } } /* * If we haven't got any reprojection we delegate affine transformation to java2D * we must switch to objectiveCRS for grid coverage */ renderingContext.switchToObjectiveCRS(); //////////////////////////////////////////////////////////////////// // 4 - Apply style // //////////////////////////////////////////////////////////////////// // RenderedImage dataImage = dataCoverage.getRenderedImage(); RenderedImage dataImage = applyStyle(ref, dataCoverage, elevationCoverage, sourceSymbol); final MathTransform2D trs2D = dataCoverage.getGridGeometry().getGridToCRS2D(PixelOrientation.UPPER_LEFT); //////////////////////////////////////////////////////////////////// // 5 - Correct cross meridian problems / render // //////////////////////////////////////////////////////////////////// if (renderingContext.wraps == null) { //single rendering renderCoverage(projectedCoverage, dataImage, trs2D); } else { //check if the geometry overlaps the meridian int nbIncRep = renderingContext.wraps.wrapIncNb; int nbDecRep = renderingContext.wraps.wrapDecNb; final Geometry objBounds = JTS.toGeometry(dataCoverage.getEnvelope()); // geometry cross the far east meridian, geometry is like : // POLYGON(-179,10, 181,10, 181,-10, 179,-10) if (objBounds.intersects(renderingContext.wraps.wrapIncLine)) { //duplicate geometry on the other warp line nbDecRep++; } // geometry cross the far west meridian, geometry is like : // POLYGON(-179,10, -181,10, -181,-10, -179,-10) else if (objBounds.intersects(renderingContext.wraps.wrapDecLine)) { //duplicate geometry on the other warp line nbIncRep++; } renderCoverage(projectedCoverage, dataImage, trs2D); //-- repetition of increasing and decreasing sides. for (int i = 0; i < nbDecRep; i++) { g2d.setTransform(renderingContext.wraps.wrapDecObjToDisp[i]); renderCoverage(projectedCoverage, dataImage, trs2D); } for (int i = 0; i < nbIncRep; i++) { g2d.setTransform(renderingContext.wraps.wrapIncObjToDisp[i]); renderCoverage(projectedCoverage, dataImage, trs2D); } } renderingContext.switchToDisplayCRS(); } catch (DisjointCoverageDomainException e) { LOGGER.log(Level.FINE,"Disjoint exception: "+e.getMessage(),e); } catch (Exception e) { LOGGER.log(Level.WARNING,"Portrayal exception: "+e.getMessage(),e); } } /** * Apply style on current coverage.<br><br> * * Style application follow way given by * <a href="http://portal.opengeospatial.org/files/?artifact_id=16700">OpenGIS_Symbology_Encoding_Implementation_Specification</a> sheet 32. * * @param ref needed to compute statistics from internal metadata in case where missing informations. * @param coverage current styled coverage. * @param elevationCoverage needed object to generate shaded relief, {àcode null} if none. * @param styleElement the {@link RasterSymbolizer} which contain styles properties. * @return styled coverage representation. * @throws ProcessException if problem during apply Color map or shaded relief styles. * @throws FactoryException if problem during apply shaded relief style. * @throws TransformException if problem during apply shaded relief style. * @throws PortrayalException if problem during apply contrast enhancement style. * @throws java.io.IOException if problem during style application * @see #applyColorMapStyle(CoverageReference, org.geotoolkit.coverage.grid.GridCoverage2D, org.opengis.style.RasterSymbolizer) * @see #applyShadedRelief(java.awt.image.RenderedImage, org.geotoolkit.coverage.grid.GridCoverage2D, org.geotoolkit.coverage.grid.GridCoverage2D, org.opengis.style.RasterSymbolizer) * @see #applyContrastEnhancement(java.awt.image.RenderedImage, org.opengis.style.RasterSymbolizer) */ public static RenderedImage applyStyle(CoverageReference ref, GridCoverage2D coverage, GridCoverage2D elevationCoverage, final RasterSymbolizer styleElement) throws ProcessException, FactoryException, TransformException, PortrayalException, IOException { RenderedImage image = applyColorMapStyle(ref, coverage, styleElement); image = applyShadedRelief(image, coverage, elevationCoverage, styleElement); image = applyContrastEnhancement(image, styleElement); return image; } /** * Returns contrast enhancement modified image. * * @param image worked image. * @param styleElement the {@link RasterSymbolizer} which contain contrast enhancement properties. * @return contrast enhancement modified image. * @throws PortrayalException if problem during gamma value application * @see #brigthen(java.awt.image.RenderedImage, int) */ private static RenderedImage applyContrastEnhancement(RenderedImage image, final RasterSymbolizer styleElement) throws PortrayalException { ArgumentChecks.ensureNonNull("image", image); ArgumentChecks.ensureNonNull("styleElement", styleElement); //-- contrast enhancement ------------------- final ContrastEnhancement ce = styleElement.getContrastEnhancement(); if(ce != null && image.getColorModel() instanceof ComponentColorModel){ // histogram/normalize adjustment ---------------------------------- final ContrastMethod method = ce.getMethod(); if (ContrastMethod.HISTOGRAM.equals(method)) { image = equalize(image); } else if(ContrastMethod.NORMALIZE.equals(method)) { image = normalize(image); } // gamma correction ------------------------------------------------ final Double gamma = ce.getGammaValue().evaluate(null, Double.class); if (gamma != null && gamma != 1) { //Specification : page 35 // A “GammaValue” tells how much to brighten (values greater than 1.0) or dim (values less than 1.0) an image. image = brigthen(image, (int) ((gamma - 1) * 255f)); } } return image; } /** * Apply shaded relief on the image parameter from coverage geographic properties and elevation coverage properties. * * @param colorMappedImage image result issue from {@link #applyColorMapStyle(CoverageReference, org.geotoolkit.coverage.grid.GridCoverage2D, org.opengis.style.RasterSymbolizer) } * @param coverage base coverage * @param elevationCoverage elevation coverage if exist, should be {@code null}, * if {@code null} image is just transformed into {@link BufferedImage#TYPE_INT_ARGB}. * @param styleElement the {@link RasterSymbolizer} which contain shaded relief properties. * @return image with shadow. * @throws FactoryException if problem during DEM generation. * @throws TransformException if problem during DEM generation. * @see #getDEMCoverage(org.geotoolkit.coverage.grid.GridCoverage2D, org.geotoolkit.coverage.grid.GridCoverage2D) */ private static RenderedImage applyShadedRelief(RenderedImage colorMappedImage, final GridCoverage2D coverage, final GridCoverage2D elevationCoverage, final RasterSymbolizer styleElement) throws FactoryException, TransformException, ProcessException { ArgumentChecks.ensureNonNull("colorMappedImage", colorMappedImage); ArgumentChecks.ensureNonNull("coverage", coverage); ArgumentChecks.ensureNonNull("styleElement", styleElement); //-- shaded relief--------------------------------------------------------- final ShadedRelief shadedRel = styleElement.getShadedRelief(); shadingCase: if (shadedRel != null && shadedRel.getReliefFactor() != null) { final double factor = shadedRel.getReliefFactor().evaluate(null, Double.class); if (factor== 0.0) break shadingCase; //BUG ? When using the grid coverage builder the color model is changed if (colorMappedImage.getColorModel() instanceof CompatibleColorModel) { final BufferedImage bi = new BufferedImage(colorMappedImage.getWidth(), colorMappedImage.getHeight(), BufferedImage.TYPE_INT_ARGB); bi.createGraphics().drawRenderedImage(colorMappedImage, new AffineTransform()); colorMappedImage = bi; } //-- ReliefShadow creating -------------------- final GridCoverage2D mntCoverage; if (elevationCoverage != null) { mntCoverage = getDEMCoverage(coverage, elevationCoverage); } else { break shadingCase; //does not have a nice result, still better then nothing //but is really slow to calculate, disabled for now. //mntCoverage = getGeoideCoverage(coverage); } final GridCoverageBuilder gcb = new GridCoverageBuilder(); gcb.setGridGeometry(coverage.getGridGeometry()); gcb.setRenderedImage(colorMappedImage); gcb.setName("tempimg"); final GridCoverage2D ti = gcb.getGridCoverage2D(); final MathTransform1D trs = (MathTransform1D) MathTransforms.linear(factor, 0); final org.geotoolkit.processing.coverage.shadedrelief.ShadedRelief proc = new org.geotoolkit.processing.coverage.shadedrelief.ShadedRelief( ti, mntCoverage, trs); final ParameterValueGroup res = proc.call(); final GridCoverage2D shaded = (GridCoverage2D) res.parameter(ShadedReliefDescriptor.OUT_COVERAGE_PARAM_NAME).getValue(); colorMappedImage = shaded.getRenderedImage(); } return colorMappedImage; } /** * Apply {@linkplain RasterSymbolizer#getColorMap() color map style properties} on current coverage if need.<br><br> * * In case where no {@linkplain ColorMap#getFunction() sample to geophysic} * transformation function is available and coverage is define as {@link ViewType#GEOPHYSICS} * a way is find to avoid empty result, like follow : <br> * The first band from {@linkplain GridCoverage2D#getRenderedImage() coverage image} is selected * and a grayscale color model is apply from {@linkplain ImageStatistics computed image statistic}. * * @param ref needed to compute statistics from internal metadata in case where missing informations. * @param coverage color map style apply on this object. * @param styleElement the {@link RasterSymbolizer} which contain color map properties. * @return image which is the coverage exprimate into {@link ViewType#PHOTOGRAPHIC}. * @throws ProcessException if problem during statistic problem. */ private static RenderedImage applyColorMapStyle(final CoverageReference ref, GridCoverage2D coverage,final RasterSymbolizer styleElement) throws ProcessException, IOException { ArgumentChecks.ensureNonNull("CoverageReference", ref); ArgumentChecks.ensureNonNull("coverage", coverage); ArgumentChecks.ensureNonNull("styleElement", styleElement); RenderedImage resultImage; //Recolor coverage ----------------------------------------------------- ColorMap recolor = styleElement.getColorMap(); //cheat on the colormap if we have only one band and no colormap recolorCase: if ((recolor == null || recolor.getFunction() == null)) { //if there is no geophysic, the same coverage is returned coverage = hasQuantitativeCategory(coverage) ? coverage.view(ViewType.GEOPHYSICS) : coverage; final RenderedImage ri = coverage.getRenderedImage(); final SampleModel sampleMod = ri.getSampleModel(); final ColorModel riColorModel = ri.getColorModel(); /** * Break computing statistic if indexcolormodel is already adapted for java 2d interpretation * (which mean index color model with positive colormap array index -> DataBuffer.TYPE_BYTE || DataBuffer.TYPE_USHORT) * or if image has already 3 or 4 bands Byte typed. */ if (!defaultStyleIsNeeded(sampleMod, riColorModel)) break recolorCase; final int nbBands = sampleMod.getNumBands(); final CoverageDescription covRefMetadata = ref.getMetadata(); ImageStatistics analyse = null; if (covRefMetadata != null) analyse = ImageStatistics.transform(covRefMetadata); if (analyse == null) analyse = Statistics.analyse(ri, true); if (nbBands < 3) { LOGGER.log(Level.FINE, "applyColorMapStyle : fallBack way is choosen." + "GrayScale interpretation of the first coverage image band."); final ImageStatistics.Band band0 = analyse.getBand(0); final double bmin = band0.getMin(); final double bmax = band0.getMax(); final Double mean = band0.getMean(); final Double std = band0.getStd(); double palMin = bmin; double palMax = bmax; if (mean != null && std != null) { palMin = StrictMath.max(bmin, mean - 2 * std); palMax = StrictMath.min(bmax, mean + 2 * std); } assert JDK8.isFinite(palMin) : "Raster Style fallback : minimum value should be finite. min = "+palMin; assert JDK8.isFinite(palMax) : "Raster Style fallback : maximum value should be finite. max = "+palMax; assert palMin >= bmin; assert palMax <= bmax; final List<InterpolationPoint> values = new ArrayList<>(); final double[] nodatas = band0.getNoData(); if (nodatas != null) for (double nodata : nodatas) { values.add(SF.interpolationPoint(nodata, SF.literal(new Color(0, 0, 0, 0)))); } values.add(SF.interpolationPoint(Float.NaN, SF.literal(new Color(0, 0, 0, 0)))); //-- Color palette // Color[] colorsPal = PaletteFactory.getDefault().getColors("rainbow-t"); Color[] colorsPal = PaletteFactory.getDefault().getColors("grayscale"); assert colorsPal.length >= 2; if (colorsPal.length < 4) { final double percent_5 = (colorsPal.length == 3) ? 0.1 : 0.05; final Color[] colorsPalTemp = colorsPal; colorsPal = Arrays.copyOf(colorsPal, colorsPal.length + 2); System.arraycopy(colorsPalTemp, 2, colorsPal, 2, colorsPalTemp.length - 2); colorsPal[colorsPal.length - 1] = colorsPalTemp[colorsPalTemp.length - 1]; colorsPal[1] = DefaultInterpolate.interpolate(colorsPalTemp[0], colorsPalTemp[1], percent_5); colorsPal[colorsPal.length - 2] = DefaultInterpolate.interpolate(colorsPalTemp[colorsPalTemp.length - 2], colorsPalTemp[colorsPalTemp.length - 1], 1 - percent_5); } //-- if difference between band minimum statistic and palette minimum, //-- define values between them as transparency values.add(SF.interpolationPoint(bmin, SF.literal(colorsPal[0]))); assert colorsPal.length >= 4; final double step = (palMax - palMin) / (colorsPal.length - 3);//-- min and max transparency double currentVal = palMin; for (int c = 1; c <= colorsPal.length - 2; c++) { values.add(SF.interpolationPoint(currentVal, SF.literal(colorsPal[c]))); currentVal += step; } assert StrictMath.abs(currentVal - step - palMax) < 1E-9; values.add(SF.interpolationPoint(bmax, SF.literal(colorsPal[colorsPal.length - 1]))); final Function function = SF.interpolateFunction(DEFAULT_CATEGORIZE_LOOKUP, values, Method.COLOR, Mode.LINEAR, DEFAULT_FALLBACK); recolor = GO2Utilities.STYLE_FACTORY.colorMap(function); } else { LOGGER.log(Level.FINE, "RGBStyle : fallBack way is choosen." + "RGB interpretation of the three first coverage image bands."); final int rgbNumBand = (riColorModel.hasAlpha()) ? 4 : 3; assert rgbNumBand <= nbBands; final int[] bands = new int[]{-1, -1, -1, -1}; final double[][] ranges = new double[][]{{-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}}; for (int b = 0; b < rgbNumBand; b++) { final ImageStatistics.Band bandb = analyse.getBand(b); double min = bandb.getMin(); double max = bandb.getMax(); final Double mean = bandb.getMean(); final Double std = bandb.getStd(); if (mean != null && std != null) { min = StrictMath.max(min, mean - 2 * std); max = StrictMath.min(max, mean + 2 * std); } assert JDK8.isFinite(min) : "Raster Style fallback : minimum value should be finite. min = "+min; assert JDK8.isFinite(max) : "Raster Style fallback : maximum value should be finite. max = "+max; bands[b] = b; ranges[b][0] = min; ranges[b][1] = max; } final DynamicRangeStretchProcess p = new DynamicRangeStretchProcess(ri, bands, ranges); final BufferedImage img = p.executeNow(); if (img instanceof WritableRenderedImage) GO2Utilities.removeBlackBorder((WritableRenderedImage)img); return img; } } //-- apply recolor function "sample to geophysic", sample interpretation. if (recolor != null && recolor.getFunction() != null) { //color map is applied on geophysics view //if there is no geophysic, the same coverage is returned coverage = hasQuantitativeCategory(coverage) ? coverage.view(ViewType.GEOPHYSICS) : coverage; resultImage = coverage.getRenderedImage(); final Function fct = recolor.getFunction(); resultImage = recolor(resultImage, fct); } else { //no color map, used the default image rendered view // coverage = coverage.view(ViewType.RENDERED); if (coverage.getViewTypes().contains(ViewType.PHOTOGRAPHIC)) { resultImage = coverage.view(ViewType.PHOTOGRAPHIC).getRenderedImage(); } else { resultImage = coverage.view(ViewType.PACKED).getRenderedImage();//-- same as rendered view into implementation } //-- if RGB force ARGB to delete black border final int[] componentSize = resultImage.getColorModel().getComponentSize(); if (componentSize.length == 3 && componentSize[0] == 8) { resultImage = GO2Utilities.forceAlpha(resultImage); if (resultImage instanceof WritableRenderedImage) GO2Utilities.removeBlackBorder((WritableRenderedImage)resultImage); } } assert resultImage != null : "applyColorMapStyle : image can't be null."; return resultImage; } /** * Returns {@code true} if the given {@link GridCoverage2D} contain an interpretable geophysic {@link Category}, * else {@code false}. * * @param coverage * @return true if coverage contain quantitative category. */ private static boolean hasQuantitativeCategory(final GridCoverage2D coverage) { ArgumentChecks.ensureNonNull("GridCoverage2D", coverage); for (GridSampleDimension gs : coverage.getSampleDimensions()) { final List<Category> categories = gs.getCategories(); if (categories != null) for (Category cat : categories) { if (cat.isQuantitative()) return true; } } return false; } /** * Returns {@code true} if a default style is needed to interpret current data * else {@code false} if java 2d will be able to interprete data. * * @param sampleModel * @param colorModel * @return {@code true} if a style creation is needed to show image datas else {@code false}. */ private static boolean defaultStyleIsNeeded(final SampleModel sampleModel, final ColorModel colorModel) { ArgumentChecks.ensureNonNull("sampleModel", sampleModel); ArgumentChecks.ensureNonNull("colorModel", colorModel); final int[] pixelSampleSize = colorModel.getComponentSize(); assert pixelSampleSize != null; final int sampleSize = pixelSampleSize[0]; if (pixelSampleSize.length > 1) { for (int s = 1; s < pixelSampleSize.length; s++) { if (pixelSampleSize[s] != sampleSize) return false; //-- special case different samplesize. } } if (pixelSampleSize.length == 2) return true; //-- special case where we select first band. final int dataBufferType = sampleModel.getDataType(); //-- one band if (pixelSampleSize.length == 1) { if (!(colorModel instanceof IndexColorModel)) return true; //-- ! IndexColorModel + Byte or UShort case return (!(sampleSize == 8 || (sampleSize == 16 && dataBufferType == DataBuffer.TYPE_USHORT))); } assert pixelSampleSize.length > 2; //-- is RGB or ARGB Byte return sampleSize != 8; } private static int getBandIndice(final String name, final Coverage coverage) throws PortrayalException{ try{ return Integer.parseInt(name); }catch(NumberFormatException ex){ //can be a name for(int i=0,n=coverage.getNumSampleDimensions();i<n;i++){ final SampleDimension sampleDim = coverage.getSampleDimension(i); if (Objects.equals(String.valueOf(sampleDim.getDescription()), n)) { return i; } } } throw new PortrayalException("Band for name/indice "+name+" not found"); } private void renderCoverage(final ProjectedCoverage projectedCoverage, RenderedImage img, MathTransform2D trs2D) throws PortrayalException{ if (trs2D instanceof AffineTransform) { g2d.setComposite(symbol.getJ2DComposite()); try { g2d.drawRenderedImage(img, (AffineTransform)trs2D); } catch (Exception ex) { final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); LOGGER.log(Level.WARNING, sw.toString());//-- more explicite way to debug if(ex instanceof ArrayIndexOutOfBoundsException){ //we can recover when it's an inapropriate componentcolormodel final StackTraceElement[] eles = ex.getStackTrace(); if(eles.length > 0 && ComponentColorModel.class.getName().equalsIgnoreCase(eles[0].getClassName())){ try{ final CoverageReference ref = projectedCoverage.getLayer().getCoverageReference(); final GridCoverageReader reader = ref.acquireReader(); final Map<String,Object> analyze = StatisticOp.analyze(reader,ref.getImageIndex()); ref.recycle(reader); final double[] minArray = (double[])analyze.get(StatisticOp.MINIMUM); final double[] maxArray = (double[])analyze.get(StatisticOp.MAXIMUM); final double min = findExtremum(minArray, true); final double max = findExtremum(maxArray, false); final List<InterpolationPoint> values = new ArrayList<InterpolationPoint>(); values.add(new DefaultInterpolationPoint(Double.NaN, GO2Utilities.STYLE_FACTORY.literal(new Color(0, 0, 0, 0)))); values.add(new DefaultInterpolationPoint(min, GO2Utilities.STYLE_FACTORY.literal(Color.BLACK))); values.add(new DefaultInterpolationPoint(max, GO2Utilities.STYLE_FACTORY.literal(Color.WHITE))); final Literal lookup = StyleConstants.DEFAULT_CATEGORIZE_LOOKUP; final Literal fallback = StyleConstants.DEFAULT_FALLBACK; final Function function = GO2Utilities.STYLE_FACTORY.interpolateFunction( lookup, values, Method.COLOR, Mode.LINEAR, fallback); final CompatibleColorModel model = new CompatibleColorModel(img.getColorModel().getPixelSize(), function); final ImageLayout layout = new ImageLayout().setColorModel(model); img = new NullOpImage(img, layout, null, OpImage.OP_COMPUTE_BOUND); g2d.drawRenderedImage(img, (AffineTransform)trs2D); }catch(Exception e){ //plenty of errors can happen when painting an image monitor.exceptionOccured(e, Level.WARNING); //raise the original error monitor.exceptionOccured(ex, Level.WARNING); } } else { //plenty of errors can happen when painting an image monitor.exceptionOccured(ex, Level.WARNING); } } else { //plenty of errors can happen when painting an image monitor.exceptionOccured(ex, Level.WARNING); } } }else if (trs2D instanceof LinearTransform) { final LinearTransform lt = (LinearTransform) trs2D; final int col = lt.getMatrix().getNumCol(); final int row = lt.getMatrix().getNumRow(); //TODO using only the first parameters of the linear transform throw new PortrayalException("Could not render image, GridToCRS is a not an AffineTransform, found a " + trs2D.getClass()); }else{ throw new PortrayalException("Could not render image, GridToCRS is a not an AffineTransform, found a " + trs2D.getClass() ); } //draw the border if there is one--------------------------------------- CachedSymbolizer outline = symbol.getOutLine(); if(outline != null){ GO2Utilities.portray(projectedCoverage, outline, renderingContext); } } /** * Fix portrayal resolutions on CoverageMapLayer bounds CRS horizontal part dimensions. * * @param resolution default resolution * @param coverageCRS CoverageMapLayer CRS * @return fixed resolutions or input resolution if coverageCRS is null. */ public static double[] fixResolutionWithCRS(final double[] resolution, final CoordinateReferenceSystem coverageCRS) { assert resolution.length == 2; //-- resolution from rendering context (2D space) if (coverageCRS == null) return resolution; final int minOrdi = CRSUtilities.firstHorizontalAxis(coverageCRS); final double[] tempRes = new double[coverageCRS.getCoordinateSystem().getDimension()]; Arrays.fill(tempRes, 1); tempRes[minOrdi] = resolution[0]; tempRes[minOrdi + 1] = resolution[1]; return tempRes; } /** * Set envelope ranges using values map extracted from Query. * This method use coverage CRS axis names to link Query parameters. * * @param values Map<String, Double> extracted from CoverageMapLayer Query * @param bounds Envelope to fix. * @param coverageCRS complete ND CRS * @return fixed Envelope or input bounds parameter if values are null or empty. */ public static Envelope fixEnvelopeWithQuery(final Map<String, Double> values, final Envelope bounds, final CoordinateReferenceSystem coverageCRS) { if (values != null && !values.isEmpty()) { final GeneralEnvelope env = new GeneralEnvelope(coverageCRS); // Set ranges from the map for (int j=0; j < bounds.getDimension(); j++) { env.setRange(j, bounds.getMinimum(j), bounds.getMaximum(j)); } // Set ranges from the filter for (int i = 0; i < coverageCRS.getCoordinateSystem().getDimension(); i++) { final CoordinateSystemAxis axis = coverageCRS.getCoordinateSystem().getAxis(i); final String axisName = axis.getName().getCode(); if (values.containsKey(axisName)) { final Double val = values.get(axisName); env.setRange(i, val, val); } } return env; } return bounds; } /** * Extract query parameters from CoverageMapLayer if his an instance of DefaultCoverageMapLayer. * * @param coverageMapLayer CoverageMapLayer * @return a Map</String,Double> with query parameters or null */ public static Map<String, Double> extractQuery(final CoverageMapLayer coverageMapLayer) { Map<String,Double> values = null; if (coverageMapLayer instanceof DefaultCoverageMapLayer) { final DefaultCoverageMapLayer covMapLayer = (DefaultCoverageMapLayer) coverageMapLayer; final Query query = covMapLayer.getQuery(); if (query != null) { // visit the filter to extract all values final FilterVisitor fv = new DefaultFilterVisitor() { @Override public Object visit(PropertyIsEqualTo filter, Object data) { final Map<String,Double> values = (Map<String,Double>) data; final String expr1 = ((PropertyName)filter.getExpression1()).getPropertyName(); final Double expr2 = Double.valueOf(((Literal)filter.getExpression2()).getValue().toString()); values.put(expr1, expr2); return values; } }; final Filter filter = query.getFilter(); values = (Map<String,Double>) filter.accept(fv, new HashMap<String, Double>()); } } return values; } /** * Return a Digital Elevation Model from source {@link ElevationModel} parameter in function of coverage parameter properties. * * @param coverage * @param elevationModel * @return a Digital Elevation Model from source {@link ElevationModel} parameter in function of coverage parameter properties. * @throws FactoryException * @throws TransformException */ public static GridCoverage2D getDEMCoverage(final GridCoverage2D coverage, final ElevationModel elevationModel) throws FactoryException, TransformException, CoverageStoreException { if (elevationModel == null) return null; // coverage attributs final GridGeometry2D covGridGeom = coverage.getGridGeometry(); final GridEnvelope2D covExtend = covGridGeom.getExtent2D(); final CoordinateReferenceSystem covCRS = coverage.getCoordinateReferenceSystem2D(); final Envelope2D covEnv2d = coverage.getGridGeometry().getEnvelope2D(); final double[] covResolution = coverage.getGridGeometry().getResolution(); final GridCoverageReader elevationReader = elevationModel.getCoverageReader(); final GeneralGridGeometry elevGridGeom = elevationReader.getGridGeometry(0); if (!(elevGridGeom instanceof GridGeometry2D)) { throw new IllegalArgumentException("the Digital Elevation Model should be instance of gridcoverage2D."+elevGridGeom); } final GridGeometry2D elevGridGeom2D = (GridGeometry2D) elevGridGeom; final CoordinateReferenceSystem demCRS = elevGridGeom2D.getCoordinateReferenceSystem2D(); final MathTransform demCRSToCov = CRS.findOperation(demCRS, covCRS, null).getMathTransform(); // dem -> cov if (elevGridGeom2D.getEnvelope2D().equals(coverage.getGridGeometry().getEnvelope2D()) && covExtend.equals(elevGridGeom2D.getExtent2D())) return (GridCoverage2D) elevationReader.read(0, null); final GeneralEnvelope readParamEnv = Envelopes.transform(demCRSToCov.inverse(), covEnv2d); final GridCoverageReadParam gcrp = new GridCoverageReadParam(); gcrp.setCoordinateReferenceSystem(demCRS); gcrp.setEnvelope(readParamEnv); final GridCoverage2D dem = (GridCoverage2D) elevationReader.read(0, gcrp); return getDEMCoverage(coverage, dem); } /** * Return a Digital Elevation Model from source DEM parameter in function of coverage parameter properties. * * @param coverage * @param dem * @return a Digital Elevation Model from source DEM parameter in function of coverage parameter properties. * @throws FactoryException * @throws TransformException */ public static GridCoverage2D getDEMCoverage(final GridCoverage2D coverage, final GridCoverage2D dem) throws FactoryException, TransformException { // coverage attributs final GridGeometry2D covGridGeom = coverage.getGridGeometry(); final GridEnvelope2D covExtend = covGridGeom.getExtent2D(); final GridGeometry2D demGridGeom = dem.getGridGeometry(); //CRS final CoordinateReferenceSystem covCRS = coverage.getCoordinateReferenceSystem2D(); final CoordinateReferenceSystem demCRS = demGridGeom.getCoordinateReferenceSystem2D(); final MathTransform demCRSToCov = CRS.findOperation(demCRS, covCRS, null).getMathTransform(); // dem -> cov if (demCRSToCov.isIdentity()) return dem; final GeneralEnvelope demDestEnv = Envelopes.transform(demCRSToCov, demGridGeom.getEnvelope2D()); // coverage envelope final Envelope2D covEnv = covGridGeom.getEnvelope2D(); /** * if the 2 coverage don't represent the same area we can't compute shadow on coverage. */ if (!demDestEnv.intersects(covEnv, true)) { return null; } // get intersection to affect relief on shared area. GeneralEnvelope intersec = new GeneralEnvelope(demDestEnv); intersec.intersect(covEnv); final RenderedImage demImage = dem.getRenderedImage(); // output mnt creation final BufferedImage destMNT = BufferedImages.createImage(covExtend.width, covExtend.height, demImage); intersec = Envelopes.transform(covGridGeom.getGridToCRS(PixelInCell.CELL_CORNER).inverse(), intersec); final Rectangle areaIterate = new Rectangle((int) intersec.getMinimum(0), (int) intersec.getMinimum(1), (int) Math.ceil(intersec.getSpan(0)), (int) Math.ceil(intersec.getSpan(1))); // dem source to dem dest final MathTransform sourcetodest = MathTransforms.concatenate(dem.getGridGeometry().getGridToCRS(PixelInCell.CELL_CENTER), demCRSToCov, covGridGeom.getGridToCRS(PixelInCell.CELL_CENTER).inverse()); final PixelIterator srcPix = PixelIteratorFactory.createRowMajorIterator(demImage); final Interpolation interpol = Interpolation.create(srcPix, InterpolationCase.BICUBIC, 2); final Resample resampl = new Resample(sourcetodest.inverse(), destMNT, areaIterate, interpol, new double[interpol.getNumBands()]); resampl.fillImage(); final GridCoverageBuilder gcb = new GridCoverageBuilder(); gcb.setCoordinateReferenceSystem(covCRS); gcb.setRenderedImage(destMNT); gcb.setEnvelope(covEnv); return gcb.getGridCoverage2D(); } /** * Create a geoide coverage to mimic an elevation model. * @param coverage * @return * @throws IllegalArgumentException * @throws FactoryException * @throws TransformException */ public static GridCoverage2D getGeoideCoverage(final GridCoverage2D coverage) throws IllegalArgumentException, FactoryException, TransformException{ final RenderedImage base = coverage.getRenderedImage(); final float[][] matrix = new float[base.getHeight()][base.getWidth()]; final EarthGravitationalModel trs = EarthGravitationalModel.create(CommonCRS.WGS84.datum(), 180); final MathTransform dataToLongLat = CRS.findOperation(coverage.getCoordinateReferenceSystem2D(), CommonCRS.WGS84.normalizedGeographic(), null).getMathTransform(); final MathTransform2D gridToCRS = coverage.getGridGeometry().getGridToCRS2D(); final MathTransform gridToLonLat = MathTransforms.concatenate(gridToCRS, dataToLongLat); final float[] buffer = new float[6]; for(int y=0;y<matrix.length;y++){ for(int x=0;x<matrix[0].length;x++){ buffer[0]=x;buffer[1]=y;buffer[2]=0; gridToLonLat.transform(buffer, 0, buffer, 0, 1); trs.transform(buffer, 0, buffer, 0, 1); matrix[y][x] = buffer[2]; } } GridCoverageBuilder gcb = new GridCoverageBuilder(); gcb.setName("geoide"); gcb.setRenderedImage(matrix); gcb.setCoordinateReferenceSystem(coverage.getCoordinateReferenceSystem2D()); gcb.setGridToCRS(gridToCRS); return gcb.getGridCoverage2D(); } //////////////////////////////////////////////////////////////////////////// // RenderedImage JAI image operations /////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc } * * Prepare coverage for Raster rendering. */ @Override protected GridCoverage2D prepareCoverageToResampling(final GridCoverage2D coverageSource, final CachedRasterSymbolizer symbolizer) { return getReadyToResampleCoverage(coverageSource, symbolizer.getSource()); } /** * Analyse input coverage to know if we need to add an alpha channel. Alpha channel is required in photographic * coverage case, in order for the resample to deliver a ready to style image. * * @param source The coverage to analyse. * @param style Style to apply on coverage data. * @return The same coverage as input if style do not require an ARGB data to properly render, or a new ARGB coverage * computed from source data. */ private static GridCoverage2D getReadyToResampleCoverage(final GridCoverage2D source, final RasterSymbolizer style) { final GridSampleDimension[] dims = source.getSampleDimensions(); final ColorMap cMap = style.getColorMap(); if ((cMap != null && cMap.getFunction() != null) || (dims != null && dims.length != 0 && dims[0].getNoDataValues() != null) || !source.getViewTypes().contains(ViewType.PHOTOGRAPHIC)) { return source; } else { final GridCoverage2D photoCvg = source.view(ViewType.PHOTOGRAPHIC); RenderedImage img = photoCvg.getRenderedImage(); final int datatype = img.getSampleModel().getDataType(); if (datatype != DataBuffer.TYPE_BYTE && datatype != DataBuffer.TYPE_USHORT) return source; RenderedImage imga = GO2Utilities.forceAlpha(img); if (imga != img) { final GridCoverageBuilder gcb = new GridCoverageBuilder(); gcb.setName("temp"); gcb.setGridGeometry(source.getGridGeometry()); gcb.setRenderedImage(imga); return gcb.getGridCoverage2D(); } else { return source; } } } /** * Returns a {@link GridCoverage2D} which contain band extracted from sourceCoverage * at band indices given by indice array parameter.<br><br> * * note : out coverage will have same band number than indice array length. * * @param sourceCoverage coverage which contain all needed band. * @param indices an array which contain band index of sourceCoverage to build another {@link GridCoverage2D}. * @return a new {@link GridCoverage2D} with extracted band from sourceCoverage at indice given by indice array. * @throws ProcessException if problem during process band selection. * @see BandSelectProcess */ private static GridCoverage2D selectBand(final GridCoverage2D sourceCoverage, final int[] indices) throws ProcessException { if (sourceCoverage.getNumSampleDimensions() < indices.length) { //not enough bands in the image LOGGER.log(Level.WARNING, "Raster Style define more bands than the data"); return sourceCoverage; } else { RenderedImage image = sourceCoverage.getRenderedImage(); final ProcessDescriptor bandSelectDesc = BandSelectDescriptor.INSTANCE; final ParameterValueGroup param = bandSelectDesc.getInputDescriptor().createValue(); ParametersExt.getOrCreateValue(param, BandSelectDescriptor.IN_IMAGE.getName().getCode()).setValue(image); ParametersExt.getOrCreateValue(param, BandSelectDescriptor.IN_BANDS.getName().getCode()).setValue(indices); final org.geotoolkit.process.Process process = bandSelectDesc.createProcess(param); final ParameterValueGroup output = process.call(); image = (RenderedImage) ParametersExt.getOrCreateValue(output, BandSelectDescriptor.OUT_IMAGE.getName().getCode()).getValue(); final GridCoverageBuilder builder = new GridCoverageBuilder(); builder.setGridCoverage(sourceCoverage); builder.setRenderedImage(image); builder.setSampleDimensions(); return builder.getGridCoverage2D(); } } private static RenderedImage recolor(final RenderedImage image, final Function function){ RenderedImage recolorImage = image; if (function instanceof Categorize) { final Categorize categorize = (Categorize) function; recolorImage = (RenderedImage) categorize.evaluate(image); } else if(function instanceof Interpolate) { final Interpolate interpolate = (Interpolate) function; recolorImage = (RenderedImage) interpolate.evaluate(image); } else if(function instanceof Jenks) { final Jenks jenks = (Jenks) function; recolorImage = (RenderedImage) jenks.evaluate(image); } return recolorImage; } /** * Rescale image colors between datatype bounds (0, 255 for bytes or 0 655535 for short data). This solution will work * well only for byte or (u)short data models. * TODO : change for a less brut-force solution. * @param source The image to process. * @return The rescaled image. */ private static RenderedImage equalize(final RenderedImage source) { final ColorModel srcModel = source.getColorModel(); // Store min and max value for each band, along with the pixel where we've found the position. final double[] minMax = new Rescaler( Interpolation.create(PixelIteratorFactory.createRowMajorIterator(source), InterpolationCase.NEIGHBOR, 2), 0, 255).getMinMaxValue(null); // Compute transformation to apply on pixel values. We want to scale the pixel range in [0..255] final int numBands = srcModel.getNumComponents(); double[] translation = new double[numBands]; final double[] scale = new double[numBands]; // Java 2D manage color scaling until ushort size. After that, current approach is totally wrong. for (int i = 0, j = 0; i < numBands; i++, j += 6) { final double min = 0, max; if (srcModel.getComponentSize(i) > 8) { max = 65532; } else { max = 255; } scale[i] = max / (minMax[j + 3] - minMax[j]); translation[i] = (min - minMax[j]) * scale[i]; } /** * If just one of the bands is already scaled right, it might means that the entire image is right scaled, we * won't perform any operation. */ boolean noOperationNeeded = false; final double scaleEpsilon = 0.05; // 5 % tolerence. final double translationTolerence = 1.0; for (int i = 0; i < scale.length; i++) { if ((scale[i] < 1.00 + scaleEpsilon && scale[i] > 1.00 - scaleEpsilon) && (translation[i] < translationTolerence && translation[i] > -translationTolerence)) { noOperationNeeded = true; break; } } if (noOperationNeeded) { return source; } else { final WritableRenderedImage destination; if (source instanceof WritableRenderedImage) { destination = (WritableRenderedImage) source; } else { destination = BufferedImages.createImage(source.getWidth(), source.getHeight(), source); } final PixelIterator pxIt = PixelIteratorFactory.createRowMajorWriteableIterator(source, destination); int band = 0; while (pxIt.next()) { pxIt.setSampleDouble(pxIt.getSampleDouble() * scale[band] + translation[band]); if (++band >= numBands) { band = 0; } } return destination; } } private static RenderedImage normalize(final RenderedImage source) { double[] mean = new double[] { 128.0,128.0,128.0 }; double[] stDev = new double[] { 34.0,34.0,34.0 }; float[][] CDFnorm = new float[3][]; CDFnorm[0] = new float[256]; CDFnorm[1] = new float[256]; CDFnorm[2] = new float[256]; double mu0 = mean[0]; double mu1 = mean[1]; double mu2 = mean[2]; double twoSigmaSquared0 = 2.0*stDev[0]*stDev[0]; double twoSigmaSquared1 = 2.0*stDev[1]*stDev[1]; double twoSigmaSquared2 = 2.0*stDev[2]*stDev[2]; CDFnorm[0][0] = (float)Math.exp(-mu0*mu0/twoSigmaSquared0); CDFnorm[1][0] = (float)Math.exp(-mu1*mu1/twoSigmaSquared1); CDFnorm[2][0] = (float)Math.exp(-mu2*mu2/twoSigmaSquared2); for ( int i = 1; i < 256; i++ ) { double deviation0 = i - mu0; double deviation1 = i - mu1; double deviation2 = i - mu2; CDFnorm[0][i] = CDFnorm[0][i-1] + (float)Math.exp(-deviation0*deviation0/twoSigmaSquared0); CDFnorm[1][i] = CDFnorm[1][i-1] + (float)Math.exp(-deviation1*deviation1/twoSigmaSquared1); CDFnorm[2][i] = CDFnorm[2][i-1] + (float)Math.exp(-deviation2*deviation2/twoSigmaSquared2); } for ( int i = 0; i < 256; i++ ) { CDFnorm[0][i] /= CDFnorm[0][255]; CDFnorm[1][i] /= CDFnorm[1][255]; CDFnorm[2][i] /= CDFnorm[2][255]; } int[] bins = { 256 }; double[] low = { 0.0D }; double[] high = { 256.0D }; ParameterBlock pb = new ParameterBlock(); pb.addSource(source); pb.add(null); pb.add(1); pb.add(1); pb.add(bins); pb.add(low); pb.add(high); RenderedOp fmt = JAI.create("histogram", pb, null); return JAI.create("matchcdf", fmt, CDFnorm); } //-- indice de confiance 3/10 (cf:johann) private static RenderedImage brigthen(final RenderedImage image,final int brightness) throws PortrayalException{ final ColorModel model = image.getColorModel(); if(model instanceof IndexColorModel){ //no contrast enhance for indexed colormap return image; }else if(model instanceof ComponentColorModel){ byte[][] lut = new byte[3][256]; byte[][] newlut = new byte[3][256]; // initialize lookup table for ( int i = 0; i < 256; i++ ) { lut[0][i] = (byte) i; lut[1][i] = (byte) i; lut[2][i] = (byte) i; } for (int i = 0; i < 256; i++ ) { int red = (int)lut[0][i]&0xFF; int green = (int)lut[1][i]&0xFF; int blue = (int)lut[2][i]&0xFF; newlut[0][i] = clamp(red + brightness); newlut[1][i] = clamp(green + brightness); newlut[2][i] = clamp(blue + brightness); } return colorize(image,newlut); }else{ throw new PortrayalException("Unsupported image color model, found :" + model.getClass()); } } private static byte clamp(final int v) { if ( v > 255 ) { return (byte)255; } else if ( v < 0 ) { return (byte)0; } else { return (byte)v; } } private static RenderedImage colorize(final RenderedImage image, final byte[][] lt) { LookupTableJAI lookup = new LookupTableJAI(lt); ParameterBlock pb = new ParameterBlock(); pb.addSource(image); pb.add(lookup); return JAI.create("lookup", pb, null); } /** * Find the min or max values in an array of double * @param data double array * @param min search min values or max values * @return min or max value. */ private static double findExtremum(final double[] data, final boolean min) { if (data.length > 0) { double extremum = data[0]; if (min) { for (int i = 0; i < data.length; i++) { extremum = Math.min(extremum, data[i]); } } else { for (int i = 0; i < data.length; i++) { extremum = Math.max(extremum, data[i]); } } return extremum; } throw new IllegalArgumentException("Array of " + (min ? "min" : "max") + " values is empty."); } }