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.objectdetection.haar;
031
032import java.util.List;
033
034import org.openimaj.citation.annotation.Reference;
035import org.openimaj.citation.annotation.ReferenceType;
036import org.openimaj.image.analysis.algorithm.SummedSqTiltAreaTable;
037
038/**
039 * Class describing a Haar-like feature. The features are typically built from
040 * two or three overlapping rectangles, and can represent edges, lines and
041 * centre-surround features.
042 * <p>
043 * The response of applying the feature to a specific point on an image (with a
044 * specific scaling) can be efficiently calculated using summed area tables.
045 * <p>
046 * Internally this implementation caches a scaled version of each rectangle for
047 * a given detection scale.
048 * 
049 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
050 * 
051 */
052@Reference(
053                type = ReferenceType.Inproceedings,
054                author = { "Viola, P.", "Jones, M." },
055                title = "Rapid object detection using a boosted cascade of simple features",
056                year = "2001",
057                booktitle = "Computer Vision and Pattern Recognition, 2001. CVPR 2001. Proceedings of the 2001 IEEE Computer Society Conference on",
058                pages = { " I", "511 ", " I", "518 vol.1" },
059                number = "",
060                volume = "1",
061                customData = {
062                                "keywords", " AdaBoost; background regions; boosted simple feature cascade; classifiers; face detection; image processing; image representation; integral image; machine learning; object specific focus-of-attention mechanism; rapid object detection; real-time applications; statistical guarantees; visual object detection; feature extraction; image classification; image representation; learning (artificial intelligence); object detection;",
063                                "doi", "10.1109/CVPR.2001.990517",
064                                "ISSN", "1063-6919 "
065                })
066public abstract class HaarFeature {
067        /**
068         * The rectangles that make up this feature.
069         */
070        public WeightedRectangle[] rects;
071
072        private final float correctionFactor;
073        protected WeightedRectangle[] cachedRects;
074
075        /**
076         * Construct a new feature
077         * 
078         * @param rects
079         * @param correctionFactor
080         */
081        private HaarFeature(WeightedRectangle[] rects, final float correctionFactor) {
082                this.rects = rects;
083                this.correctionFactor = correctionFactor;
084
085                cachedRects = new WeightedRectangle[rects.length];
086                for (int i = 0; i < cachedRects.length; i++) {
087                        cachedRects[i] = new WeightedRectangle(0, 0, 0, 0, 0);
088                }
089        }
090
091        final void updateCaches(StageTreeClassifier cascade) {
092                setScale(cascade.cachedScale, cascade.cachedInvArea);
093        }
094
095        /**
096         * Set the current detection scale, setting up the internal caches
097         * appropriately.
098         * 
099         * @param scale
100         *            the scale
101         * @param invArea
102         *            the inverse of the detector area
103         */
104        public final void setScale(float scale, float invArea) {
105                double sum0 = 0;
106                double area0 = 0;
107
108                int base_w = Integer.MAX_VALUE;
109                int base_h = Integer.MAX_VALUE;
110                int new_base_w = 0;
111                int new_base_h = 0;
112                int kx;
113                int ky;
114                boolean flagx = false;
115                boolean flagy = false;
116                int x0 = 0;
117                int y0 = 0;
118
119                final WeightedRectangle firstArea = rects[0];
120                for (final WeightedRectangle r : rects) {
121                        if ((r.width - 1) >= 0) {
122                                base_w = Math.min(base_w, (r.width - 1));
123                        }
124                        if ((r.x - firstArea.x - 1) >= 0) {
125                                base_w = Math.min(base_w, (r.x - firstArea.x - 1));
126                        }
127                        if ((r.height - 1) >= 0) {
128                                base_h = Math.min(base_h, (r.height - 1));
129                        }
130                        if ((r.y - firstArea.y - 1) >= 0) {
131                                base_h = Math.min(base_h, (r.y - firstArea.y - 1));
132                        }
133                }
134
135                base_w += 1;
136                base_h += 1;
137                kx = firstArea.width / base_w;
138                ky = firstArea.height / base_h;
139
140                if (kx <= 0) {
141                        flagx = true;
142                        new_base_w = Math.round(firstArea.width * scale) / kx;
143                        x0 = Math.round(firstArea.x * scale);
144                }
145
146                if (ky <= 0) {
147                        flagy = true;
148                        new_base_h = Math.round(firstArea.height * scale)
149                                        / ky;
150                        y0 = Math.round(firstArea.y * scale);
151                }
152
153                for (int k = 0; k < rects.length; k++) {
154                        final WeightedRectangle r = rects[k];
155                        int x;
156                        int y;
157                        int width;
158                        int height;
159                        float correction_ratio;
160
161                        if (flagx) {
162                                x = (r.x - firstArea.x) * new_base_w / base_w + x0;
163                                width = r.width * new_base_w / base_w;
164                        } else {
165                                x = Math.round(r.x * scale);
166                                width = Math.round(r.width * scale);
167                        }
168
169                        if (flagy) {
170                                y = (r.y - firstArea.y) * new_base_h / base_h + y0;
171                                height = r.height * new_base_h / base_h;
172                        } else {
173                                y = Math.round(r.y * scale);
174                                height = Math.round(r.height * scale);
175                        }
176
177                        correction_ratio = correctionFactor * invArea;
178
179                        cachedRects[k].weight = (rects[k].weight * correction_ratio);
180                        cachedRects[k].x = x;
181                        cachedRects[k].y = y;
182                        cachedRects[k].width = width;
183                        cachedRects[k].height = height;
184
185                        if (k == 0) {
186                                area0 = width * height;
187                        } else {
188                                sum0 += cachedRects[k].weight * width * height;
189                        }
190                }
191
192                cachedRects[0].weight = (float) (-sum0 / area0);
193        }
194
195        /**
196         * Compute the response of this feature at the given location. The scale of
197         * the feature must have previously been set through a call to
198         * {@link #setScale(float, float)} (this is only required once per scale).
199         * 
200         * @param sat
201         *            the summed area table(s). If there are tilted features, then
202         *            this must include the tilted SAT.
203         * @param x
204         *            the x-ordinate for the window being tested
205         * @param y
206         *            the y-ordinate for the window being tested
207         * @return the response to the feature
208         */
209        public abstract float computeResponse(SummedSqTiltAreaTable sat, int x, int y);
210
211        static class TiltedFeature extends HaarFeature {
212                public TiltedFeature(WeightedRectangle[] rects) {
213                        super(rects, 2f);
214                }
215
216                @Override
217                public float computeResponse(SummedSqTiltAreaTable sat, int rx, int ry) {
218                        float total = 0;
219                        for (int i = 0; i < cachedRects.length; i++) {
220                                final WeightedRectangle rect = cachedRects[i];
221
222                                final int x = rx + rect.x;
223                                final int y = ry + rect.y;
224                                final int width = rect.width;
225                                final int height = rect.height;
226
227                                final float p0 = sat.tiltSum.pixels[y][x];
228                                final float p1 = sat.tiltSum.pixels[y + height][x - height];
229                                final float p2 = sat.tiltSum.pixels[y + width][x + width];
230                                final float p3 = sat.tiltSum.pixels[y + width + height][x + width - height];
231
232                                final float regionSum = p0 - p1 - p2 + p3;
233
234                                total += regionSum * rect.weight;
235                        }
236
237                        return total;
238                }
239        }
240
241        static class NormalFeature extends HaarFeature {
242                public NormalFeature(WeightedRectangle[] rects) {
243                        super(rects, 1f);
244                }
245
246                @Override
247                public float computeResponse(SummedSqTiltAreaTable sat, int rx, int ry) {
248                        float total = 0;
249                        for (int i = 0; i < cachedRects.length; i++) {
250                                final WeightedRectangle rect = cachedRects[i];
251
252                                final int x = rx + rect.x;
253                                final int y = ry + rect.y;
254                                final int width = rect.width;
255                                final int height = rect.height;
256
257                                final int yh = y + height;
258                                final int xw = x + width;
259
260                                final float regionSum = sat.sum.pixels[yh][xw] - sat.sum.pixels[yh][x]
261                                                - sat.sum.pixels[y][xw] + sat.sum.pixels[y][x];
262
263                                total += regionSum * rect.weight;
264                        }
265
266                        return total;
267                }
268        }
269
270        /**
271         * Create a feature from the given data. The specific type of feature
272         * created depends on whether or not the feature is tilted.
273         * 
274         * @param rectList
275         *            the rectangles defining the feature
276         * @param tilted
277         *            is the feature tilted?
278         * @return the new {@link HaarFeature} object.
279         */
280        public static HaarFeature create(List<WeightedRectangle> rectList, boolean tilted) {
281                final WeightedRectangle[] rects = rectList.toArray(new WeightedRectangle[rectList.size()]);
282
283                if (tilted)
284                        return new TiltedFeature(rects);
285
286                return new NormalFeature(rects);
287        }
288
289        /**
290         * Construct a feature with the given parameters.
291         * 
292         * @param tilted
293         *            is the feature tilted?
294         * @param x0
295         *            x-ordinate of top-left of first rectangle
296         * @param y0
297         *            y-ordinate of top-left of first rectangle
298         * @param w0
299         *            width of first rectangle
300         * @param h0
301         *            height of first rectangle
302         * @param wt0
303         *            weight of first rectangle
304         * @param x1
305         *            x-ordinate of top-left of second rectangle
306         * @param y1
307         *            y-ordinate of top-left of second rectangle
308         * @param w1
309         *            width of second rectangle
310         * @param h1
311         *            height of second rectangle
312         * @param wt1
313         *            weight of second rectangle
314         * @return the feature
315         */
316        public static HaarFeature create(boolean tilted,
317                        int x0, int y0, int w0, int h0, float wt0,
318                        int x1, int y1, int w1, int h1, float wt1)
319        {
320                final WeightedRectangle[] rects = new WeightedRectangle[2];
321                rects[0] = new WeightedRectangle(x0, y0, w0, h0, wt0);
322                rects[1] = new WeightedRectangle(x1, y1, w1, h1, wt1);
323
324                return tilted ? new TiltedFeature(rects) : new NormalFeature(rects);
325        }
326
327        /**
328         * Construct a feature with the given parameters.
329         * 
330         * @param tilted
331         *            is the feature tilted?
332         * @param x0
333         *            x-ordinate of top-left of first rectangle
334         * @param y0
335         *            y-ordinate of top-left of first rectangle
336         * @param w0
337         *            width of first rectangle
338         * @param h0
339         *            height of first rectangle
340         * @param wt0
341         *            weight of first rectangle
342         * @param x1
343         *            x-ordinate of top-left of second rectangle
344         * @param y1
345         *            y-ordinate of top-left of second rectangle
346         * @param w1
347         *            width of second rectangle
348         * @param h1
349         *            height of second rectangle
350         * @param wt1
351         *            weight of second rectangle
352         * @param x2
353         *            x-ordinate of top-left of third rectangle
354         * @param y2
355         *            y-ordinate of top-left of third rectangle
356         * @param w2
357         *            width of third rectangle
358         * @param h2
359         *            height of third rectangle
360         * @param wt2
361         *            weight of third rectangle
362         * @return the feature
363         */
364        public static HaarFeature create(boolean tilted,
365                        int x0, int y0, int w0, int h0, float wt0,
366                        int x1, int y1, int w1, int h1, float wt1,
367                        int x2, int y2, int w2, int h2, float wt2)
368        {
369                final WeightedRectangle[] rects = new WeightedRectangle[3];
370                rects[0] = new WeightedRectangle(x0, y0, w0, h0, wt0);
371                rects[1] = new WeightedRectangle(x1, y1, w1, h1, wt1);
372                rects[2] = new WeightedRectangle(x2, y2, w2, h2, wt2);
373
374                return tilted ? new TiltedFeature(rects) : new NormalFeature(rects);
375        }
376}