View Javadoc

1   /**
2    * Copyright (c) 2011, The University of Southampton and the individual contributors.
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    *
8    *   * 	Redistributions of source code must retain the above copyright notice,
9    * 	this list of conditions and the following disclaimer.
10   *
11   *   *	Redistributions in binary form must reproduce the above copyright notice,
12   * 	this list of conditions and the following disclaimer in the documentation
13   * 	and/or other materials provided with the distribution.
14   *
15   *   *	Neither the name of the University of Southampton nor the names of its
16   * 	contributors may be used to endorse or promote products derived from this
17   * 	software without specific prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package org.openimaj.image.pixel.statistics;
31  
32  import java.util.Arrays;
33  
34  import org.apache.commons.math.DimensionMismatchException;
35  import org.apache.commons.math.stat.descriptive.MultivariateSummaryStatistics;
36  import org.openimaj.image.FImage;
37  import org.openimaj.image.pixel.sampling.FLineSampler;
38  import org.openimaj.math.geometry.line.Line2d;
39  import org.openimaj.math.geometry.point.Point2d;
40  import org.openimaj.math.geometry.point.Point2dImpl;
41  import org.openimaj.math.util.FloatArrayStatsUtils;
42  import org.openimaj.util.array.ArrayUtils;
43  
44  import Jama.Matrix;
45  
46  /**
47   * An {@link FStatisticalPixelProfileModel} is a statistical model of pixels
48   * from an {@link FImage} sampled along a line.
49   * 
50   * The model allows for various sampling strategies (see {@link FLineSampler})
51   * and uses the mean and covariance as its internal state.
52   * 
53   * The model is updateable, but does not hold on to previously seen samples to
54   * reduce memory usage.
55   * 
56   * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
57   */
58  @SuppressWarnings("deprecation")
59  public class FStatisticalPixelProfileModel implements PixelProfileModel<FImage> {
60  	private MultivariateSummaryStatistics statistics;
61  	private int nsamples;
62  	private FLineSampler sampler;
63  
64  	private double[] mean;
65  	private Matrix invCovar;
66  
67  	/**
68  	 * Construct a new {@link FStatisticalPixelProfileModel} with the given
69  	 * number of samples per line, and the given sampling strategy.
70  	 * 
71  	 * @param nsamples
72  	 *            number of samples
73  	 * @param sampler
74  	 *            line sampling strategy
75  	 */
76  	public FStatisticalPixelProfileModel(int nsamples, FLineSampler sampler) {
77  		this.nsamples = nsamples;
78  		this.statistics = new MultivariateSummaryStatistics(nsamples, true);
79  		this.sampler = sampler;
80  	}
81  
82  	private float[] normaliseSamples(float[] samples) {
83  		final float sum = FloatArrayStatsUtils.sum(samples);
84  
85  		for (int i = 0; i < samples.length; i++) {
86  			samples[i] /= sum;
87  		}
88  
89  		return samples;
90  	}
91  
92  	@Override
93  	public void updateModel(FImage image, Line2d line) {
94  		final float[] samples = normaliseSamples(sampler.extractSamples(line, image, nsamples));
95  		try {
96  			statistics.addValue(ArrayUtils.convertToDouble(samples));
97  		} catch (final DimensionMismatchException e) {
98  			throw new RuntimeException(e);
99  		}
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 }