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 */
030/**
031 *
032 */
033package org.openimaj.demos.sandbox.video;
034
035import java.util.ArrayList;
036import java.util.Collection;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Set;
040
041import org.kohsuke.args4j.Argument;
042import org.kohsuke.args4j.CmdLineException;
043import org.kohsuke.args4j.CmdLineParser;
044import org.kohsuke.args4j.Option;
045import org.openimaj.image.MBFImage;
046import org.openimaj.ml.annotation.Annotated;
047import org.openimaj.video.Video;
048import org.openimaj.video.processing.shotdetector.CombiShotDetector;
049import org.openimaj.video.processing.shotdetector.HistogramVideoShotDetector;
050import org.openimaj.video.processing.shotdetector.LocalHistogramVideoShotDetector;
051import org.openimaj.video.processing.shotdetector.ShotBoundary;
052import org.openimaj.video.timecode.HrsMinSecFrameTimecode;
053import org.openimaj.video.timecode.VideoTimecode;
054import org.openimaj.video.xuggle.XuggleVideo;
055
056/**
057 * A tool for annotating scenes in a video. A scene is defined rather vaguely as
058 * a selection of shots of the same subject but, at least for now, each shot
059 * will be a scene. However, there is the ability for scenes to include multiple
060 * shots. Each scene also has a set of {@link SceneAnnotation} objects which
061 * each have a set of annotations that describe the content of the scene.
062 *
063 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
064 * @created 21 Jan 2013
065 * @version $Author$, $Revision$, $Date$
066 */
067public class VideoSceneAnnotationTool
068{
069        /**
070         * Represents a set of contiguous frames in a video that represent a single
071         * scene. This may or may not contain multiple shots. Equals and hashCode
072         * are implemented based on the start and end times of the scene boundaries
073         * (requires that the VideoTimecode used also supports equals).
074         *
075         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
076         * @created 21 Jan 2013
077         * @version $Author$, $Revision$, $Date$
078         */
079        protected static class VideoScene
080        {
081                /** The shot boundary at the beginning of each shot */
082                protected List<ShotBoundary<MBFImage>> listOfShots;
083
084                /** The timecode of the first frame in the scene */
085                protected VideoTimecode startOfScene;
086
087                /** The timecode of the last frame in the scene */
088                protected VideoTimecode endOfScene;
089
090                /**
091                 * Constructor
092                 */
093                public VideoScene()
094                {
095                        this.listOfShots = new ArrayList<ShotBoundary<MBFImage>>();
096                }
097
098                @Override
099                public int hashCode()
100                {
101                        return (int) (this.startOfScene.getTimecodeInMilliseconds() + this.endOfScene.getTimecodeInMilliseconds());
102                }
103
104                @Override
105                public boolean equals(final Object obj)
106                {
107                        if (obj == this)
108                                return true;
109
110                        if (obj instanceof VideoScene)
111                        {
112                                final VideoScene vs = (VideoScene) obj;
113                                return this.startOfScene.equals(vs.startOfScene) &&
114                                                this.endOfScene.equals(vs.endOfScene);
115                        }
116
117                        return false;
118                }
119        }
120
121        /**
122         * A {@link VideoScene} that has a list of annotations associated with it.
123         *
124         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
125         * @created 22 Jan 2013
126         * @version $Author$, $Revision$, $Date$
127         */
128        protected static class AnnotatedVideoScene extends VideoScene
129                        implements Annotated<VideoScene, SceneAnnotation>
130        {
131                /** A list of the annotations from each annotator */
132                public Set<SceneAnnotation> annotations;
133
134                /**
135                 * Constructor
136                 */
137                public AnnotatedVideoScene()
138                {
139                        this.annotations = new HashSet<SceneAnnotation>();
140                }
141
142                /**
143                 * {@inheritDoc}
144                 * 
145                 * @see org.openimaj.ml.annotation.Annotated#getObject()
146                 */
147                @Override
148                public VideoScene getObject()
149                {
150                        return this;
151                }
152
153                /**
154                 * {@inheritDoc}
155                 * 
156                 * @see org.openimaj.ml.annotation.Annotated#getAnnotations()
157                 */
158                @Override
159                public Collection<SceneAnnotation> getAnnotations()
160                {
161                        return this.annotations;
162                }
163
164                /**
165                 * {@inheritDoc}
166                 * 
167                 * @see java.lang.Object#toString()
168                 */
169                @Override
170                public String toString()
171                {
172                        String s = "Annotated Video Scene " + super.toString() + "\n";
173                        s += "======================================\n";
174                        s += "start      : " + this.startOfScene.toString() + "\n";
175                        s += "end        : " + this.endOfScene.toString() + "\n";
176                        s += "annotations: " + this.getAnnotations() + "\n";
177                        s += "======================================\n";
178                        return s;
179                }
180        }
181
182        /**
183         * Stores a scene and its annotations.
184         *
185         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
186         * @created 21 Jan 2013
187         * @version $Author$, $Revision$, $Date$
188         */
189        public static class SceneAnnotation implements
190                        Annotated<VideoScene, String>
191        {
192                /** The annotator that produced the annotations */
193                @SuppressWarnings("rawtypes")
194                protected Class<? extends VideoAnnotator> annotatorClass;
195
196                /** The scene being annotated */
197                protected VideoScene scene;
198
199                /** The annotations */
200                protected Set<String> annotations;
201
202                /**
203                 * Constructor
204                 */
205                public SceneAnnotation()
206                {
207                        this.annotations = new HashSet<String>();
208                }
209
210                /**
211                 * {@inheritDoc}
212                 * 
213                 * @see org.openimaj.ml.annotation.Annotated#getObject()
214                 */
215                @Override
216                public VideoScene getObject()
217                {
218                        return this.scene;
219                }
220
221                /**
222                 * {@inheritDoc}
223                 * 
224                 * @see org.openimaj.ml.annotation.Annotated#getAnnotations()
225                 */
226                @Override
227                public Collection<String> getAnnotations()
228                {
229                        return this.annotations;
230                }
231
232                @Override
233                public String toString()
234                {
235                        return this.annotations.toString();
236                }
237        }
238
239        /**
240         * Options for the tool
241         *
242         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
243         * @created 23 Jan 2013
244         * @version $Author$, $Revision$, $Date$
245         */
246        public static class VideoSceneAnnotationToolOptions
247        {
248                /** The file to use as input */
249                @Argument(
250                                usage = "Video to process",
251                                required = true,
252                                metaVar = "FILE")
253                public String inputFile;
254
255                /** The number of milliseconds under which a scene is discarded */
256                @Option(
257                                name = "--shortestScene",
258                                aliases = "-ss",
259                                usage = "Shortest Scene Length (ms) - default 1500")
260                private final double nMillisecondsShortestScene = 1500;
261        }
262
263        /** The list of video processors that will annotate the video */
264        private final List<VideoAnnotator<MBFImage, String>> annotators;
265
266        /** The list of scene annotations that were last processed */
267        private List<AnnotatedVideoScene> annotatedScenes;
268
269        /** The current scene being processed */
270        private AnnotatedVideoScene currentScene = null;
271
272        /** The number of milliseconds under which a scene is discarded */
273        private double nMillisecondsShortestScene = 1500;
274
275        /**
276         * Constructor that does no processing but sets up the class ready for
277         * processing. Use this constructor if you want to alter the set of
278         * annotation algorithms prior to processing.
279         */
280        public VideoSceneAnnotationTool()
281        {
282                this.annotators = new ArrayList<VideoAnnotator<MBFImage, String>>();
283        }
284
285        /**
286         * Constructor that takes the video to process and begins processing
287         * immediately.
288         * 
289         * @param video
290         *            The video to process.
291         */
292        public VideoSceneAnnotationTool(final Video<MBFImage> video)
293        {
294                this();
295                this.processVideo(video);
296        }
297
298        /**
299         * Add a video annotator into the chain.
300         *
301         * @param annotator
302         *            The annotator to add
303         */
304        public void addVideoAnnotator(final VideoAnnotator<MBFImage, String> annotator)
305        {
306                this.annotators.add(annotator);
307        }
308
309        /**
310         * Processes a video from start to finish.
311         * 
312         * @param video
313         *            The video to process
314         * @return A list of scene annotations
315         */
316        public List<AnnotatedVideoScene> processVideo(final Video<MBFImage> video)
317        {
318                this.annotatedScenes = new ArrayList<AnnotatedVideoScene>();
319
320                // Create a shot detector as we're actually going to be processing
321                // shots, not the whole video
322                final CombiShotDetector vsd = new CombiShotDetector(video);
323                vsd.addVideoShotDetector(new HistogramVideoShotDetector(video), 1);
324                vsd.addVideoShotDetector(new LocalHistogramVideoShotDetector(video, 20), 1);
325
326                // Go through the frames in the video detecting shots
327                for (final MBFImage frame : video)
328                {
329                        // Used to determine whether this is a new scene.
330                        boolean newScene = false;
331
332                        // Process the frame with the shot detector
333                        // final OtsuThreshold o = new OtsuThreshold();
334                        // double t = (o.calculateThreshold( frame.getBand( 0 ) ) +
335                        // o.calculateThreshold( frame.getBand( 1 ) ) +
336                        // o.calculateThreshold( frame.getBand( 2 ) ) ) / 3;
337                        // t *= frame.getWidth() * frame.getHeight()/2;
338                        // System.out.println( t );
339                        // vsd.setThreshold( t );
340                        vsd.processFrame(frame);
341
342                        // TODO: Scene detection needs to be implemented. We use shots here.
343                        if (vsd.wasLastFrameBoundary() || this.currentScene == null)
344                                newScene = true;
345
346                        // If we are entering a new scene, then we finish off this current
347                        // scene and create a new one.
348                        if (newScene)
349                        {
350                                // First time around, currentScene will be null.
351                                if (this.currentScene != null)
352                                {
353                                        // If we already had a scene, we'll set its end
354                                        // time to be the previous frame processed.
355                                        this.currentScene.endOfScene = new HrsMinSecFrameTimecode(
356                                                        video.getCurrentFrameIndex() - 1, video.getFPS());
357
358                                        // Store the current annotations for the last scene
359                                        this.currentScene.annotations = new HashSet<SceneAnnotation>();
360                                        for (final VideoAnnotator<MBFImage, String> annotator : this.annotators)
361                                        {
362                                                final SceneAnnotation sc = new SceneAnnotation();
363                                                sc.scene = this.currentScene;
364                                                sc.annotatorClass = annotator.getClass();
365
366                                                // Store annotations
367                                                sc.annotations.addAll(annotator.getAnnotations());
368
369                                                // Add this scene annotation to the current scene
370                                                this.currentScene.annotations.add(sc);
371                                        }
372
373                                        System.out.println("Scene complete: ");
374
375                                        // Check the scene is long enough to be considered a scene.
376                                        if (this.currentScene.endOfScene.getTimecodeInMilliseconds() -
377                                                        this.currentScene.startOfScene.getTimecodeInMilliseconds()
378                                                        >= this.nMillisecondsShortestScene)
379                                                this.annotatedScenes.add(this.currentScene);
380                                        else
381                                                System.out.println("Scene discarded: Too short");
382                                }
383
384                                // Create the new scene to annotate
385                                this.currentScene = new AnnotatedVideoScene();
386                                this.currentScene.startOfScene = new HrsMinSecFrameTimecode(
387                                                video.getCurrentFrameIndex() - 1, video.getFPS());
388
389                                // Now move on to process the next shot
390                                this.resetAnnotators();
391                        }
392
393                        // If we've changed shot within this scene, then add the
394                        // shot to the scene
395                        if (vsd.wasLastFrameBoundary())
396                                this.currentScene.listOfShots.add(vsd.getLastShotBoundary());
397
398                        // Carry on annotating the scene with the next frame
399                        this.processFrame(frame);
400                }
401
402                vsd.close();
403
404                return this.annotatedScenes;
405        }
406
407        /**
408         * Reset the annotators being used
409         */
410        private void resetAnnotators()
411        {
412                for (final VideoAnnotator<?, ?> annotator : this.annotators)
413                        annotator.reset();
414        }
415
416        /**
417         * Process the given frame with the analysis annotators
418         *
419         * @param frame
420         *            the frame
421         */
422        private void processFrame(final MBFImage frame)
423        {
424                // Send the frame to each of the annotators
425                for (final VideoAnnotator<MBFImage, ?> annotator : this.annotators)
426                        annotator.processFrame(frame);
427        }
428
429        /**
430         * Set the shortest length of scene that will be stored.
431         *
432         * @param ms
433         *            The number of milliseconds
434         */
435        public void setShortestSceneLength(final double ms)
436        {
437                this.nMillisecondsShortestScene = ms;
438        }
439
440        /**
441         * @param args
442         */
443        public static void main(final String[] args)
444        {
445                final VideoSceneAnnotationToolOptions vsato =
446                                new VideoSceneAnnotationToolOptions();
447                final CmdLineParser parser = new CmdLineParser(vsato);
448                try
449                {
450                        parser.parseArgument(args);
451                } catch (final CmdLineException e)
452                {
453                        System.out.println(e.getMessage());
454                        System.out.println();
455                        System.out.println("VideoSceneAnnotationTool FILE [options]");
456                        parser.printUsage(System.out);
457                        System.exit(1);
458                }
459
460                // Create the tool
461                final VideoSceneAnnotationTool vsa = new VideoSceneAnnotationTool();
462                vsa.setShortestSceneLength(vsato.nMillisecondsShortestScene);
463
464                // Setup the tool - add some annotators
465                vsa.addVideoAnnotator(new FaceShotTypeAnnotator());
466
467                // Create the video to process and then process it
468                final XuggleVideo video = new XuggleVideo(vsato.inputFile);
469                final List<AnnotatedVideoScene> finalScenes = vsa.processVideo(video);
470
471                System.out.println("\n\n\n=============================================");
472                System.out.println("=============================================");
473                System.out.println("Final List of Scenes: ");
474                System.out.println(finalScenes);
475                System.out.println("=============================================");
476        }
477}