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.processing.face.tracking.clm;
031
032import java.util.ArrayList;
033import java.util.List;
034
035import org.openimaj.image.FImage;
036import org.openimaj.image.MBFImage;
037import org.openimaj.image.colour.RGBColour;
038import org.openimaj.image.processing.face.tracking.clm.MultiTracker.TrackedFace;
039import org.openimaj.image.processing.face.tracking.clm.MultiTracker.TrackerVars;
040import org.openimaj.image.processing.resize.ResizeProcessor;
041import org.openimaj.math.geometry.point.Point2dImpl;
042import org.openimaj.math.geometry.shape.Rectangle;
043import org.openimaj.math.geometry.shape.Triangle;
044
045import Jama.Matrix;
046
047import com.jsaragih.IO;
048import com.jsaragih.Tracker;
049
050/**
051 * CLM-based face tracker
052 *
053 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
054 */
055public class CLMFaceTracker {
056        /** The tracker to use */
057        public MultiTracker model = null;
058
059        /** The face mesh */
060        public int[][] triangles = null;
061
062        /** The face connections */
063        public int[][] connections = null;
064
065        /** The scale at which to process the video */
066        public float scale = 1f;
067
068        /** Whether to use the face check (using pixels as a face classifier) */
069        public boolean fcheck = false;
070
071        /** Number of frames on which to force a redetection */
072        public int fpd = -1;
073
074        /** Search window size while tracking */
075        public int[] wSize1 = { 7 };
076
077        /** Search window size when initialising after a failed track */
078        public int[] wSize2 = { 11, 9, 7 };
079
080        /** Number of iterations to use for model fitting */
081        public int nIter = 5;
082
083        /** Number of standard deviations from the mean face to allow in the model */
084        public double clamp = 3;
085
086        /** Model fitting optimisation tolerance */
087        public double fTol = 0.01;
088
089        /** Whether the last track failed */
090        private boolean failed = true;
091
092        /** The size of the search area for redetection (template matching) */
093        public float searchAreaSize = 1.4f;
094
095        /** Colour to draw the connections */
096        private Float[] connectionColour = RGBColour.WHITE;
097
098        /** Colour to draw the points */
099        private Float[] pointColour = RGBColour.GREEN;
100
101        /** Colour to draw the mesh */
102        private Float[] meshColour = RGBColour.BLACK;
103
104        /** Colour to draw the bounding box */
105        private Float[] boundingBoxColour = RGBColour.RED;
106
107        /** Colour to draw the search area */
108        private Float[] searchAreaColour = RGBColour.YELLOW;
109
110        /**
111         * Instantiates a tracker for tracking faces based on some default values
112         * and models.
113         */
114        public CLMFaceTracker() {
115                this.model = new MultiTracker(MultiTracker.load(Tracker.class
116                                .getResourceAsStream("face2.tracker")));
117                this.triangles = IO.loadTri(Tracker.class.getResourceAsStream("face.tri"));
118                this.connections = IO.loadCon(Tracker.class.getResourceAsStream("face.con"));
119        }
120
121        /**
122         * Track the face in the given frame.
123         *
124         * @param frame
125         *            The frame
126         */
127        public void track(final MBFImage frame) {
128                // Make a greyscale image
129                final FImage im = frame.flatten();
130
131                this.track(im);
132        }
133
134        /**
135         * Track the face in the given frame.
136         *
137         * @param im
138         *            The frame
139         */
140        public void track(FImage im) {
141                // If we're to rescale, let's do that first
142                if (this.scale != 1)
143                        if (this.scale == 0.5f)
144                                im = ResizeProcessor.halfSize(im);
145                        else
146                                im = ResizeProcessor.resample(im, (int) (this.scale * im.width),
147                                                (int) (this.scale * im.height));
148
149                int[] wSize;
150                if (this.failed)
151                        wSize = this.wSize2;
152                else
153                        wSize = this.wSize1;
154
155                // Track the face
156                if (this.model.track(im, wSize, this.fpd, this.nIter, this.clamp, this.fTol, this.fcheck,
157                                this.searchAreaSize) == 0)
158                {
159                        this.failed = false;
160                } else {
161                        this.model.frameReset();
162                        this.failed = true;
163                }
164        }
165
166        /**
167         * Force a reset on the next frame to be tracked.
168         */
169        public void reset() {
170                this.model.frameReset();
171        }
172
173        /**
174         * Draw the model onto the image
175         *
176         * @param image
177         *            The image to draw onto
178         * @param drawTriangles
179         *            Whether to draw the triangles
180         * @param drawConnections
181         *            Whether to draw the connections
182         * @param drawPoints
183         *            Whether to draw the points
184         * @param drawSearchArea
185         *            Whether to draw the search area
186         * @param drawBounds
187         *            Whether to draw the bounds
188         */
189        public void drawModel(final MBFImage image, final boolean drawTriangles,
190                        final boolean drawConnections, final boolean drawPoints,
191                        final boolean drawSearchArea, final boolean drawBounds)
192        {
193                for (int fc = 0; fc < this.model.trackedFaces.size(); fc++) {
194                        final MultiTracker.TrackedFace f = this.model.trackedFaces.get(fc);
195
196                        if (drawSearchArea) {
197                                // Draw the search area size
198                                final Rectangle r = f.lastMatchBounds.clone();
199                                r.scaleCentroid(this.searchAreaSize);
200                                image.createRenderer().drawShape(r, RGBColour.YELLOW);
201                        }
202
203                        // Draw the face model
204                        CLMFaceTracker.drawFaceModel(image, f, drawTriangles, drawConnections, drawPoints,
205                                        drawSearchArea, drawBounds, this.triangles, this.connections, this.scale,
206                                        this.boundingBoxColour, this.meshColour, this.connectionColour,
207                                        this.pointColour);
208                }
209        }
210
211        /**
212         * Draw onto the given image, the given face model.
213         *
214         * @param image
215         *            The image to draw onto
216         * @param f
217         *            The face model to draw
218         * @param drawTriangles
219         *            Whether to draw the triangles
220         * @param drawConnections
221         *            Whether to draw the connections
222         * @param drawPoints
223         *            Whether to draw the points
224         * @param drawSearchArea
225         *            Whether to draw the search area
226         * @param drawBounds
227         *            Whether to draw the bounds
228         * @param triangles
229         *            The reference triangles
230         * @param connections
231         *            The reference connections
232         * @param scale
233         *            The scale at which to draw
234         * @param boundingBoxColour
235         *            Colour to draw the bounding box
236         * @param meshColour
237         *            Colour to draw the mesh
238         * @param connectionColour
239         *            Colour to draw the connections
240         * @param pointColour
241         *            Colour to draw the points
242         */
243        public static void drawFaceModel(final MBFImage image, final MultiTracker.TrackedFace f,
244                        final boolean drawTriangles, final boolean drawConnections, final boolean drawPoints,
245                        final boolean drawSearchArea, final boolean drawBounds, final int[][] triangles,
246                        final int[][] connections, final float scale, final Float[] boundingBoxColour,
247                        final Float[] meshColour, final Float[] connectionColour, final Float[] pointColour)
248        {
249                final int n = f.shape.getRowDimension() / 2;
250                final Matrix visi = f.clm._visi[f.clm.getViewIdx()];
251
252                if (drawBounds && f.lastMatchBounds != null)
253                        image.createRenderer().drawShape(f.lastMatchBounds,
254                                        boundingBoxColour);
255
256                if (drawTriangles) {
257                        // Draw triangulation
258                        for (int i = 0; i < triangles.length; i++) {
259                                if (visi.get(triangles[i][0], 0) == 0 ||
260                                        visi.get(triangles[i][1], 0) == 0 ||
261                                        visi.get(triangles[i][2], 0) == 0
262                                ) continue;
263
264                                final Triangle t = new Triangle(
265                                        new Point2dImpl(
266                                                (float) f.shape.get(triangles[i][0], 0) / scale,
267                                                (float) f.shape.get(triangles[i][0] + n, 0) / scale),
268                                        new Point2dImpl(
269                                                (float) f.shape.get(triangles[i][1], 0) / scale,
270                                                (float) f.shape.get(triangles[i][1] + n, 0) / scale),
271                                        new Point2dImpl(
272                                                (float) f.shape.get(triangles[i][2], 0) / scale,
273                                                (float) f.shape.get(triangles[i][2] + n, 0) / scale)
274                                );
275                                image.drawShape(t, meshColour);
276                        }
277                }
278
279                if (drawConnections) {
280                        // draw connections
281                        for (int i = 0; i < connections[0].length; i++) {
282                                if (visi.get(connections[0][i], 0) == 0
283                                                || visi.get(connections[1][i], 0) == 0)
284                                        continue;
285
286                                image.drawLine(
287                                                new Point2dImpl((float) f.shape.get(connections[0][i],
288                                                                0) / scale, (float) f.shape.get(
289                                                                connections[0][i] + n, 0) / scale),
290                                                new Point2dImpl((float) f.shape.get(connections[1][i],
291                                                                0) / scale, (float) f.shape.get(
292                                                                connections[1][i] + n, 0) / scale),
293                                                connectionColour);
294                        }
295                }
296
297                if (drawPoints) {
298                        // draw points
299                        for (int i = 0; i < n; i++) {
300                                if (visi.get(i, 0) == 0)
301                                        continue;
302
303                                image.drawPoint(new Point2dImpl((float) f.shape.get(i, 0)
304                                                / scale, (float) f.shape.get(i + n, 0) / scale),
305                                                pointColour, 2);
306                        }
307                }
308        }
309
310        /**
311         * Get the reference triangles
312         *
313         * @return The triangles
314         */
315        public int[][] getReferenceTriangles() {
316                return this.triangles;
317        }
318
319        /**
320         * Get the reference connections
321         *
322         * @return The connections
323         */
324        public int[][] getReferenceConnections() {
325                return this.connections;
326        }
327
328        /**
329         * Returns the model tracker
330         *
331         * @return The model tracker
332         */
333        public MultiTracker getModelTracker() {
334                return this.model;
335        }
336
337        /**
338         * Returns the initial variables that will be used by the tracker for each
339         * found face.
340         *
341         * @return The initial tracker variables.
342         */
343        public TrackerVars getInitialVars() {
344                return this.model.getInitialVars();
345        }
346
347        /**
348         * Initialises the face model for the tracked face by calling
349         * {@link MultiTracker#initShape(Rectangle, Matrix, Matrix)} with the
350         * rectangle of {@link TrackedFace#redetectedBounds} and the face shape and
351         * the reference shape. Assumes that the bounds have been already set up.
352         *
353         * @param face
354         *            The face to initialise
355         */
356        public void initialiseFaceModel(final TrackedFace face) {
357                this.model.initShape(face.redetectedBounds, face.shape,
358                                face.referenceShape);
359        }
360
361        /**
362         * @return the searchAreaSize
363         */
364        public float getSearchAreaSize() {
365                return this.searchAreaSize;
366        }
367
368        /**
369         * @param searchAreaSize
370         *            the searchAreaSize to set
371         */
372        public void setSearchAreaSize(final float searchAreaSize) {
373                this.searchAreaSize = searchAreaSize;
374        }
375
376        /**
377         * @return the connectionColour
378         */
379        public Float[] getConnectionColour() {
380                return this.connectionColour;
381        }
382
383        /**
384         * @param connectionColour
385         *            the connectionColour to set
386         */
387        public void setConnectionColour(final Float[] connectionColour) {
388                this.connectionColour = connectionColour;
389        }
390
391        /**
392         * @return the pointColour
393         */
394        public Float[] getPointColour() {
395                return this.pointColour;
396        }
397
398        /**
399         * @param pointColour
400         *            the pointColour to set
401         */
402        public void setPointColour(final Float[] pointColour) {
403                this.pointColour = pointColour;
404        }
405
406        /**
407         * @return the meshColour
408         */
409        public Float[] getMeshColour() {
410                return this.meshColour;
411        }
412
413        /**
414         * @param meshColour
415         *            the meshColour to set
416         */
417        public void setMeshColour(final Float[] meshColour) {
418                this.meshColour = meshColour;
419        }
420
421        /**
422         * @return the boundingBoxColour
423         */
424        public Float[] getBoundingBoxColour() {
425                return this.boundingBoxColour;
426        }
427
428        /**
429         * @param boundingBoxColour
430         *            the boundingBoxColour to set
431         */
432        public void setBoundingBoxColour(final Float[] boundingBoxColour) {
433                this.boundingBoxColour = boundingBoxColour;
434        }
435
436        /**
437         * @return the searchAreaColour
438         */
439        public Float[] getSearchAreaColour() {
440                return this.searchAreaColour;
441        }
442
443        /**
444         * @param searchAreaColour
445         *            the searchAreaColour to set
446         */
447        public void setSearchAreaColour(final Float[] searchAreaColour) {
448                this.searchAreaColour = searchAreaColour;
449        }
450
451        /**
452         * @return the list of tracked faces from the previous call to
453         *         {@link #track(MBFImage)} or {@link #track(FImage)}.
454         */
455        public List<TrackedFace> getTrackedFaces() {
456                return this.model.trackedFaces;
457        }
458
459        /**
460         * Get the triangle mesh corresponding to a tracked face.
461         *
462         * @param face
463         *            the {@link TrackedFace}
464         * @return the mesh
465         */
466        public List<Triangle> getTriangles(final TrackedFace face) {
467                return CLMFaceTracker.getTriangles(face.shape, face.clm._visi[face.clm.getViewIdx()], this.triangles);
468        }
469
470        /**
471         * Get the triangle mesh corresponding to a tracked face.
472         *
473         * @param shape
474         *            the shape matrix
475         * @param visi
476         *            the visibility matrix
477         * @param triangles
478         *            the triangle definitions
479         *
480         * @return the mesh
481         */
482        public static List<Triangle> getTriangles(final Matrix shape, final Matrix visi, final int[][] triangles) {
483                final int n = shape.getRowDimension() / 2;
484                final List<Triangle> tris = new ArrayList<Triangle>();
485
486                for (int i = 0; i < triangles.length; i++) {
487                        if (visi != null &&
488                                        (visi.get(triangles[i][0], 0) == 0 ||
489                                                        visi.get(triangles[i][1], 0) == 0 ||
490                                        visi.get(triangles[i][2], 0) == 0))
491                        {
492                                tris.add(null);
493                        } else {
494                                final Triangle t = new Triangle(
495                                                new Point2dImpl((float) shape.get(triangles[i][0], 0),
496                                                                (float) shape.get(triangles[i][0] + n, 0)),
497                                                new Point2dImpl((float) shape.get(triangles[i][1], 0),
498                                                                (float) shape.get(triangles[i][1] + n, 0)),
499                                                new Point2dImpl((float) shape.get(triangles[i][2], 0),
500                                                                (float) shape.get(triangles[i][2] + n, 0))
501                                                );
502                                tris.add(t);
503                        }
504                }
505
506                return tris;
507        }
508
509        /**
510         *      Set the number of frames after which a redection is forced by the tracker.
511         *      Set it to -1 to avoid forcing any redetection.
512         *      @param nFrames The number of frames.
513         */
514        public void setRedetectEvery( final int nFrames )
515        {
516                this.fpd = nFrames;
517        }
518}