/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, 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.coverage.processing.operation; // J2SE, JAI and extensions import java.awt.Color; import java.awt.geom.AffineTransform; import javax.media.jai.KernelJAI; import javax.media.jai.ParameterList; import javax.media.jai.ParameterListDescriptor; import javax.media.jai.ParameterListDescriptorImpl; import javax.media.jai.operator.GradientMagnitudeDescriptor; // For javadoc import javax.measure.unit.Unit; // OpenGIS dependencies import org.opengis.util.InternationalString; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.MathTransform2D; // Geotools dependencies import org.geotools.coverage.Category; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.processing.OperationJAI; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.util.NumberRange; /** * Edge detector which computes the magnitude of the image gradient vector in two orthogonal * directions. The result of the {@code "GradientMagnitude"} operation may be defined as: *

*

* dst[x][y][b] = {@linkplain Math#sqrt sqrt}( * SH(x,y,b)2 + * SV(x,y,b)2) * × scale *
*

* where {@code SH(x,y,b)} and {@code SV(x,y,b)} are the horizontal and vertical gradient images * generated from band b of the source image by correlating it with the supplied * orthogonal (horizontal and vertical) gradient masks. *

* Before to compute the gradients, the kernels are tested against artificials horizontal and * vertical gradients of one unit of sample / unit of localization. For example: *

*

*

* Kernels are normalized by dividing all their coefficients by the result of this test. In other * words, kernels are normalized in such a way that applying the {@code "GradientMagnitude"} * operation on a horizontal or vertical gradient of 1 such "geophysical" units will give a result * of 1. This is an attempt to give geophysical meaning to the numbers produced by the * {@code "GradientMagnitude"} operation. This normalization depends of the coverage's * {@linkplain GridCoverage2D#getGridGeometry grid geometry}. *

* NOTE: When the masks are symetric (e.g. Sobel, Prewitt (or Smoothed), * isotropic, etc.), then the above-cited algorithm produces the same result * than the "normalization factor" × "spatial factor" published by: * *

* Simpson, J.J. (1990), "On the accurate detection and enhancement of oceanic features * observed in satellite data" in Remote sensing environment, 33:17-33. *
* * However, for non-symetric masks (e.g. Kirsch), then a difference is found. * *

Name: "GradientMagnitude"
* JAI operator: "{@linkplain GradientMagnitudeDescriptor GradientMagnitude}"
* Parameters:

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
NameClassDefault valueMinimum valueMaximum value
{@code "Source"}{@link org.geotools.coverage.grid.GridCoverage2D}N/AN/AN/A
{@code "Mask1"}{@link KernelJAI}{@link KernelJAI#GRADIENT_MASK_SOBEL_HORIZONTAL}N/AN/A
{@code "Mask2"}{@link KernelJAI}{@link KernelJAI#GRADIENT_MASK_SOBEL_VERTICAL}N/AN/A
* * @since 2.2 * * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @see org.geotools.coverage.processing.Operations#gradientMagnitude * @see GradientMagnitudeDescriptor */ public class GradientMagnitude extends OperationJAI { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -1514713427236924048L; /** * Set to {@code true} for enabling some tracing code. */ private static final boolean DEBUG = false; /** * Set to {@code true} to enable automatic kernel normalization. Normalization modifies * kernel coefficients according the "grid to coordinate system" transform in order to get * some meaningful engineering units (e.g. °C/km). The normalization factor is computed by * testing the original kernels against synthetic horizontal and vertical gradients of * 1 sampleUnit/csUnit. */ private static final boolean NORMALIZE = true; /** * The default scale factor to apply on the range computed by {@link #deriveCategory}. For * example a value of 0.25 means that only values from 0 to 25% of the expected maximum will * appears in different colors. */ private static final double DEFAULT_RANGE_SCALE = 1.0; /** * The default color palette for the gradients. */ private static final Color[] DEFAULT_COLOR_PALETTE = new Color[] { new Color( 16, 32, 64), new Color(192,224,255) }; /** * A flag indicating that {@link #getNormalizationFactorSquared} * should tests the horizontal gradient computed by the supplied kernel. */ private static final int HORIZONTAL = 1; /** * A flag indicating that {@link #getNormalizationFactorSquared} * should tests the vertical gradient computed by the supplied kernel. */ private static final int VERTICAL = 2; /** * Constructs a default gradient magnitude operation. */ public GradientMagnitude() { super("GradientMagnitude"); } /** * Returns a scale factor for the supplied kernel. If {@code kernel} computes horizontal * grandient, this method returns {@code scaleX}. Otherwise, if {@code kernel} computes * vertical gradient, then this method returns {@code scaleY}. Otherwise, returns a geometric * combinaison of both. */ private static double getScaleFactor(final KernelJAI kernel, double scaleX, double scaleY) { scaleX *= scaleX; scaleY *= scaleY; double factorX = getNormalizationFactorSquared(kernel, HORIZONTAL); double factorY = getNormalizationFactorSquared(kernel, VERTICAL); double factor2 = (factorX*scaleX + factorY*scaleY) / (factorX+factorY); return Math.sqrt(factor2); } /** * Returns the square of a normalization factor for the supplied kernel. * The kernel can be normalized by invoking {@link #divide(KernelJAI,double)} * with the square root of this value. * * @param kernel The kernel for which to compute normalization factor. * @param type Any combinaison of {@link #HORIZONTAL} and {@link #VERTICAL}. * @return The square of a normalization factor that could be applied on the kernel. */ private static double getNormalizationFactorSquared(final KernelJAI kernel, final int type) { double sumH = 0; double sumV = 0; final int width = kernel.getWidth(); final int height = kernel.getHeight(); /* * Tests the kernel with a horizontal gradient [ -1 0 1 ] * of 1/pixel. For example, we get sumH=8 with [ -2 0 2 ] * the horizontal Sobel kernel show on right: [ -1 0 1 ] */ if ((type & HORIZONTAL) != 0) { int value = kernel.getYOrigin(); for (int y=height; --y>=0;) { for (int x=width; --x>=0;) { sumH += value * kernel.getElement(x,y); } value--; } } /* * Tests the kernel with a vertical gradient of [ -1 -2 -1 ] * 1/pixel. For example, we get sumV=8 with the [ 0 0 0 ] * vertical Sobel kernel show on right: [ 1 2 1 ] */ if ((type & VERTICAL) != 0) { int value = kernel.getXOrigin(); for (int x=width; --x>=0;) { for (int y=height; --y>=0;) { sumV += value * kernel.getElement(x,y); } value--; } } return (sumH*sumH) + (sumV*sumV); } /** * Returns the normalization factor for the supplied kernel. The kernel can * be normalized by invoking {@link #divide(KernelJAI,double)} with this factor. * * @param mask1 The first kernel for which to compute a normalization factor. * @param mask2 The second kernel for which to compute a normalization factor. * @return The normalization factor that could be applied on both kernels. * * @todo When the masks are symetric (e.g. Sobel, Prewitt (or Smoothed), isotropic, etc.), * then this algorithm matches the "normalization factor" times "spatial factor" * provided by * * J.J. Simpson (1990), "On the accurate detection and enhancement of oceanic features * observed in satellite data" in Remote sensing environment, 33:17-33. * * However, for non-symetric masks (e.g. Kirsch), then a difference is found. * We should provides a way to disable normalization when the user did it himself * according some other rules than the one used here. */ private static double getNormalizationFactor(final KernelJAI mask1, final KernelJAI mask2) { double factor; factor = getNormalizationFactorSquared(mask1, HORIZONTAL|VERTICAL); factor += getNormalizationFactorSquared(mask2, HORIZONTAL|VERTICAL); factor = Math.sqrt(factor/2); return factor; } /** * Divides a kernel by some number. * * @param kernel The kernel to divide. * @param denominator The factor to divide by. * @return The resulting kernel. */ private static KernelJAI divide(KernelJAI kernel, final double denominator) { if (denominator != 1) { final float[] data = kernel.getKernelData(); final int length = data.length; for (int i=0; i 0)) { // Do not transform if factor is 0 or NaN. factor = 1; } /* * Computes a scale factor taking in account the transformation from * grid to coordinate system. This scale will convert gradient from * 1 unit/pixel to 1 unit/meters or 1 unit/degrees, depending the * coordinate reference systems axis unit. */ double scaleMask1 = 1; double scaleMask2 = 1; if (sources.length != 0) { final MathTransform2D mtr; mtr = ((GridGeometry2D) sources[0].getGridGeometry()).getGridToCRS2D(); if (mtr instanceof AffineTransform) { final AffineTransform tr = (AffineTransform) mtr; final double scaleX = XAffineTransform.getScaleX0(tr); final double scaleY = XAffineTransform.getScaleY0(tr); scaleMask1 = getScaleFactor(mask1, scaleX, scaleY); scaleMask2 = getScaleFactor(mask2, scaleX, scaleY); if (!(scaleMask1>0 && scaleMask2>0)) { // Do not rescale if scale is 0 or NaN. scaleMask1 = 1; scaleMask2 = 1; } if (DEBUG) { System.out.print("factor= "); System.out.println(factor); System.out.print("scaleMask1= "); System.out.println(scaleMask1); System.out.print("scaleMask1= "); System.out.println(scaleMask2); } } } block.setParameter("Mask1", divide(mask1, factor*scaleMask1)); block.setParameter("Mask2", divide(mask2, factor*scaleMask2)); } return super.deriveGridCoverage(sources, parameters); } /** * Derives the quantitative category for a band in the destination image. * This implementation computes the expected gradient range from the two * masks and the value range in the source grid coverage. */ protected Category deriveCategory(final Category[] categories, final Parameters parameters) { NumberRange range = null; Category category = categories[0]; final NumberRange samples = category.geophysics(false).getRange(); final boolean isGeophysics = (category == category.geophysics(true)); /* * Computes a default range of output values one from the normalized kernels. * The normalization has been done by 'deriveGridCoverage' before this method * is invoked. The algorithm is as below: * * - Computes the value produced by the kernels for an artificial gradient of 1 unit/pixel. * - Transforms into a lower gradient of 1 unit/(kernel size). * - Transforms into a gradient of (maximal range)/(kernel size). * - Applies an arbitrary correction factor for more convenient range in most cases. */ final ParameterList block = parameters.parameters; final KernelJAI mask1 = (KernelJAI) block.getObjectParameter("Mask1"); final KernelJAI mask2 = (KernelJAI) block.getObjectParameter("Mask2"); final double size = (mask1.getWidth() + mask1.getHeight() + mask2.getWidth() + mask2.getHeight()) / 4.0; double factor = getNormalizationFactor(mask1, mask2) / (size-1); if (factor>0 && !Double.isInfinite(factor)) { range = category.geophysics(true).getRange(); final double minimum = range.getMinimum(); final double maximum = range.getMaximum(); factor *= (maximum - minimum) * DEFAULT_RANGE_SCALE; range = NumberRange.create(0, factor); } if (range != null) { category = new Category(category.getName(), DEFAULT_COLOR_PALETTE, samples, range); return category.geophysics(isGeophysics); } return super.deriveCategory(categories, parameters); } /** * Derives the unit of data for a band in the destination image. * This method compute {@code sample}/{@code axis} where: * * */ protected Unit deriveUnit(final Unit[] units, final Parameters parameters) { final CoordinateSystem cs = parameters.crs.getCoordinateSystem(); if (!DEBUG && units.length==1 && units[0]!=null) { final Unit spatialUnit = cs.getAxis(0).getUnit(); for (int i=Math.min(cs.getDimension(), 2); --i>=0;) { if (!spatialUnit.equals(cs.getAxis(i).getUnit())) { return super.deriveUnit(units, parameters); } } try { return units[0].divide(spatialUnit); } catch (RuntimeException exception) { // Can't compute units... We will compute image data // anyway, but the result will have no know unit. // TODO: Catch a more specific exception. } } return super.deriveUnit(units, parameters); } }