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.gt; 034 035import java.awt.Dimension; 036import java.awt.GridBagConstraints; 037import java.awt.GridBagLayout; 038import java.awt.Insets; 039import java.awt.event.ActionEvent; 040import java.awt.event.ActionListener; 041import java.awt.event.ItemEvent; 042import java.awt.event.ItemListener; 043import java.util.ArrayList; 044import java.util.HashSet; 045import java.util.List; 046import java.util.Set; 047 048import javax.swing.AbstractButton; 049import javax.swing.ImageIcon; 050import javax.swing.JFrame; 051import javax.swing.JLabel; 052import javax.swing.JPanel; 053import javax.swing.JToggleButton; 054 055import org.openimaj.audio.AudioStream; 056import org.openimaj.data.dataset.Dataset; 057import org.openimaj.data.identity.Identifiable; 058import org.openimaj.demos.sandbox.video.gt.VideoGroundTruth.IdentifiableVideoFrame; 059import org.openimaj.demos.sandbox.video.gt.VideoGroundTruth.IdentifierProducer; 060import org.openimaj.demos.sandbox.video.gt.VideoGroundTruth.StateProvider; 061import org.openimaj.image.ImageUtilities; 062import org.openimaj.image.MBFImage; 063import org.openimaj.image.processing.resize.ResizeProcessor; 064import org.openimaj.video.Video; 065import org.openimaj.video.VideoDisplay; 066import org.openimaj.video.VideoDisplayListener; 067import org.openimaj.video.processing.shotdetector.HistogramVideoShotDetector; 068import org.openimaj.video.timecode.HrsMinSecFrameTimecode; 069import org.openimaj.video.timecode.VideoTimecode; 070 071/** 072 * A tool for annotating scenes within videos. This class provides the methods 073 * and UI for doing shot detection (using a {@link HistogramVideoShotDetector}), 074 * and then adding annotations to the detected scenes. The tags for annotation 075 * are provided by subclasses of this class through the {@link #getStates()} 076 * method. Annotated scenes are added to a {@link Dataset} via the 077 * {@link VideoGroundTruth} class. 078 * 079 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 080 * @created 5 Dec 2012 081 * @version $Author$, $Revision$, $Date$ 082 */ 083public abstract class SceneLabellingVideoAnnotationTool extends JPanel 084 implements IdentifierProducer, StateProvider 085{ 086 /** */ 087 private static final long serialVersionUID = 1L; 088 089 /** The video ground truth writer */ 090 private VideoGroundTruth groundTruth = null; 091 092 /** The shot detector to use to analyse the video */ 093 private HistogramVideoShotDetector shotDetector = null; 094 095 /** A list of the detected shot boundaries */ 096 private final Set<IdentifiableVideoFrame> shotBoundaries = 097 new HashSet<IdentifiableVideoFrame>(); 098 099 /** The last video shot boundary that was detected */ 100 private IdentifiableVideoFrame lastShotBoundary = null; 101 102 /** The video being processed */ 103 private final Video<MBFImage> video; 104 105 /** The JLabel that shows the last boundary frame */ 106 private final JLabel lastShotBoundaryFrameLabel = new JLabel(); 107 108 /** The JLabel that shows the last boundary timecode */ 109 private final JLabel lastShotBoundaryTimecodeLabel = new JLabel(); 110 111 /** The timecode of the previous frame to be processed */ 112 private VideoTimecode previousFrameTimecode = null; 113 114 /** The frame for this UI */ 115 private JFrame frame; 116 117 /** The list of state radio buttons */ 118 private AbstractButton[] radioButtons = null; 119 120 /** A button group for the states */ 121 private final ButtonGroup stateButtonGroup = new ButtonGroup(); 122 123 /** The currently selected state */ 124 private List<String> currentState; 125 126 /** 127 * Determines whether the tags are singular (more than one cannot be 128 * selected at once) 129 */ 130 private final boolean exclusiveTags = true; 131 132 /** 133 * Returns a list of the states that are able to be labelled for any one 134 * scene. 135 * 136 * @return The list of states 137 */ 138 public abstract String[] getStates(); 139 140 /** 141 * @param video 142 * @param audio 143 */ 144 public SceneLabellingVideoAnnotationTool(final Video<MBFImage> video, 145 final AudioStream audio) 146 { 147 this.video = video; 148 this.groundTruth = new VideoGroundTruth(video, audio, this); 149 this.shotDetector = new HistogramVideoShotDetector(video); 150 this.currentState = new ArrayList<String>(); 151 this.init(); 152 } 153 154 /** 155 * {@inheritDoc} 156 * 157 * @see org.openimaj.demos.sandbox.video.gt.VideoGroundTruth.StateProvider#getCurrentState(org.openimaj.data.identity.Identifiable) 158 */ 159 @Override 160 public List<String> getCurrentState(final Identifiable id) 161 { 162 if (this.currentState == null) 163 return null; 164 165 final List<String> list = new ArrayList<String>(); 166 list.addAll(this.currentState); 167 return list; 168 } 169 170 /** 171 * {@inheritDoc} 172 * 173 * @see org.openimaj.demos.sandbox.video.gt.VideoGroundTruth.IdentifierProducer#getIdentifiers() 174 */ 175 @Override 176 public List<Identifiable> getIdentifiers() 177 { 178 final List<Identifiable> list = new ArrayList<Identifiable>(); 179 list.add(this.lastShotBoundary); 180 return list; 181 } 182 183 /** 184 * Set up the UI 185 */ 186 private void init() 187 { 188 this.setLayout(new GridBagLayout()); 189 190 // Set up a default constraints. We'll change this as we add 191 // bits and bobs to the UI 192 final GridBagConstraints gbc = new GridBagConstraints(); 193 gbc.gridx = gbc.gridy = 0; 194 gbc.gridwidth = 1; 195 gbc.fill = GridBagConstraints.BOTH; 196 gbc.weightx = gbc.weighty = 0; 197 198 // Add the label that shows the last shot boundary frame 199 gbc.insets = new Insets(4, 4, 4, 4); 200 gbc.weightx = 1; 201 this.add(this.lastShotBoundaryFrameLabel, gbc); 202 203 // Add the label that shows the timecode for the last shot boundary 204 gbc.gridx++; 205 gbc.weightx = 1; 206 final Dimension d = this.lastShotBoundaryTimecodeLabel.getPreferredSize(); 207 d.width = 300; 208 this.lastShotBoundaryTimecodeLabel.setPreferredSize(d); 209 this.add(this.lastShotBoundaryTimecodeLabel, gbc); 210 211 // Now add on radio buttons for all the possible states 212 gbc.gridwidth = 2; 213 gbc.gridx = 0; 214 gbc.insets = new Insets(1, 1, 1, 1); 215 final String[] states = this.getStates(); 216 this.radioButtons = new AbstractButton[states.length]; 217 for (int i = 0; i < states.length; i++) 218 { 219 gbc.gridy++; 220 this.add(this.radioButtons[i] = 221 new JToggleButton(states[i]), gbc); 222 223 // If only one tag can be selected at a time, then the button group 224 // itemListener will be used to change the currentState member 225 // This itemListener is added immediately after this loop. 226 // Otherwise, we use an actionListener on the button to add or 227 // remove the state represented by the button to the currentState 228 // field. Use setExclusiveTags() to determine this option. 229 if (this.exclusiveTags) 230 this.stateButtonGroup.add(this.radioButtons[i]); 231 else 232 { 233 final int f = i; 234 this.radioButtons[i].addActionListener(new ActionListener() 235 { 236 @Override 237 public void actionPerformed(final ActionEvent e) 238 { 239 if (SceneLabellingVideoAnnotationTool.this. 240 radioButtons[f].isSelected()) 241 SceneLabellingVideoAnnotationTool.this. 242 currentState.add(SceneLabellingVideoAnnotationTool. 243 this.radioButtons[f].getText()); 244 else 245 SceneLabellingVideoAnnotationTool.this. 246 currentState.remove(SceneLabellingVideoAnnotationTool. 247 this.radioButtons[f].getText()); 248 } 249 }); 250 } 251 } 252 253 // Add an item listener that sets the current state when a button is 254 // pressed 255 // This is only used when the button group is used, and that's only used 256 // when only one tag can be selected for a scene. 257 this.stateButtonGroup.addItemListener(new ItemListener() 258 { 259 @Override 260 public void itemStateChanged(final ItemEvent e) 261 { 262 SceneLabellingVideoAnnotationTool.this.currentState.clear(); 263 SceneLabellingVideoAnnotationTool.this.currentState.add( 264 SceneLabellingVideoAnnotationTool.this. 265 stateButtonGroup.getSelected().getText()); 266 } 267 }); 268 269 // Set up the video player. 270 this.groundTruth.getVideoPlayer().setButtons(new String[] { "play", "pause" }); 271 this.groundTruth.getVideoPlayer().pause(); 272 this.groundTruth.getVideoPlayer().addVideoListener( 273 new VideoDisplayListener<MBFImage>() 274 { 275 @Override 276 public void beforeUpdate(final MBFImage frame) 277 { 278 SceneLabellingVideoAnnotationTool.this.processFrame(frame); 279 } 280 281 @Override 282 public void afterUpdate(final VideoDisplay<MBFImage> display) 283 { 284 } 285 }); 286 287 // Show the video player 288 final JFrame f = this.groundTruth.getVideoPlayer().showFrame(); 289 290 // Show the tool 291 final JFrame ff = this.showFrame(); 292 ff.setLocation(f.getLocation().x + f.getWidth(), f.getLocation().y); 293 ff.setSize(ff.getWidth(), f.getHeight()); 294 } 295 296 /** 297 * Show the UI frame if it's not already being shown. 298 * 299 * @return The UI frame 300 */ 301 private JFrame showFrame() 302 { 303 if (this.frame == null) 304 { 305 this.frame = new JFrame(); 306 this.frame.add(this); 307 this.frame.pack(); 308 } 309 310 this.frame.setVisible(true); 311 return this.frame; 312 } 313 314 /** 315 * Process a frame from the video 316 * 317 * @param frame 318 * The video frame 319 */ 320 protected void processFrame(final MBFImage frame) 321 { 322 // Pass the frame to our shot detector to see if the shot has changed. 323 this.shotDetector.processFrame(frame); 324 325 // Timecode for the current frame 326 final HrsMinSecFrameTimecode tc = new HrsMinSecFrameTimecode( 327 this.video.getCurrentFrameIndex(), this.video.getFPS()); 328 329 // If we're in to a new scene, we update the scene counter 330 if (this.shotDetector.wasLastFrameBoundary()) 331 { 332 // If we already have a shot that we're dealing with, then 333 // we'll create a region-based annotation for this shot and 334 // give it to the video ground truth thingy 335 if (this.lastShotBoundary != null) 336 { 337 // Add this as a data item to the ground truthed dataset 338 this.groundTruth.updateIdentifiableRegion(this.lastShotBoundary, 339 this.lastShotBoundary.timecode, this.previousFrameTimecode); 340 } 341 342 // New shot boundary here 343 this.lastShotBoundary = new IdentifiableVideoFrame(frame, tc); 344 345 // Store this shot boundary 346 this.shotBoundaries.add(this.lastShotBoundary); 347 348 // Update the labels in the UI 349 this.lastShotBoundaryFrameLabel.setIcon( 350 new ImageIcon(ImageUtilities.createBufferedImage( 351 frame.process(new ResizeProcessor(200, 200, true))))); 352 this.lastShotBoundaryTimecodeLabel.setText(tc.toString()); 353 354 // Check whether there's a default state for our state buttons 355 final String defaultState = this.getDefaultState(); 356 357 // Reset all the radio buttons - we don't want this new scene 358 // to have the settings from the last scene 359 for (final AbstractButton button : this.radioButtons) 360 { 361 if (defaultState == null || !button.getText().equals(defaultState)) 362 button.setSelected(false); 363 else 364 button.setSelected(true); 365 } 366 367 this.currentState = null; 368 369 // Make sure the UI gets updated 370 this.repaint(); 371 } 372 373 // Remember this timecode for the next processing loop 374 this.previousFrameTimecode = tc; 375 } 376 377 /** 378 * Returns the default state to be set for each new scene. The method can 379 * return null to indicate that no default state should be used. If it 380 * returns a state, it should return a value that matches one of those 381 * returned by {@link #getStates()}. 382 * 383 * @return The default state 384 */ 385 public String getDefaultState() 386 { 387 return null; 388 } 389 390 /** 391 * Determines whether tags are exclusive (only one can be selected at a 392 * time). 393 * 394 * @param tf 395 * Whether tags can be selected or not. 396 */ 397 public void setExclusiveTags(final boolean tf) 398 { 399 this.stateButtonGroup.clear(); 400 if (tf) 401 for (final AbstractButton b : this.radioButtons) 402 this.stateButtonGroup.add(b); 403 } 404}