001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.image.pixel.statistics; 031 032import java.util.Arrays; 033 034import org.apache.commons.math.DimensionMismatchException; 035import org.apache.commons.math.stat.descriptive.MultivariateSummaryStatistics; 036import org.openimaj.image.FImage; 037import org.openimaj.image.pixel.sampling.FLineSampler; 038import org.openimaj.math.geometry.line.Line2d; 039import org.openimaj.math.geometry.point.Point2d; 040import org.openimaj.math.geometry.point.Point2dImpl; 041import org.openimaj.math.util.FloatArrayStatsUtils; 042import org.openimaj.util.array.ArrayUtils; 043 044import Jama.Matrix; 045 046/** 047 * An {@link FStatisticalPixelProfileModel} is a statistical model of pixels 048 * from an {@link FImage} sampled along a line. 049 * 050 * The model allows for various sampling strategies (see {@link FLineSampler}) 051 * and uses the mean and covariance as its internal state. 052 * 053 * The model is updateable, but does not hold on to previously seen samples to 054 * reduce memory usage. 055 * 056 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 057 */ 058@SuppressWarnings("deprecation") 059public class FStatisticalPixelProfileModel implements PixelProfileModel<FImage> { 060 private MultivariateSummaryStatistics statistics; 061 private int nsamples; 062 private FLineSampler sampler; 063 064 private double[] mean; 065 private Matrix invCovar; 066 067 /** 068 * Construct a new {@link FStatisticalPixelProfileModel} with the given 069 * number of samples per line, and the given sampling strategy. 070 * 071 * @param nsamples 072 * number of samples 073 * @param sampler 074 * line sampling strategy 075 */ 076 public FStatisticalPixelProfileModel(int nsamples, FLineSampler sampler) { 077 this.nsamples = nsamples; 078 this.statistics = new MultivariateSummaryStatistics(nsamples, true); 079 this.sampler = sampler; 080 } 081 082 private float[] normaliseSamples(float[] samples) { 083 final float sum = FloatArrayStatsUtils.sum(samples); 084 085 for (int i = 0; i < samples.length; i++) { 086 samples[i] /= sum; 087 } 088 089 return samples; 090 } 091 092 @Override 093 public void updateModel(FImage image, Line2d line) { 094 final float[] samples = normaliseSamples(sampler.extractSamples(line, image, nsamples)); 095 try { 096 statistics.addValue(ArrayUtils.convertToDouble(samples)); 097 } catch (final DimensionMismatchException e) { 098 throw new RuntimeException(e); 099 } 100 101 invCovar = null; 102 mean = null; 103 } 104 105 /** 106 * @return the mean of the model 107 */ 108 public double[] getMean() { 109 if (mean == null) { 110 mean = statistics.getMean(); 111 invCovar = new Matrix(statistics.getCovariance().getData()).inverse(); 112 } 113 114 return mean; 115 } 116 117 /** 118 * @return the covariance of the model 119 */ 120 public Matrix getCovariance() { 121 if (mean == null) { 122 mean = statistics.getMean(); 123 invCovar = new Matrix(statistics.getCovariance().getData()).inverse(); 124 } 125 return new Matrix(statistics.getCovariance().getData()); 126 } 127 128 /** 129 * @return the inverse of the covariance matrix 130 */ 131 public Matrix getInverseCovariance() { 132 if (mean == null) { 133 mean = statistics.getMean(); 134 invCovar = new Matrix(statistics.getCovariance().getData()).inverse(); 135 } 136 return invCovar; 137 } 138 139 /** 140 * Compute the Mahalanobis distance of the given vector to the internal 141 * model. The vector must have the same size as the number of samples given 142 * during construction. 143 * 144 * @param vector 145 * the vector 146 * @return the computed Mahalanobis distance 147 */ 148 public float computeMahalanobis(float[] vector) { 149 if (mean == null) { 150 mean = statistics.getMean(); 151 try { 152 invCovar = new Matrix(statistics.getCovariance().getData()).inverse(); 153 } catch (final RuntimeException e) { 154 invCovar = Matrix.identity(nsamples, nsamples); 155 } 156 } 157 158 final double[] meanCentered = new double[mean.length]; 159 for (int i = 0; i < mean.length; i++) { 160 meanCentered[i] = vector[i] - mean[i]; 161 } 162 163 final Matrix mct = new Matrix(new double[][] { meanCentered }); 164 final Matrix mc = mct.transpose(); 165 166 final Matrix dist = mct.times(invCovar).times(mc); 167 168 return (float) dist.get(0, 0); 169 } 170 171 /** 172 * Compute the Mahalanobis distance of a vector of samples extracted along a 173 * line in the given image to the internal model. 174 * 175 * @param image 176 * the image to sample 177 * @param line 178 * the line to sample along 179 * @return the computed Mahalanobis distance 180 */ 181 public float computeMahalanobis(FImage image, Line2d line) { 182 final float[] samples = normaliseSamples(sampler.extractSamples(line, image, nsamples)); 183 return computeMahalanobis(samples); 184 } 185 186 /** 187 * Extract numSamples samples from the line in the image and then compare 188 * this model at each overlapping position starting from the first sample at 189 * the beginning of the line. 190 * 191 * numSamples must be bigger than the number of samples used to construct 192 * the model. In addition, callers are responsible for ensuring the sampling 193 * rate between the new samples and the model is equal. 194 * 195 * @param image 196 * the image to sample 197 * @param line 198 * the line to sample along 199 * @param numSamples 200 * the number of samples to make 201 * @return an array of the computed Mahalanobis distances at each offset 202 */ 203 public float[] computeMahalanobisWindowed(FImage image, Line2d line, int numSamples) { 204 final float[] samples = sampler.extractSamples(line, image, numSamples); 205 return computeMahalanobisWindowed(samples); 206 } 207 208 @Override 209 public Point2d computeNewBest(FImage image, Line2d line, int numSamples) { 210 final float[] resp = computeMahalanobisWindowed(image, line, numSamples); 211 212 final int minIdx = ArrayUtils.minIndex(resp); 213 final int offset = (numSamples - nsamples) / 2; 214 215 if (resp[offset] == resp[minIdx]) // prefer the centre over another 216 // value if same response 217 return line.calculateCentroid(); 218 219 // the sample line might be different, so we need to measure relative to 220 // it... 221 line = this.sampler.getSampleLine(line, image, numSamples); 222 223 final float x = line.begin.getX(); 224 final float y = line.begin.getY(); 225 final float dxStep = (line.end.getX() - x) / (numSamples - 1); 226 final float dyStep = (line.end.getY() - y) / (numSamples - 1); 227 228 return new Point2dImpl(x + (minIdx + offset) * dxStep, y + (minIdx + offset) * dyStep); 229 } 230 231 @Override 232 public float computeMovementDistance(FImage image, Line2d line, int numSamples, Point2d pt) { 233 final Line2d sampleLine = sampler.getSampleLine(line, image, numSamples); 234 235 return (float) (2 * Line2d.distance(sampleLine.calculateCentroid(), pt) / sampleLine.calculateLength()); 236 } 237 238 /** 239 * Compare this model at each overlapping position of the given vector 240 * starting from the first sample and return the distance for each overlap. 241 * 242 * The length of the vector must be bigger than the number of samples used 243 * to construct the model. In addition, callers are responsible for ensuring 244 * the sampling rate between the new samples and the model is equal. 245 * 246 * @param vector 247 * array of samples 248 * @return an array of the computed Mahalanobis distances at each offset 249 */ 250 public float[] computeMahalanobisWindowed(float[] vector) { 251 final int maxShift = vector.length - nsamples + 1; 252 253 final float[] responses = new float[maxShift]; 254 float[] samples = new float[nsamples]; 255 for (int i = 0; i < maxShift; i++) { 256 System.arraycopy(vector, i, samples, 0, nsamples); 257 samples = normaliseSamples(samples); 258 responses[i] = computeMahalanobis(samples); 259 } 260 261 return responses; 262 } 263 264 @Override 265 public String toString() { 266 return "\nPixelProfileModel[\n" + 267 "\tcount = " + statistics.getN() + "\n" + 268 "\tmean = " + Arrays.toString(statistics.getMean()) + "\n" + 269 "\tcovar = " + statistics.getCovariance() + "\n" + 270 "]"; 271 } 272 273 /** 274 * @return the number of samples used along each profile line 275 */ 276 public int getNumberSamples() { 277 return nsamples; 278 } 279 280 /** 281 * @return the sampler used by the model to extract samples along profiles 282 */ 283 public FLineSampler getSampler() { 284 return sampler; 285 } 286 287 @Override 288 public float computeCost(FImage image, Line2d line) { 289 return computeMahalanobis(image, line); 290 } 291}