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}