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}