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.MBFImage;
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 MBFStatisticalPixelProfileModel} is a statistical model of pixels
048 * from an {@link MBFImage} 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 * Internally, the model is one-dimensional, and is created by stacking the
057 * samples from each image band.
058 * 
059 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
060 */
061@SuppressWarnings("deprecation")
062public class MBFStatisticalPixelProfileModel implements PixelProfileModel<MBFImage> {
063        private MultivariateSummaryStatistics statistics;
064        private int nsamples;
065        private FLineSampler sampler;
066
067        private double[] mean;
068        private Matrix invCovar;
069
070        /**
071         * Construct a new {@link MBFStatisticalPixelProfileModel} with the given
072         * number of samples per line, and the given sampling strategy.
073         * 
074         * @param nsamples
075         *            number of samples
076         * @param sampler
077         *            line sampling strategy
078         */
079        public MBFStatisticalPixelProfileModel(int nsamples, FLineSampler sampler) {
080                this.nsamples = nsamples;
081                this.statistics = new MultivariateSummaryStatistics(nsamples, true);
082                this.sampler = sampler;
083        }
084
085        private float[] normaliseSamples(float[] samples) {
086                final float sum = FloatArrayStatsUtils.sum(samples);
087
088                for (int i = 0; i < samples.length; i++) {
089                        samples[i] /= sum;
090                }
091
092                return samples;
093        }
094
095        @Override
096        public void updateModel(MBFImage image, Line2d line) {
097                final float[] samples = extractNormalisedStacked(image, line);
098
099                try {
100                        statistics.addValue(ArrayUtils.convertToDouble(samples));
101                } catch (final DimensionMismatchException e) {
102                        throw new RuntimeException(e);
103                }
104
105                invCovar = null;
106                mean = null;
107        }
108
109        private float[][] extract(MBFImage image, Line2d line, int numSamples) {
110                final float[][] samples = new float[image.numBands()][];
111
112                for (int i = 0; i < image.numBands(); i++) {
113                        samples[i] = sampler.extractSamples(line, image.getBand(i), numSamples);
114                }
115
116                return samples;
117        }
118
119        /**
120         * Extract normalised stacked samples b1b1b1bb2b2b2b3b3b3...
121         * 
122         * @param image
123         * @param line
124         * @return
125         */
126        private float[] extractNormalisedStacked(MBFImage image, Line2d line) {
127                final float[] samples = new float[nsamples * image.numBands()];
128
129                for (int i = 0; i < image.numBands(); i++) {
130                        final float[] s = sampler.extractSamples(line, image.getBand(i), nsamples);
131                        System.arraycopy(s, 0, samples, i * nsamples, nsamples);
132                }
133
134                return normaliseSamples(samples);
135        }
136
137        /**
138         * @return the mean of the model
139         */
140        public double[] getMean() {
141                if (mean == null) {
142                        mean = statistics.getMean();
143                        invCovar = new Matrix(statistics.getCovariance().getData()).inverse();
144                }
145
146                return mean;
147        }
148
149        /**
150         * @return the covariance of the model
151         */
152        public Matrix getCovariance() {
153                if (mean == null) {
154                        mean = statistics.getMean();
155                        invCovar = new Matrix(statistics.getCovariance().getData()).inverse();
156                }
157                return new Matrix(statistics.getCovariance().getData());
158        }
159
160        /**
161         * @return the inverse of the covariance matrix
162         */
163        public Matrix getInverseCovariance() {
164                if (mean == null) {
165                        mean = statistics.getMean();
166                        invCovar = new Matrix(statistics.getCovariance().getData()).inverse();
167                }
168                return invCovar;
169        }
170
171        /**
172         * Compute the Mahalanobis distance of the given vector to the internal
173         * model. The vector must have the same size as the number of samples given
174         * during construction.
175         * 
176         * @param vector
177         *            the vector
178         * @return the computed Mahalanobis distance
179         */
180        public float computeMahalanobis(float[] vector) {
181                if (mean == null) {
182                        mean = statistics.getMean();
183                        try {
184                                invCovar = new Matrix(statistics.getCovariance().getData()).inverse();
185                        } catch (final RuntimeException e) {
186                                invCovar = Matrix.identity(nsamples, nsamples);
187                        }
188                }
189
190                final double[] meanCentered = new double[mean.length];
191                for (int i = 0; i < mean.length; i++) {
192                        meanCentered[i] = vector[i] - mean[i];
193                }
194
195                final Matrix mct = new Matrix(new double[][] { meanCentered });
196                final Matrix mc = mct.transpose();
197
198                final Matrix dist = mct.times(invCovar).times(mc);
199
200                return (float) dist.get(0, 0);
201        }
202
203        /**
204         * Compute the Mahalanobis distance of a vector of samples extracted along a
205         * line in the given image to the internal model.
206         * 
207         * @param image
208         *            the image to sample
209         * @param line
210         *            the line to sample along
211         * @return the computed Mahalanobis distance
212         */
213        public float computeMahalanobis(MBFImage image, Line2d line) {
214                final float[] samples = extractNormalisedStacked(image, line);
215                return computeMahalanobis(samples);
216        }
217
218        /**
219         * Extract numSamples samples from the line in the image and then compare
220         * this model at each overlapping position starting from the first sample at
221         * the beginning of the line.
222         * 
223         * numSamples must be bigger than the number of samples used to construct
224         * the model. In addition, callers are responsible for ensuring the sampling
225         * rate between the new samples and the model is equal.
226         * 
227         * @param image
228         *            the image to sample
229         * @param line
230         *            the line to sample along
231         * @param numSamples
232         *            the number of samples to make
233         * @return an array of the computed Mahalanobis distances at each offset
234         */
235        public float[] computeMahalanobisWindowed(MBFImage image, Line2d line, int numSamples) {
236                final float[][] samples = extract(image, line, numSamples);
237                return computeMahalanobisWindowed(samples);
238        }
239
240        @Override
241        public Point2d computeNewBest(MBFImage image, Line2d line, int numSamples) {
242                final float[] resp = computeMahalanobisWindowed(image, line, numSamples);
243
244                final int minIdx = ArrayUtils.minIndex(resp);
245                final int offset = (numSamples - nsamples) / 2;
246
247                if (resp[offset] == resp[minIdx]) // prefer the centre over another
248                                                                                        // value if same response
249                        return line.calculateCentroid();
250
251                // the sample line might be different, so we need to measure relative to
252                // it...
253                line = this.sampler.getSampleLine(line, image.getBand(0), numSamples);
254
255                final float x = line.begin.getX();
256                final float y = line.begin.getY();
257                final float dxStep = (line.end.getX() - x) / (numSamples - 1);
258                final float dyStep = (line.end.getY() - y) / (numSamples - 1);
259
260                return new Point2dImpl(x + (minIdx + offset) * dxStep, y + (minIdx + offset) * dyStep);
261        }
262
263        @Override
264        public float computeMovementDistance(MBFImage image, Line2d line, int numSamples, Point2d pt) {
265                final Line2d sampleLine = sampler.getSampleLine(line, image.getBand(0), numSamples);
266
267                return (float) (2 * Line2d.distance(sampleLine.calculateCentroid(), pt) / sampleLine.calculateLength());
268        }
269
270        /**
271         * Compare this model at each overlapping position of the given vector
272         * starting from the first sample and return the distance for each overlap.
273         * 
274         * The length of the vector must be bigger than the number of samples used
275         * to construct the model. In addition, callers are responsible for ensuring
276         * the sampling rate between the new samples and the model is equal.
277         * 
278         * @param vector
279         *            array of samples, one vector per band
280         * @return an array of the computed Mahalanobis distances at each offset
281         */
282        public float[] computeMahalanobisWindowed(float[][] vector) {
283                final int maxShift = vector.length - nsamples + 1;
284
285                final float[] responses = new float[maxShift];
286                float[] samples = new float[nsamples * vector.length];
287                for (int i = 0; i < maxShift; i++) {
288                        for (int j = 0; j < vector.length; j++) {
289                                System.arraycopy(vector[j], i, samples, nsamples * j, nsamples);
290                        }
291                        samples = normaliseSamples(samples);
292                        responses[i] = computeMahalanobis(samples);
293                }
294
295                return responses;
296        }
297
298        @Override
299        public String toString() {
300                return "\nPixelProfileModel[\n" +
301                                "\tcount = " + statistics.getN() + "\n" +
302                                "\tmean = " + Arrays.toString(statistics.getMean()) + "\n" +
303                                "\tcovar = " + statistics.getCovariance() + "\n" +
304                                "]";
305        }
306
307        /**
308         * @return the number of samples used along each profile line
309         */
310        public int getNumberSamples() {
311                return nsamples;
312        }
313
314        /**
315         * @return the sampler used by the model to extract samples along profiles
316         */
317        public FLineSampler getSampler() {
318                return sampler;
319        }
320
321        @Override
322        public float computeCost(MBFImage image, Line2d line) {
323                return computeMahalanobis(image, line);
324        }
325}