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}