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.video.processing.shotdetector;
034
035import gnu.trove.list.array.TDoubleArrayList;
036
037import java.awt.HeadlessException;
038import java.util.ArrayList;
039import java.util.List;
040
041import org.openimaj.feature.DoubleFV;
042import org.openimaj.image.Image;
043import org.openimaj.video.Video;
044import org.openimaj.video.VideoDisplay;
045import org.openimaj.video.VideoDisplay.EndAction;
046import org.openimaj.video.VideoDisplayListener;
047import org.openimaj.video.processor.VideoProcessor;
048import org.openimaj.video.timecode.HrsMinSecFrameTimecode;
049import org.openimaj.video.timecode.VideoTimecode;
050
051/**
052 *      Video shot detector class implemented as a video display listener. This
053 *      means that shots can be detected as the video plays. The class also
054 *      supports direct processing of a video file (with no display).
055 *      <p>
056 *      The default threshold boundary should be set by implementing methods
057 *      as the distances returned by those implementations will only sensibly understand
058 *      where the threshold should be.
059 *      <p>
060 *      Only the last keyframe is stored during processing, so if you want to store
061 *      a list of keyframes you must store this list yourself by listening to the
062 *      ShotDetected event which provides a VideoKeyframe which has a timecode
063 *      and an image. Each event will receive the same VideoKeyframe instance
064 *      containing different information. Use VideoKeyframe#clone() to make a copy.
065 *
066 *  @author David Dupplaw (dpd@ecs.soton.ac.uk)
067 *      @param <I> The type of image
068 *      @created 1 Jun 2011
069 */
070public abstract class VideoShotDetector<I extends Image<?,I>>
071        extends VideoProcessor<I>
072        implements VideoDisplayListener<I>
073{
074        /** The current keyframe */
075        private VideoKeyframe<I> currentKeyframe = null;
076
077        /** The list of shot boundaries */
078        private final List<ShotBoundary<I>> shotBoundaries =
079                new ArrayList<ShotBoundary<I>>();
080
081        /** Differences between consecutive frames */
082        private final TDoubleArrayList differentials = new TDoubleArrayList();
083
084        /** The frame we're at within the video */
085        private int frameCounter = 0;
086
087        /** The video being processed */
088        private Video<I> video = null;
089
090        /** Whether to find keyframes */
091        private boolean findKeyframes = true;
092
093        /** Whether to store all frame differentials */
094        private boolean storeAllDiffs = false;
095
096        /** Whether an event is required to be fired next time */
097        private boolean needFire = false;
098
099        /** Whether the last processed frame was a boundary */
100        protected boolean lastFrameWasBoundary = false;
101
102        /** A list of the listeners that want to know about new shots */
103        private final List<ShotDetectedListener<I>> listeners = new ArrayList<ShotDetectedListener<I>>();
104
105        /** The number of frames per second of the source material */
106        private double fps = 25;
107
108        /** Whether the first frame is being processed */
109        private boolean firstFrame = true;
110
111        /** Whether to generate a shot boundary for the first frame of a video */
112        private final boolean generateStartShot = true;
113
114        /** The threshold to use to determine a shot boundary - this default is arbitrary */
115        protected double threshold = 100;
116
117        /**
118         *      This constructor assumes that you will set the number of
119         *      frames per second your video uses (using {@link #setFPS(double)})
120         *      when you know what that will be, otherwise your timecodes will
121         *      all be messed up.
122         */
123        public VideoShotDetector()
124        {
125        }
126
127        /**
128         *      Default constructor that allows the processor to be used ad-hoc
129         *      on frames from any source. The number of FPS is required so that
130         *      timecodes can be generated for the shot boundaries. Be aware that if
131         *      your source material does not have a specific number of frames per
132         *      second then the timecodes will not have any meaning in the detected
133         *      shot boundaries.
134         *
135         *      @param fps The number of frames per second of the source material
136         */
137        public VideoShotDetector( final double fps )
138        {
139                this.fps = fps;
140        }
141
142        /**
143         *      Constructor that takes the video file to process.
144         *
145         *  @param video The video to process.
146         */
147        public VideoShotDetector( final Video<I> video )
148        {
149                this( video, false );
150        }
151
152        /**
153         *      Default constructor that takes the video file to process and
154         *      whether or not to display the video as it's being processed.
155         *
156         *  @param video The video to process
157         *  @param display Whether to display the video during processing.
158         */
159        public VideoShotDetector( final Video<I> video, final boolean display )
160    {
161                this.video = video;
162                this.fps = video.getFPS();
163                if( display )
164                {
165                        try
166                {
167                        final VideoDisplay<I> vd = VideoDisplay.createVideoDisplay( video );
168                                vd.addVideoListener( this );
169                                vd.setEndAction( EndAction.STOP_AT_END );
170                }
171                catch( final HeadlessException e )
172                {
173                        e.printStackTrace();
174                }
175                }
176    }
177
178        /**
179         *      Returns whether the last processed frame was a shot boundary - that is
180         *      the last processed frame marks a new scene.
181         *      @return Whether the last frame was a boundary.
182         */
183        public boolean wasLastFrameBoundary()
184        {
185                return this.lastFrameWasBoundary;
186        }
187
188        /**
189         *      Process the video.
190         */
191        @Override
192        public void process()
193        {
194                super.process( this.video );
195        }
196
197        /**
198         *  {@inheritDoc}
199         *  @see org.openimaj.video.VideoDisplayListener#afterUpdate(org.openimaj.video.VideoDisplay)
200         */
201        @Override
202        public void afterUpdate( final VideoDisplay<I> display )
203    {
204    }
205
206        /**
207         *  {@inheritDoc}
208         *  @see org.openimaj.video.VideoDisplayListener#beforeUpdate(org.openimaj.image.Image)
209         */
210        @Override
211        public void beforeUpdate( final I frame )
212    {
213                this.checkForShotBoundary( frame );
214    }
215
216        /**
217         *      Add the given shot detected listener to the list of listeners in this
218         *      object
219         *
220         *  @param sdl The shot detected listener to add
221         */
222        public void addShotDetectedListener( final ShotDetectedListener<I> sdl )
223        {
224                this.listeners.add( sdl );
225        }
226
227        /**
228         *      Remove the given shot detected listener from this object.
229         *
230         *  @param sdl The shot detected listener to remove
231         */
232        public void removeShotDetectedListener( final ShotDetectedListener<I> sdl )
233        {
234                this.listeners.remove( sdl );
235        }
236
237        /**
238         *      Return the last shot boundary in the list.
239         *      @return The last shot boundary in the list.
240         */
241        public ShotBoundary<I> getLastShotBoundary()
242        {
243                if( this.shotBoundaries.size() == 0 )
244                        return null;
245                return this.shotBoundaries.get( this.shotBoundaries.size()-1 );
246        }
247
248        /**
249         *      Returns the last video keyframe that was generated.
250         *      @return The last video keyframe that was generated.
251         */
252        public VideoKeyframe<I> getLastKeyframe()
253        {
254                return this.currentKeyframe;
255        }
256
257        /**
258         *      Checks whether a shot boundary occurred between the given frame
259         *      and the previous frame, and if so, it will add a shot boundary
260         *      to the shot boundary list.
261         *
262         *  @param frame The new frame to process.
263         */
264        private void checkForShotBoundary( final I frame )
265        {
266                this.lastFrameWasBoundary = false;
267                final double dist = this.getInterframeDistance( frame );
268
269                if( this.storeAllDiffs )
270                {
271                        this.differentials.add( dist );
272                        this.fireDifferentialCalculated( new HrsMinSecFrameTimecode(
273                                        this.frameCounter, this.video.getFPS() ), dist, frame );
274                }
275
276//              System.out.println( "is "+dist+" > "+this.threshold+"? "+(dist>this.threshold) );
277
278                // We generate a shot boundary if the threshold is exceeded or we're
279                // at the very start of the video.
280                if( dist > this.threshold || (this.generateStartShot && this.firstFrame) )
281                {
282                        this.needFire = true;
283
284                        // The timecode of this frame
285                        final VideoTimecode tc = new HrsMinSecFrameTimecode(
286                                        this.frameCounter, this.fps );
287
288                        // The last shot boundary we created
289                        final ShotBoundary<I> sb = this.getLastShotBoundary();
290
291                        // If this frame is sequential to the last
292                        if( sb != null &&
293                                tc.getFrameNumber() - sb.getTimecode().getFrameNumber() < 4  )
294                        {
295                                // If the shot boundary is a fade, we simply change the end
296                                // timecode, otherwise we replace the given shot boundary
297                                // with a new one.
298                                if( sb instanceof FadeShotBoundary )
299                                                ((FadeShotBoundary<I>)sb).setEndTimecode( tc );
300                                else
301                                {
302                                        // Remove the old one.
303                                        this.shotBoundaries.remove( sb );
304
305                                        // Change it to a fade.
306                                        final FadeShotBoundary<I> fsb = new FadeShotBoundary<I>( sb );
307                                        fsb.setEndTimecode( tc );
308
309                                        this.lastFrameWasBoundary = true;
310
311                                        if( this.findKeyframes )
312                                        {
313                                                if( this.currentKeyframe == null )
314                                                        this.currentKeyframe = new VideoKeyframe<I>( tc, frame );
315                                                else
316                                                {
317                                                        this.currentKeyframe.timecode = tc;
318                                                        this.currentKeyframe.imageAtBoundary = frame.clone();
319                                                }
320                                                fsb.keyframe = this.currentKeyframe.clone();
321                                        }
322
323                                        this.shotBoundaries.add( fsb );
324                                }
325                        }
326                        else
327                        {
328                                // Create a new shot boundary
329                                final ShotBoundary<I> sb2 = new ShotBoundary<I>( tc );
330
331                                if( this.findKeyframes )
332                                {
333                                        if( this.currentKeyframe == null )
334                                                this.currentKeyframe = new VideoKeyframe<I>( tc, frame );
335                                        else
336                                        {
337                                                this.currentKeyframe.timecode = tc;
338                                                this.currentKeyframe.imageAtBoundary = frame;
339                                        }
340                                        sb2.keyframe = this.currentKeyframe.clone();
341                                }
342
343                                this.lastFrameWasBoundary = true;
344                                this.shotBoundaries.add( sb2 );
345                                this.fireShotDetected( sb2, this.currentKeyframe );
346                        }
347                }
348                else
349                {
350                        // The frame matches with the last (no boundary) but we'll check whether
351                        // the last thing added to the shot boundaries was a fade and its
352                        // end time was the timecode before this one. If so, we can fire a
353                        // shot detected event.
354                        if( this.frameCounter > 0 && this.needFire )
355                        {
356                                this.needFire = false;
357
358                                final VideoTimecode tc = new HrsMinSecFrameTimecode(
359                                                this.frameCounter-1, this.fps );
360
361                                final ShotBoundary<I> lastShot = this.getLastShotBoundary();
362
363                                if( lastShot != null && lastShot instanceof FadeShotBoundary )
364                                        if( ((FadeShotBoundary<I>)lastShot).getEndTimecode().equals( tc ) )
365                                                this.fireShotDetected( lastShot, this.getLastKeyframe() );
366                        }
367                }
368
369                this.frameCounter++;
370                this.firstFrame = false;
371    }
372
373        /**
374         *      Returns the inter-frame distance between this frame and the last.
375         *      @return The inter-frame distance
376         */
377        protected abstract double getInterframeDistance( I thisFrame );
378
379        /**
380         *      Get the list of shot boundaries that have been extracted so far.
381         *  @return The list of shot boundaries.
382         */
383        public List<ShotBoundary<I>> getShotBoundaries()
384        {
385                return this.shotBoundaries;
386        }
387
388        /**
389         *      Set the threshold that will determine a shot boundary.
390         *
391         *  @param threshold The new threshold.
392         */
393        public void setThreshold( final double threshold )
394        {
395                this.threshold = threshold;
396        }
397
398        /**
399         *      Returns the current threshold value.
400         *      @return The current threshold
401         */
402        public double getThreshold()
403        {
404                return this.threshold;
405        }
406
407        /**
408         *      Set whether to store keyframes of boundaries when they
409         *      have been found.
410         *
411         *      @param k TRUE to store keyframes; FALSE otherwise
412         */
413        public void setFindKeyframes( final boolean k )
414        {
415                this.findKeyframes = k;
416        }
417
418        /**
419         *      Set whether to store differentials during the processing
420         *      stage.
421         *
422         *      @param d TRUE to store all differentials; FALSE otherwise
423         */
424        public void setStoreAllDifferentials( final boolean d )
425        {
426                this.storeAllDiffs = d;
427        }
428
429        /**
430         *      Get the differentials between frames (if storeAllDiff is true).
431         *      @return The differentials between frames as a List of Double.
432         */
433        public DoubleFV getDifferentials()
434        {
435                return new DoubleFV( this.differentials.toArray() );
436        }
437
438        /**
439         *  {@inheritDoc}
440         *  @see org.openimaj.video.processor.VideoProcessor#processFrame(org.openimaj.image.Image)
441         */
442        @Override
443    public I processFrame( final I frame )
444    {
445                if( frame == null ) return null;
446                this.checkForShotBoundary( frame );
447                return frame;
448    }
449
450        /**
451         *      Fire the event to the listeners that a new shot has been detected.
452         *  @param sb The shot boundary defintion
453         *  @param vk The video keyframe
454         */
455        protected void fireShotDetected( final ShotBoundary<I> sb, final VideoKeyframe<I> vk )
456        {
457                for( final ShotDetectedListener<I> sdl : this.listeners )
458                        sdl.shotDetected( sb, vk );
459        }
460
461        /**
462         *      Fired each time a differential is calculated between frames.
463         *      @param vt The timecode of the differential
464         *      @param d The differential value
465         *      @param frame The different frame
466         */
467        protected void fireDifferentialCalculated( final VideoTimecode vt, final double d, final I frame )
468        {
469                for( final ShotDetectedListener<I> sdl : this.listeners )
470                        sdl.differentialCalculated( vt, d, frame );
471        }
472
473        /**
474         *      {@inheritDoc}
475         *      @see org.openimaj.video.processor.VideoProcessor#reset()
476         */
477        @Override
478        public void reset()
479        {
480        }
481
482        /**
483         *      Set the frames per second value for the video being processed.
484         *      @param fps The number of frames per second.
485         */
486        public void setFPS( final double fps )
487        {
488                this.fps = fps;
489        }
490}