/* JAI-Ext - OpenSource Java Advanced Image Extensions Library
* http://www.geo-solutions.it/
* Copyright 2014 GeoSolutions
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.geosolutions.jaiext.bandmerge;
import static org.junit.Assert.*;
import it.geosolutions.jaiext.iterators.RandomIterFactory;
import it.geosolutions.jaiext.range.Range;
import it.geosolutions.jaiext.range.RangeFactory;
import it.geosolutions.jaiext.testclasses.TestBase;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.util.ArrayList;
import java.util.List;
import javax.media.jai.ImageLayout;
import javax.media.jai.JAI;
import javax.media.jai.ROI;
import javax.media.jai.ROIShape;
import javax.media.jai.RenderedOp;
import javax.media.jai.TiledImage;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.operator.TranslateDescriptor;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import com.sun.media.jai.util.ImageUtil;
/**
* This test class checks if the BandMergeOpImage is able to merge various multibanded images into a single multibanded image. The test is made by
* taking 4 images for each JAI allowed data type, and then passing them to the BandMerge operator. The test ensures that the final image band number
* is equal to the total sources band number and each sample value is equal to that of the related source image band. If No Data are present, then the
* test check if the every No Data value is set to the "destination No Data value".
*
* @author geosolutions
*
*/
public class BandMergeTest extends TestBase {
/** Constant indicating the image height */
private final static int IMAGE_HEIGHT = 128;
/** Constant indicating the image width */
private final static int IMAGE_WIDTH = 128;
/** Constant indicating the final number of bands for each merged image */
private final static int BAND_NUMBER = 4;
/** Tolerance value used for double comparison */
private final static double TOLERANCE = 0.1d;
private static final int ROI_WIDTH = 40;
private static final int ROI_HEIGHT = 40;
/** RenderedImage array used for performing the tests */
private static RenderedImage[][] images;
/** No Data Range array for Byte images */
private static Range[] noDataByte;
/** No Data Range array for Unsigned Short images */
private static Range[] noDataUShort;
/** No Data Range array for Short images */
private static Range[] noDataShort;
/** No Data Range array for Integer images */
private static Range[] noDataInt;
/** No Data Range array for Float images */
private static Range[] noDataFloat;
/** No Data Range array for Double images */
private static Range[] noDataDouble;
/** Double value used for destination No Data */
private static double destNoData;
/** ROI to use */
private static ROI roiData;
@BeforeClass
public static void initialSetup() {
// This parameter is set to true so that the image are filled with data
IMAGE_FILLER = true;
// Creation of all the image array
// The 6 inner arrays contain 4 images to merge
// Each inner array is associated with a different data type
images = new RenderedImage[6][4];
byte noDataB = 50;
short noDataS = 50;
int noDataI = 50;
float noDataF = 50;
double noDataD = 50;
for (int band = 0; band < BAND_NUMBER; band++) {
images[DataBuffer.TYPE_BYTE][band] = createTestImage(DataBuffer.TYPE_BYTE, IMAGE_WIDTH,
IMAGE_HEIGHT, noDataB, false, 1);
images[DataBuffer.TYPE_USHORT][band] = createTestImage(DataBuffer.TYPE_USHORT,
IMAGE_WIDTH, IMAGE_HEIGHT, noDataS, false, 1);
images[DataBuffer.TYPE_SHORT][band] = createTestImage(DataBuffer.TYPE_SHORT,
IMAGE_WIDTH, IMAGE_HEIGHT, noDataS, false, 1);
images[DataBuffer.TYPE_INT][band] = createTestImage(DataBuffer.TYPE_INT, IMAGE_WIDTH,
IMAGE_HEIGHT, noDataI, false, 1);
images[DataBuffer.TYPE_FLOAT][band] = createTestImage(DataBuffer.TYPE_FLOAT,
IMAGE_WIDTH, IMAGE_HEIGHT, noDataF, false, 1);
images[DataBuffer.TYPE_DOUBLE][band] = createTestImage(DataBuffer.TYPE_DOUBLE,
IMAGE_WIDTH, IMAGE_HEIGHT, noDataD, false, 1);
}
IMAGE_FILLER = false;
// No Data Ranges
boolean minIncluded = true;
boolean maxIncluded = true;
noDataByte = new Range[] { RangeFactory.create(noDataB, minIncluded, noDataB, maxIncluded) };
noDataUShort = new Range[] { RangeFactory.createU(noDataS, minIncluded, noDataS,
maxIncluded) };
noDataShort = new Range[] { RangeFactory.create(noDataS, minIncluded, noDataS, maxIncluded) };
noDataInt = new Range[] { RangeFactory.create(noDataI, minIncluded, noDataI, maxIncluded) };
noDataFloat = new Range[] { RangeFactory.create(noDataF, minIncluded, noDataF, maxIncluded,
true) };
noDataDouble = new Range[] { RangeFactory.create(noDataD, minIncluded, noDataD,
maxIncluded, true) };
// Destination No Data
destNoData = 100;
// ROI
roiData = new ROIShape(new Rectangle(0, 0, ROI_WIDTH, ROI_HEIGHT));
}
@Test
public void testBandMerge() {
// This test checks the BandMerge operation on all the possible data types without No Data
boolean noDataUsed = false;
boolean roiUsed = false;
testBandMerge(images[0], noDataUsed, roiUsed);
testBandMerge(images[1], noDataUsed, roiUsed);
testBandMerge(images[2], noDataUsed, roiUsed);
testBandMerge(images[3], noDataUsed, roiUsed);
testBandMerge(images[4], noDataUsed, roiUsed);
testBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testBandMergeNoData() {
// This test checks the BandMerge operation on all the possible data types with No Data
boolean noDataUsed = true;
boolean roiUsed = false;
testBandMerge(images[0], noDataUsed, roiUsed);
testBandMerge(images[1], noDataUsed, roiUsed);
testBandMerge(images[2], noDataUsed, roiUsed);
testBandMerge(images[3], noDataUsed, roiUsed);
testBandMerge(images[4], noDataUsed, roiUsed);
testBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testBandMergeROI() {
// This test checks the BandMerge operation on all the possible data types with ROI
boolean noDataUsed = false;
boolean roiUsed = true;
testBandMerge(images[0], noDataUsed, roiUsed);
testBandMerge(images[1], noDataUsed, roiUsed);
testBandMerge(images[2], noDataUsed, roiUsed);
testBandMerge(images[3], noDataUsed, roiUsed);
testBandMerge(images[4], noDataUsed, roiUsed);
testBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testBandMergeNoDataROI() {
// This test checks the BandMerge operation on all the possible data types with No Data and ROI
boolean noDataUsed = true;
boolean roiUsed = true;
testBandMerge(images[0], noDataUsed, roiUsed);
testBandMerge(images[1], noDataUsed, roiUsed);
testBandMerge(images[2], noDataUsed, roiUsed);
testBandMerge(images[3], noDataUsed, roiUsed);
testBandMerge(images[4], noDataUsed, roiUsed);
testBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testExtendedBandMerge() {
// This test checks the BandMerge operation on all the possible data types without No Data and ROI
// Also it tests if the use of AffineTransformations is correct
boolean noDataUsed = false;
boolean roiUsed = false;
testExtendedBandMerge(images[0], noDataUsed, roiUsed);
testExtendedBandMerge(images[1], noDataUsed, roiUsed);
testExtendedBandMerge(images[2], noDataUsed, roiUsed);
testExtendedBandMerge(images[3], noDataUsed, roiUsed);
testExtendedBandMerge(images[4], noDataUsed, roiUsed);
testExtendedBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testExtendedBandMergeNoData() {
// This test checks the BandMerge operation on all the possible data types with No Data
// Also it tests if the use of AffineTransformations is correct
boolean noDataUsed = true;
boolean roiUsed = false;
testExtendedBandMerge(images[0], noDataUsed, roiUsed);
testExtendedBandMerge(images[1], noDataUsed, roiUsed);
testExtendedBandMerge(images[2], noDataUsed, roiUsed);
testExtendedBandMerge(images[3], noDataUsed, roiUsed);
testExtendedBandMerge(images[4], noDataUsed, roiUsed);
testExtendedBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testExtendedBandMergeROI() {
// This test checks the BandMerge operation on all the possible data types with ROI
// Also it tests if the use of AffineTransformations is correct
boolean noDataUsed = false;
boolean roiUsed = true;
testExtendedBandMerge(images[0], noDataUsed, roiUsed);
testExtendedBandMerge(images[1], noDataUsed, roiUsed);
testExtendedBandMerge(images[2], noDataUsed, roiUsed);
testExtendedBandMerge(images[3], noDataUsed, roiUsed);
testExtendedBandMerge(images[4], noDataUsed, roiUsed);
testExtendedBandMerge(images[5], noDataUsed, roiUsed);
}
@Test
public void testExtendedBandMergeNoDataROI() {
// This test checks the BandMerge operation on all the possible data types with No Data and ROI
// Also it tests if the use of AffineTransformations is correct
boolean noDataUsed = true;
boolean roiUsed = true;
testExtendedBandMerge(images[0], noDataUsed, roiUsed);
testExtendedBandMerge(images[1], noDataUsed, roiUsed);
testExtendedBandMerge(images[2], noDataUsed, roiUsed);
testExtendedBandMerge(images[3], noDataUsed, roiUsed);
testExtendedBandMerge(images[4], noDataUsed, roiUsed);
testExtendedBandMerge(images[5], noDataUsed, roiUsed);
}
@AfterClass
public static void disposal() {
// Disposal of the images
for (int band = 0; band < BAND_NUMBER; band++) {
((TiledImage) images[DataBuffer.TYPE_BYTE][band]).dispose();
((TiledImage) images[DataBuffer.TYPE_USHORT][band]).dispose();
((TiledImage) images[DataBuffer.TYPE_SHORT][band]).dispose();
((TiledImage) images[DataBuffer.TYPE_INT][band]).dispose();
((TiledImage) images[DataBuffer.TYPE_FLOAT][band]).dispose();
((TiledImage) images[DataBuffer.TYPE_DOUBLE][band]).dispose();
}
}
private void testBandMerge(RenderedImage[] sources, boolean noDataUsed, boolean roiUsed) {
// Optional No Data Range used
Range[] noData;
// Source image data type
int dataType = sources[0].getSampleModel().getDataType();
// If no Data are present, the No Data Range associated is used
if (noDataUsed) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
noData = noDataByte;
break;
case DataBuffer.TYPE_USHORT:
noData = noDataUShort;
break;
case DataBuffer.TYPE_SHORT:
noData = noDataShort;
break;
case DataBuffer.TYPE_INT:
noData = noDataInt;
break;
case DataBuffer.TYPE_FLOAT:
noData = noDataFloat;
break;
case DataBuffer.TYPE_DOUBLE:
noData = noDataDouble;
break;
default:
throw new IllegalArgumentException("Wrong data type");
}
} else {
noData = null;
}
// ROI to use
ROI roi = null;
if (roiUsed) {
roi = roiData;
}
// BandMerge operation
RenderedOp merged = BandMergeDescriptor
.create(noData, destNoData, false, null, null, roi, sources);
// Check if the bands number is the same
assertEquals(BAND_NUMBER, merged.getNumBands());
// Ensure the final ColorModel exists and has not an alpha band
if (merged.getSampleModel().getDataType() != DataBuffer.TYPE_SHORT) {
assertNotNull(merged.getColorModel());
assertTrue(!merged.getColorModel().hasAlpha());
}
// Upper-Left tile indexes
int minTileX = merged.getMinTileX();
int minTileY = merged.getMinTileY();
// Raster object
Raster upperLeftTile = merged.getTile(minTileX, minTileY);
// Tile bounds
int minX = upperLeftTile.getMinX();
int minY = upperLeftTile.getMinY();
int maxX = upperLeftTile.getWidth() + minX;
int maxY = upperLeftTile.getHeight() + minY;
// Cycle on all the tile Bands
for (int b = 0; b < BAND_NUMBER; b++) {
// Selection of the source raster associated with the band
Raster bandRaster = sources[b].getTile(minTileX, minTileY);
// Cycle on the y-axis
for (int x = minX; x < maxX; x++) {
// Cycle on the x-axis
for (int y = minY; y < maxY; y++) {
// Calculated value
double value = upperLeftTile.getSampleDouble(x, y, b);
// Old band value
double valueOld = bandRaster.getSampleDouble(x, y, 0);
// ROI CHECK
boolean contained = true;
if (roiUsed) {
if (!roi.contains(x, y)) {
contained = false;
// Comparison if the final value is not inside a ROI
assertEquals(value, destNoData, TOLERANCE);
}
}
if (contained) {
// If no Data are present, no data check is performed
if (noDataUsed) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
byte sampleB = ImageUtil.clampRoundByte(value);
byte sampleBOld = ImageUtil.clampRoundByte(valueOld);
if (noData[0].contains(sampleBOld)) {
assertEquals(sampleB, destNoData, TOLERANCE);
} else {
assertEquals(sampleB, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_USHORT:
short sampleUS = ImageUtil.clampRoundUShort(value);
short sampleUSOld = ImageUtil.clampRoundUShort(valueOld);
if (noData[0].contains(sampleUSOld)) {
assertEquals(sampleUS, destNoData, TOLERANCE);
} else {
assertEquals(sampleUS, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_SHORT:
short sampleS = ImageUtil.clampRoundShort(value);
short sampleSOld = ImageUtil.clampRoundShort(valueOld);
if (noData[0].contains(sampleSOld)) {
assertEquals(sampleS, destNoData, TOLERANCE);
} else {
assertEquals(sampleS, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_INT:
int sampleI = ImageUtil.clampRoundInt(value);
int sampleIOld = ImageUtil.clampRoundInt(valueOld);
if (noData[0].contains(sampleIOld)) {
assertEquals(sampleI, destNoData, TOLERANCE);
} else {
assertEquals(sampleI, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_FLOAT:
float sampleF = ImageUtil.clampFloat(value);
float sampleFOld = ImageUtil.clampFloat(valueOld);
if (noData[0].contains(sampleFOld)) {
assertEquals(sampleF, destNoData, TOLERANCE);
} else {
assertEquals(sampleF, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_DOUBLE:
if (noData[0].contains(valueOld)) {
assertEquals(value, destNoData, TOLERANCE);
} else {
assertEquals(value, valueOld, TOLERANCE);
}
break;
default:
throw new IllegalArgumentException("Wrong data type");
}
} else {
// Else a simple value comparison is done
assertEquals(value, valueOld, TOLERANCE);
}
}
}
}
}
// Disposal of the output image
merged.dispose();
}
// This method is similar to the testBandMerge method but it tests the ExtendedBandMergeOpImage class
private void testExtendedBandMerge(RenderedImage[] sources, boolean noDataUsed, boolean roiUsed) {
// Optional No Data Range used
Range[] noData;
// Source image data type
int dataType = sources[0].getSampleModel().getDataType();
// If no Data are present, the No Data Range associated is used
if (noDataUsed) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
noData = noDataByte;
break;
case DataBuffer.TYPE_USHORT:
noData = noDataUShort;
break;
case DataBuffer.TYPE_SHORT:
noData = noDataShort;
break;
case DataBuffer.TYPE_INT:
noData = noDataInt;
break;
case DataBuffer.TYPE_FLOAT:
noData = noDataFloat;
break;
case DataBuffer.TYPE_DOUBLE:
noData = noDataDouble;
break;
default:
throw new IllegalArgumentException("Wrong data type");
}
} else {
noData = null;
}
// ROI to use
ROI roi = null;
if (roiUsed) {
roi = roiData;
}
// New array ofr the transformed source images
RenderedOp[] translated = new RenderedOp[sources.length];
List<AffineTransform> transform = new ArrayList<AffineTransform>();
for (int i = 0; i < sources.length; i++) {
// Translation coefficients
int xTrans = (int) (Math.random() * 10);
int yTrans = (int) (Math.random() * 10);
// Translation operation
AffineTransform tr = AffineTransform.getTranslateInstance(xTrans, yTrans);
// Addition to the transformations list
transform.add(tr);
// Translation of the image
translated[i] = TranslateDescriptor.create(sources[i], (float) xTrans, (float) yTrans,
null, null);
}
// Definition of the final image dimensions
ImageLayout layout = new ImageLayout();
layout.setMinX(sources[0].getMinX());
layout.setMinY(sources[0].getMinY());
layout.setWidth(sources[0].getWidth());
layout.setHeight(sources[0].getHeight());
RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout);
// BandMerge operation
RenderedOp merged = BandMergeDescriptor.create(noData, destNoData, false, hints, transform, roi,
translated);
Assert.assertNotNull(merged.getTiles());
// Check if the bands number is the same
assertEquals(BAND_NUMBER, merged.getNumBands());
// Upper-Left tile indexes
int minTileX = merged.getMinTileX();
int minTileY = merged.getMinTileY();
// Raster object
Raster upperLeftTile = merged.getTile(minTileX, minTileY);
// Tile bounds
int minX = upperLeftTile.getMinX();
int minY = upperLeftTile.getMinY();
int maxX = upperLeftTile.getWidth() + minX;
int maxY = upperLeftTile.getHeight() + minY;
// Source corners
final int dstMinX = merged.getMinX();
final int dstMinY = merged.getMinY();
final int dstMaxX = merged.getMaxX();
final int dstMaxY = merged.getMaxY();
Point2D ptDst = new Point2D.Double(0, 0);
Point2D ptSrc = new Point2D.Double(0, 0);
// Cycle on all the tile Bands
for (int b = 0; b < BAND_NUMBER; b++) {
RandomIter iter = RandomIterFactory.create(translated[b], null, true, true);
// Source corners
final int srcMinX = translated[b].getMinX();
final int srcMinY = translated[b].getMinY();
final int srcMaxX = translated[b].getMaxX();
final int srcMaxY = translated[b].getMaxY();
// Cycle on the y-axis
for (int x = minX; x < maxX; x++) {
// Cycle on the x-axis
for (int y = minY; y < maxY; y++) {
// Calculated value
double value = upperLeftTile.getSampleDouble(x, y, b);
// If the tile pixels are outside the image bounds, then no data is set.
if (x < dstMinX || x >= dstMaxX || y < dstMinY || y >= dstMaxY) {
value = destNoData;
}
// Set the x,y destination pixel location
ptDst.setLocation(x, y);
// Map destination pixel to source pixel
transform.get(b).transform(ptDst, ptSrc);
// Source pixel indexes
int srcX = round(ptSrc.getX());
int srcY = round(ptSrc.getY());
double valueOld = destNoData;
// Check if the pixel is inside the source bounds
if (!(srcX < srcMinX || srcX >= srcMaxX || srcY < srcMinY || srcY >= srcMaxY)) {
// Old band value
valueOld = iter.getSampleDouble(srcX, srcY, 0);
}
// ROI CHECK
boolean contained = true;
if (roiUsed) {
if (!roi.contains(x, y)) {
contained = false;
// Comparison if the final value is not inside a ROI
assertEquals(value, destNoData, TOLERANCE);
}
}
if (contained) {
// If no Data are present, no data check is performed
if (noDataUsed) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
byte sampleB = ImageUtil.clampRoundByte(value);
byte sampleBOld = ImageUtil.clampRoundByte(valueOld);
if (noData[0].contains(sampleBOld)) {
assertEquals(sampleB, destNoData, TOLERANCE);
} else {
assertEquals(sampleB, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_USHORT:
short sampleUS = ImageUtil.clampRoundUShort(value);
short sampleUSOld = ImageUtil.clampRoundUShort(valueOld);
if (noData[0].contains(sampleUSOld)) {
assertEquals(sampleUS, destNoData, TOLERANCE);
} else {
assertEquals(sampleUS, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_SHORT:
short sampleS = ImageUtil.clampRoundShort(value);
short sampleSOld = ImageUtil.clampRoundShort(valueOld);
if (noData[0].contains(sampleSOld)) {
assertEquals(sampleS, destNoData, TOLERANCE);
} else {
assertEquals(sampleS, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_INT:
int sampleI = ImageUtil.clampRoundInt(value);
int sampleIOld = ImageUtil.clampRoundInt(valueOld);
if (noData[0].contains(sampleIOld)) {
assertEquals(sampleI, destNoData, TOLERANCE);
} else {
assertEquals(sampleI, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_FLOAT:
float sampleF = ImageUtil.clampFloat(value);
float sampleFOld = ImageUtil.clampFloat(valueOld);
if (noData[0].contains(sampleFOld)) {
assertEquals(sampleF, destNoData, TOLERANCE);
} else {
assertEquals(sampleF, valueOld, TOLERANCE);
}
break;
case DataBuffer.TYPE_DOUBLE:
if (noData[0].contains(valueOld)) {
assertEquals(value, destNoData, TOLERANCE);
} else {
assertEquals(value, valueOld, TOLERANCE);
}
break;
default:
throw new IllegalArgumentException("Wrong data type");
}
} else {
// Else a simple value comparison is done
assertEquals(value, valueOld, TOLERANCE);
}
}
}
}
}
// Disposal of the output image
merged.dispose();
}
/** Returns the "round" value of a float. */
private static int round(double f) {
return f >= 0 ? (int) (f + 0.5F) : (int) (f - 0.5F);
}
}