/* 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.zonal; import static org.junit.Assert.assertEquals; import java.awt.Rectangle; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import javax.media.jai.iterator.RandomIter; import it.geosolutions.jaiext.iterators.RandomIterFactory; import it.geosolutions.jaiext.range.Range; import it.geosolutions.jaiext.range.RangeFactory; import it.geosolutions.jaiext.stats.Statistics; import it.geosolutions.jaiext.stats.Statistics.StatsType; import it.geosolutions.jaiext.testclasses.TestBase; import org.junit.BeforeClass; import org.junit.Test; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.index.strtree.STRtree; /** * This test class is used for evaluating the functionalities of the ZonalStats operation. This operation consists of calculating different statistics * on a list of geometries passed to the image operator. The possible operations to do are: * <ul> * <li>Mean</li> * <li>Sum</li> * <li>Max</li> * <li>Min</li> * <li>Extrema</li> * <li>Variance</li> * <li>Standard Deviation</li> * <li>Histogram</li> * <li>Mode</li> * <li>Median</li> * </ul> * An additional Classifier image can be used for dividing the statistics between the classes defined by it. The ZonalStatsOpImage is tested on 6 * different images each one with a different data type (except Long data type). The operation is tested with and without the presence of NoData, with * and without the presence of ROI. The classifier can be used by setting to true the JVM parameter JAI.Ext.Classifier. All the results are compared * with the previously calculated statistics for checking the correctness of the calculations. */ public class ZonalStatsTest extends TestBase { /** Tolerance value used for comparison between double */ private final static double TOLERANCE = 0.1d; /** Boolean indicating if a classifier image must be used */ private static final boolean CLASSIFIER = Boolean.getBoolean("JAI.Ext.Classifier"); /** Statistics operation to execute */ private static StatsType[] stats; /** Source image arrays */ private static RenderedImage[] sourceIMG; /** List of geometries */ private static List<ROI> roiList; /** Optional classifier image */ private static RenderedImage classifier; /** Spatial index for fast searching the geometries associated with a selected pixel */ private static STRtree[][] spatial; /** Array containing the list of all the zones */ private static List<ZoneGeometry>[][] zonesLists; /** Union of all the input geometries bounds */ private static Rectangle union; /** Selected bands to perform statistical calculations */ private static int[] bands; /** NoData value for Byte images */ private static byte noDataB; /** NoData value for Unsigned Short images */ private static short noDataU; /** NoData value for Short images */ private static short noDataS; /** NoData value for Integer images */ private static int noDataI; /** NoData value for Float images */ private static float noDataF; /** NoData value for Double images */ private static double noDataD; /** NoData Range for Byte data */ private static Range noDataByte; /** NoData Range for Unsigned Short data */ private static Range noDataUShort; /** NoData Range for Short data */ private static Range noDataShort; /** NoData Range for Integer data */ private static Range noDataInt; /** NoData Range for Float data */ private static Range noDataFloat; /** NoData Range for Double data */ private static Range noDataDouble; /** Array indicating the minimum bounds for each band */ private static double[] minBound; /** Array indicating the maximum bounds for each band */ private static double[] maxBound; /** Array indicating the number of bins for each band */ private static int[] numBins; private static List<Range>[] rangeList; private static ROI roiObject; @BeforeClass public static void initialSetup() { // Definition of the Histogram, Mode and Median parameters minBound = new double[] { -3, -3, -3 }; maxBound = new double[] { 3, 3, 3 }; numBins = new int[] { 4, 4, 4 }; // Statistics types stats = new StatsType[] { StatsType.MEAN, StatsType.SUM, StatsType.MAX, StatsType.MIN, StatsType.EXTREMA, StatsType.VARIANCE, StatsType.DEV_STD, StatsType.HISTOGRAM, StatsType.MODE, StatsType.MEDIAN }; roiList = new ArrayList<ROI>(); // Geometry parameters int initial_value = 10; int final_value = 30; int dimension = (final_value - initial_value); int interval = dimension / 10; // ROI LIST for (int i = 0; i < 10; i++) { ROI roiGeom = new ROIShape(new Rectangle(initial_value, initial_value, dimension, dimension)); roiList.add(roiGeom); // Translation of the geometry with overlap initial_value += interval; } // Creation of the ROI // ROI creation Rectangle roiBounds = new Rectangle(5, 5, DEFAULT_WIDTH / 4, DEFAULT_HEIGHT / 4); roiObject = new ROIShape(roiBounds); rangeList = new ArrayList[6]; rangeList[0] = new ArrayList<Range>(1); rangeList[1] = new ArrayList<Range>(1); rangeList[2] = new ArrayList<Range>(1); rangeList[3] = new ArrayList<Range>(1); rangeList[4] = new ArrayList<Range>(1); rangeList[5] = new ArrayList<Range>(1); // Data Range creation rangeList[0].add(RangeFactory.create((byte) 0, true, (byte) 100, true)); rangeList[1].add(RangeFactory.createU((short) 0.0, true, (short) 100, true)); rangeList[2].add(RangeFactory.create((short) -1, true, (short) 100, true)); rangeList[3].add(RangeFactory.create(-1, true, 100, true)); rangeList[4].add(RangeFactory.create(-1f, true, 100f, true, false)); rangeList[5].add(RangeFactory.create(-1d, true, 100d, true, false)); // Band array creation bands = new int[] { 0 }; // Spatial indexing // Creation of the spatial indexes and of the final List results spatial = new STRtree[6][4]; zonesLists = new ArrayList[6][4]; for (int i = 0; i < 6; i++) { // Spatial Indexes spatial[i][0] = new STRtree(); spatial[i][1] = new STRtree(); spatial[i][2] = new STRtree(); spatial[i][3] = new STRtree(); // Zones Lists zonesLists[i][0] = new ArrayList<ZoneGeometry>(); zonesLists[i][1] = new ArrayList<ZoneGeometry>(); zonesLists[i][2] = new ArrayList<ZoneGeometry>(); zonesLists[i][3] = new ArrayList<ZoneGeometry>(); } // Bounds Union union = new Rectangle(roiList.get(0).getBounds()); // Insertion of the zones to the spatial index and union of the bounds for every ROI/Zone object for (ROI roi : roiList) { // Spatial index creation Rectangle rect = roi.getBounds(); double minX = rect.getMinX(); double maxX = rect.getMaxX(); double minY = rect.getMinY(); double maxY = rect.getMaxY(); Envelope env = new Envelope(minX, maxX, minY, maxY); // Union union = union.union(rect); // Addition to the geometries list for (int i = 0; i < 6; i++) { for (int z = 0; z < 4; z++) { // Creation of a new ZoneGeometry ZoneGeometry geom = new ZoneGeometry(roi, rangeList[i], bands, stats, CLASSIFIER, minBound, maxBound, numBins); spatial[i][z].insert(env, geom); zonesLists[i][z].add(geom); } } } // Classifier classifier = createTestImage(DataBuffer.TYPE_INT, DEFAULT_WIDTH, DEFAULT_HEIGHT, 2, false); // Classifier bound Rectangle rectClass = new Rectangle(classifier.getMinX(), classifier.getMinY(), classifier.getWidth(), classifier.getHeight()); // Iterator on the classifier image RandomIter iterator = RandomIterFactory.create(classifier, rectClass, true, true); // No Data values noDataB = 50; noDataU = 50; noDataS = 50; noDataI = 50; noDataF = 50; noDataD = 50; // Range parameters boolean minIncluded = true; boolean maxIncluded = true; boolean nanIncluded = true; noDataByte = RangeFactory.create(noDataB, minIncluded, noDataB, maxIncluded); noDataUShort = RangeFactory.createU(noDataU, minIncluded, noDataU, maxIncluded); noDataShort = RangeFactory.create(noDataS, minIncluded, noDataS, maxIncluded); noDataInt = RangeFactory.create(noDataI, minIncluded, noDataI, maxIncluded); noDataFloat = RangeFactory.create(noDataF, minIncluded, noDataF, maxIncluded, nanIncluded); noDataDouble = RangeFactory.create(noDataD, minIncluded, noDataD, maxIncluded, nanIncluded); // Image creations sourceIMG = new RenderedImage[6]; // Creation of the source images IMAGE_FILLER = false; sourceIMG[0] = createTestImage(DataBuffer.TYPE_BYTE, DEFAULT_WIDTH, DEFAULT_HEIGHT, noDataB, false); sourceIMG[1] = createTestImage(DataBuffer.TYPE_USHORT, DEFAULT_WIDTH, DEFAULT_HEIGHT, noDataU, false); sourceIMG[2] = createTestImage(DataBuffer.TYPE_SHORT, DEFAULT_WIDTH, DEFAULT_HEIGHT, noDataS, false); sourceIMG[3] = createTestImage(DataBuffer.TYPE_INT, DEFAULT_WIDTH, DEFAULT_HEIGHT, noDataI, false); sourceIMG[4] = createTestImage(DataBuffer.TYPE_FLOAT, DEFAULT_WIDTH, DEFAULT_HEIGHT, noDataF, false); sourceIMG[5] = createTestImage(DataBuffer.TYPE_DOUBLE, DEFAULT_WIDTH, DEFAULT_HEIGHT, noDataD, false); // Image Tiles positions int minTileX = sourceIMG[0].getMinTileX(); int minTileY = sourceIMG[0].getMinTileY(); int maxTileX = minTileX + sourceIMG[0].getNumXTiles(); int maxTileY = minTileY + sourceIMG[0].getNumYTiles(); // STATISTICAL CALCULATION // Cycle on all pixels of the 6 images for (int i = minTileX; i < maxTileX; i++) { for (int j = minTileY; j < maxTileY; j++) { // Selection of a Raster Raster arrayRas = sourceIMG[0].getTile(i, j); // Raster pixels positions int minX = arrayRas.getMinX(); int minY = arrayRas.getMinY(); int maxX = minX + arrayRas.getWidth(); int maxY = minY + arrayRas.getHeight(); // Cycle on the Raster pixels for (int x = minX; x < maxX; x++) { for (int y = minY; y < maxY; y++) { for (int z = 0; z < 4; z++) { // Check if the selected pixel is inside the union of all the geometries if (union.contains(x, y)) { byte value = (byte) arrayRas.getSample(x, y, 0); // pixel coordinate creation for spatial indexing Coordinate p = new Coordinate(x, y); // Creation of an Envelop containing the pixel coordinates Envelope searchEnv = new Envelope(p); // Query on the geometry list for (int v = 0; v < 6; v++) { List<ZoneGeometry> geomList = spatial[v][z].query(searchEnv); // classId classifier initial value int classId = 0; // If the classifier is present then the classId value is taken if (CLASSIFIER) { // Selection of the classId point classId = iterator.getSample(x, y, 0); } // Cycle on all the geometries found for (ZoneGeometry zoneGeo : geomList) { ROI geometry = zoneGeo.getROI(); // if every geometry really contains the selected point if (geometry.contains(x, y)) { // Cycle for the 2 cases: with and without NoData switch (z) { case 0: zoneGeo.add(value, 0, classId, rangeList[v].get(0)); break; case 1: if (!noDataByte.contains(value)) { zoneGeo.add(value, 0, classId, rangeList[v].get(0)); } break; case 2: if (roiObject.contains(x, y)) { zoneGeo.add(value, 0, classId, rangeList[v].get(0)); } break; case 3: if (!noDataByte.contains(value) && roiObject.contains(x, y)) { zoneGeo.add(value, 0, classId, rangeList[v].get(0)); } break; } } } } } } } } } } } @Test public void testNoNoDataNoROI() { // This test calculates zonal statistics without NoData boolean noDataRangeUsed = false; boolean roiUsed = false; boolean useROIAccessor = false; testZonalStats(sourceIMG[0], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[0]); testZonalStats(sourceIMG[1], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[1]); testZonalStats(sourceIMG[2], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[2]); testZonalStats(sourceIMG[3], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[3]); testZonalStats(sourceIMG[4], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[4]); testZonalStats(sourceIMG[5], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[5]); } @Test public void testOnlyROI() { // This test calculates zonal statistics with NoData and without ROI boolean noDataRangeUsed = false; boolean roiUsed = true; boolean useROIAccessor = false; testZonalStats(sourceIMG[0], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[0]); testZonalStats(sourceIMG[1], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[1]); testZonalStats(sourceIMG[2], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[2]); testZonalStats(sourceIMG[3], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[3]); testZonalStats(sourceIMG[4], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[4]); testZonalStats(sourceIMG[5], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[5]); } @Test public void testOnlyROIRasterAccessor() { // This test calculates zonal statistics without NoData boolean noDataRangeUsed = false; boolean roiUsed = true; boolean useROIAccessor = true; testZonalStats(sourceIMG[0], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[0]); testZonalStats(sourceIMG[1], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[1]); testZonalStats(sourceIMG[2], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[2]); testZonalStats(sourceIMG[3], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[3]); testZonalStats(sourceIMG[4], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[4]); testZonalStats(sourceIMG[5], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[5]); } @Test public void testOnlyNoData() { // This test calculates zonal statistics with NoData and without ROI boolean noDataRangeUsed = true; boolean roiUsed = false; boolean useROIAccessor = false; testZonalStats(sourceIMG[0], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[0]); testZonalStats(sourceIMG[1], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[1]); testZonalStats(sourceIMG[2], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[2]); testZonalStats(sourceIMG[3], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[3]); testZonalStats(sourceIMG[4], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[4]); testZonalStats(sourceIMG[5], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[5]); } @Test public void testNoDataAndROI() { // This test calculates zonal statistics without NoData boolean noDataRangeUsed = true; boolean roiUsed = true; boolean useROIAccessor = false; testZonalStats(sourceIMG[0], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[0]); testZonalStats(sourceIMG[1], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[1]); testZonalStats(sourceIMG[2], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[2]); testZonalStats(sourceIMG[3], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[3]); testZonalStats(sourceIMG[4], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[4]); testZonalStats(sourceIMG[5], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[5]); } @Test public void testNoDataAndROIRasterAccessor() { // This test calculates zonal statistics with NoData and without ROI boolean noDataRangeUsed = true; boolean roiUsed = true; boolean useROIAccessor = true; testZonalStats(sourceIMG[0], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[0]); testZonalStats(sourceIMG[1], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[1]); testZonalStats(sourceIMG[2], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[2]); testZonalStats(sourceIMG[3], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[3]); testZonalStats(sourceIMG[4], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[4]); testZonalStats(sourceIMG[5], CLASSIFIER, noDataRangeUsed, roiUsed, useROIAccessor, rangeList[5]); } @Test /** * Test building up a set of pre-defined ranges and classifying stats for each. */ public void testStatsEntireImageNoROI() { // build up the ranges int n = 4; byte min = rangeList[0].get(0).getMin().byteValue(); byte max = rangeList[0].get(0).getMax().byteValue(); byte delta = (byte)((max - min) / n); List<Range> ranges = new ArrayList<Range>(); byte b = min; for (int i = 0; i < n; i++) { byte c = (byte) (b + delta); ranges.add(RangeFactory.create(b, true, c, i == n-1)); b = c; } // calculate stats on the entire image RenderedImage destination = ZonalStatsDescriptor.create(sourceIMG[0], null, null, null, noDataByte, null, false, bands, new StatsType[]{StatsType.MEAN}, ranges, true, null); List<ZoneGeometry> result = (List<ZoneGeometry>) destination.getProperty(ZonalStatsDescriptor.ZS_PROPERTY); Map<Range, Statistics[]> stats = result.get(0).getStatsPerBandPerClass(0, 0); assertEquals(4, stats.size()); } public void testZonalStats(RenderedImage source, boolean classifierUsed, boolean noDataRangeUsed, boolean roiUsed, boolean useROIAccessor, List<Range> rangeList) { // The classifier is used, if selected by the related boolean. RenderedImage classifierIMG; if (classifierUsed) { classifierIMG = classifier; } else { classifierIMG = null; } ROI roi = null; if (roiUsed) { roi = roiObject; } // The precalculated NoData Range is used, if selected by the related boolean. Range noDataRange; if (noDataRangeUsed) { int dataType = source.getSampleModel().getDataType(); switch (dataType) { case DataBuffer.TYPE_BYTE: noDataRange = noDataByte; break; case DataBuffer.TYPE_USHORT: noDataRange = noDataUShort; break; case DataBuffer.TYPE_SHORT: noDataRange = noDataShort; break; case DataBuffer.TYPE_INT: noDataRange = noDataInt; break; case DataBuffer.TYPE_FLOAT: noDataRange = noDataFloat; break; case DataBuffer.TYPE_DOUBLE: noDataRange = noDataDouble; break; default: throw new IllegalArgumentException("Wrong data type"); } } else { noDataRange = null; } // Index used for indicating which index of the "calculation" array must be taken for reading the precalculated statistic values int statsIndex = 0; if (noDataRangeUsed && roiUsed) { statsIndex = 3; } else if (noDataRangeUsed) { statsIndex = 1; } else if (roiUsed) { statsIndex = 2; } // Creation of the Image RenderedImage destination = ZonalStatsDescriptor.create(source, classifierIMG, null, roiList, noDataRange, roi, useROIAccessor, bands, stats, minBound, maxBound, numBins, rangeList, false, null); // Statistic calculation List<ZoneGeometry> result = (List<ZoneGeometry>) destination .getProperty(ZonalStatsDescriptor.ZS_PROPERTY); // Calculated Results int dataType = destination.getSampleModel().getDataType(); List<ZoneGeometry> zoneList = zonesLists[dataType][statsIndex]; // Test if the calculated values are equal with a tolerance value if (CLASSIFIER) { // Cycle on all the geometries for (int i = 0; i < result.size(); i++) { ZoneGeometry zoneResult = result.get(i); ZoneGeometry zoneCalculated = zoneList.get(i); // Selection of the zone statistics for the selected band Map<Integer, Map<Range, Statistics[]>> resultPerClass = (Map<Integer, Map<Range, Statistics[]>>) zoneResult .getStatsPerBand(0); // Set of all the keys indicating the various classifier zones Set<Integer> zoneset = resultPerClass.keySet(); // Cycle on all the zones for (int zone : zoneset) { // Result from ZonalStats operation Statistics[] statsResult = (Statistics[]) zoneResult.getStatsPerBandNoRange(0, zone); // Result from calculation Statistics[] statsCalculated = (Statistics[]) zoneCalculated .getStatsPerBandNoRange(0, zone); // Check if the results have the same dimensions assertEquals(statsResult.length, statsCalculated.length); // Check if all the calculations are equal, with a tolerance value for (int j = 0; j < statsResult.length; j++) { Statistics statR = statsResult[j]; Statistics statC = statsCalculated[j]; switch (j) { case 0: case 1: case 2: case 3: case 5: case 6: case 8: case 9: double valueR = (Double) statR.getResult(); double valueC = (Double) statC.getResult(); assertEquals(valueR, valueC, TOLERANCE); break; case 4: double[] extremaR = (double[]) statR.getResult(); double maxR = extremaR[1]; double minR = extremaR[0]; double[] extremaC = (double[]) statC.getResult(); double maxC = extremaC[1]; double minC = extremaC[0]; assertEquals(minR, minC, TOLERANCE); assertEquals(maxR, maxC, TOLERANCE); break; case 7: double[] histR = (double[]) statR.getResult(); double[] histC = (double[]) statC.getResult(); assertEquals(histR.length, histC.length); for (int bin = 0; bin < histR.length; bin++) { double binR = histR[bin]; double binC = histC[bin]; assertEquals(binR, binC, TOLERANCE); } break; } } } } } else { // Cycle on all the geometries for (int i = 0; i < result.size(); i++) { ZoneGeometry zoneResult = result.get(i); ZoneGeometry zoneCalculated = zoneList.get(i); // Selection of the statistics for the selected band // Result from ZonalStats operation Statistics[] statsResult = (Statistics[]) zoneResult .getStatsPerBandNoClassifierNoRange(0); // Result from calculation Statistics[] statsCalculated = (Statistics[]) zoneCalculated .getStatsPerBandNoClassifier(0, rangeList.get(0)); // Check if the results have the same dimensions assertEquals(statsResult.length, statsCalculated.length); // Check if all the calculations are equal, with a tolerance value for (int j = 0; j < statsResult.length; j++) { Statistics statR = statsResult[j]; Statistics statC = statsCalculated[j]; switch (j) { case 0: case 1: case 2: case 3: case 5: case 6: case 8: case 9: double valueR = (Double) statR.getResult(); double valueC = (Double) statC.getResult(); assertEquals(valueR, valueC, TOLERANCE); break; case 4: double[] extremaR = (double[]) statR.getResult(); double maxR = extremaR[1]; double minR = extremaR[0]; double[] extremaC = (double[]) statC.getResult(); double maxC = extremaC[1]; double minC = extremaC[0]; assertEquals(minR, minC, TOLERANCE); assertEquals(maxR, maxC, TOLERANCE); break; case 7: double[] histR = (double[]) statR.getResult(); double[] histC = (double[]) statC.getResult(); assertEquals(histR.length, histC.length); for (int bin = 0; bin < histR.length; bin++) { double binR = histR[bin]; double binC = histC[bin]; assertEquals(binR, binC, TOLERANCE); } } } } } } }