View Javadoc

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 }