1 /**
2 * Copyright (c) 2011, The University of Southampton and the individual contributors.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without modification,
6 * are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 *
11 * * Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * * Neither the name of the University of Southampton nor the names of its
16 * contributors may be used to endorse or promote products derived from this
17 * software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 /**
31 *
32 */
33 package org.openimaj.video.processing.shotdetector;
34
35 import gnu.trove.list.array.TDoubleArrayList;
36
37 import java.awt.HeadlessException;
38 import java.util.ArrayList;
39 import java.util.List;
40
41 import org.openimaj.feature.DoubleFV;
42 import org.openimaj.image.Image;
43 import org.openimaj.video.Video;
44 import org.openimaj.video.VideoDisplay;
45 import org.openimaj.video.VideoDisplay.EndAction;
46 import org.openimaj.video.VideoDisplayListener;
47 import org.openimaj.video.processor.VideoProcessor;
48 import org.openimaj.video.timecode.HrsMinSecFrameTimecode;
49 import org.openimaj.video.timecode.VideoTimecode;
50
51 /**
52 * Video shot detector class implemented as a video display listener. This
53 * means that shots can be detected as the video plays. The class also
54 * supports direct processing of a video file (with no display).
55 * <p>
56 * The default threshold boundary should be set by implementing methods
57 * as the distances returned by those implementations will only sensibly understand
58 * where the threshold should be.
59 * <p>
60 * Only the last keyframe is stored during processing, so if you want to store
61 * a list of keyframes you must store this list yourself by listening to the
62 * ShotDetected event which provides a VideoKeyframe which has a timecode
63 * and an image. Each event will receive the same VideoKeyframe instance
64 * containing different information. Use VideoKeyframe#clone() to make a copy.
65 *
66 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
67 * @param <I> The type of image
68 * @created 1 Jun 2011
69 */
70 public abstract class VideoShotDetector<I extends Image<?,I>>
71 extends VideoProcessor<I>
72 implements VideoDisplayListener<I>
73 {
74 /** The current keyframe */
75 private VideoKeyframe<I> currentKeyframe = null;
76
77 /** The list of shot boundaries */
78 private final List<ShotBoundary<I>> shotBoundaries =
79 new ArrayList<ShotBoundary<I>>();
80
81 /** Differences between consecutive frames */
82 private final TDoubleArrayList differentials = new TDoubleArrayList();
83
84 /** The frame we're at within the video */
85 private int frameCounter = 0;
86
87 /** The video being processed */
88 private Video<I> video = null;
89
90 /** Whether to find keyframes */
91 private boolean findKeyframes = true;
92
93 /** Whether to store all frame differentials */
94 private boolean storeAllDiffs = false;
95
96 /** Whether an event is required to be fired next time */
97 private boolean needFire = false;
98
99 /** 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 }