/*
* Copyright (c) 2011-2016, Peter Abeles. All Rights Reserved.
*
* This file is part of BoofCV (http://boofcv.org).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package boofcv.gui;
import boofcv.io.MediaManager;
import boofcv.io.PathLabel;
import boofcv.io.UtilIO;
import boofcv.io.image.ConvertBufferedImage;
import boofcv.io.image.SimpleImageSequence;
import boofcv.io.image.UtilImageIO;
import boofcv.io.wrapper.DefaultMediaManager;
import boofcv.struct.image.ImageBase;
import boofcv.struct.image.ImageType;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.List;
/**
* Provides some common basic functionality for demonstrations
*
* @author Peter Abeles
*/
public abstract class DemonstrationBase<T extends ImageBase> extends JPanel {
protected JMenuBar menuBar;
JMenuItem menuFile, menuWebcam, menuQuit;
final JFileChooser fc = new JFileChooser();
protected InputMethod inputMethod = InputMethod.NONE;
ImageSequenceThread sequenceThread;
volatile boolean waitingToOpenImage = false;
final Object waitingLock = new Object();
volatile boolean processRunning = false;
volatile boolean processRequested = false;
final Object processLock = new Object();
BufferedImage imageCopy0;
BufferedImage imageCopy1;
T boofCopy1;
protected ImageType<T> imageType;
T input;
BufferedImage inputBuffered;
protected MediaManager media = new DefaultMediaManager();
// If true then any stream will be paused. If a webcam is running it will skip new images
// if a video it will stop processing the input
protected volatile boolean streamPaused = false;
// minimum elapsed time between the each stream frame being processed, in milliseconds
protected volatile long streamPeriod = 30;
// File path to previously opened image or video. null if webcam
protected String inputFilePath;
protected boolean allowImages = true;
protected boolean allowVideos = true;
protected boolean allowWebcameras = true;
/**
* Constructor that specifies examples and input image type
*
* @param exampleInputs List of paths to examples. Either a String file path or {@link PathLabel}.
* @param imageType Type of image it's processing
*/
public DemonstrationBase(List<?> exampleInputs, ImageType<T> imageType) {
super(new BorderLayout());
createMenuBar(exampleInputs);
this.input = imageType.createImage(1,1);
this.imageType = imageType;
this.boofCopy1 = imageType.createImage(1,1);
}
private void createMenuBar(List<?> exampleInputs) {
menuBar = new JMenuBar();
JMenu menu = new JMenu("File");
menuBar.add(menu);
ActionListener listener = createActionListener();
menuFile = new JMenuItem("Open File", KeyEvent.VK_O);
menuFile.addActionListener(listener);
menuFile.setAccelerator(KeyStroke.getKeyStroke(
KeyEvent.VK_O, ActionEvent.CTRL_MASK));
menuWebcam = new JMenuItem("Open Webcam", KeyEvent.VK_W);
menuWebcam.addActionListener(listener);
menuWebcam.setAccelerator(KeyStroke.getKeyStroke(
KeyEvent.VK_W, ActionEvent.CTRL_MASK));
menuQuit = new JMenuItem("Quit", KeyEvent.VK_Q);
menuQuit.addActionListener(listener);
menuQuit.setAccelerator(KeyStroke.getKeyStroke(
KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
menu.add(menuFile);
menu.add(menuWebcam);
menu.addSeparator();
menu.add(menuQuit);
menu = new JMenu("Examples");
menuBar.add(menu);
for (final Object o : exampleInputs ) {
final String path,name;
if( o instanceof PathLabel ) {
path = ((PathLabel)o).getPath();
name = ((PathLabel)o).getLabel();
} else if( o instanceof String ){
path = (String)o;
name = new File((String)o).getName();
} else {
throw new IllegalArgumentException("Example must be a PathLabel or a String path");
}
JMenuItem menuItem = new JMenuItem( name );
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
openFile(new File(path));
}
});
menu.add(menuItem);
}
add(BorderLayout.NORTH, menuBar);
}
/**
* Override to be notified when the input has changed. This is also a good location to change the default
* max FPS for streaming data. It will be 0 for webcam and 30 FPS for videos
*
* @param method Type of input source
* @param width Width of input image
* @param height Height of input image
*/
protected void handleInputChange( InputMethod method , int width , int height ) {
}
/**
* Process the image. Will be called in its own thread, but doesn't need to be re-entrant. If image
* is null then reprocess the previous image.
*/
public abstract void processImage(final BufferedImage buffered , final T input );
protected void processImageThread( final BufferedImage buffered , final T input ) {
// See if there is already a thread running that's processing an image. If so copy
// the new image into storage for the new image that is to be processed after it finishes
// processing the current image
synchronized (processLock) {
if(processRunning) {
if( buffered != null ) {
imageCopy1 = checkCopyBuffered(buffered, imageCopy1);
boofCopy1.setTo(input);
} else
imageCopy1 = null;
processRequested = true;
return;
} else {
processRunning = true;
processRequested = false;
if( buffered != null )
imageCopy0 = checkCopyBuffered(buffered, imageCopy0);
else
imageCopy0 = null;
}
}
new Thread() {
@Override
public void run() {
try {
while (true) {
synchronized (waitingLock) {
waitingToOpenImage = false;
}
if (imageCopy0 != null)
processImage(imageCopy0, input);
else
processImage(null, null);
synchronized (processLock) {
if (!processRequested) {
processRunning = false;
break;
}
processRequested = false;
if (imageCopy1 != null) {
imageCopy0 = checkCopyBuffered(imageCopy1, imageCopy0);
input.setTo(boofCopy1);
} else
imageCopy0 = null;
}
}
} catch( RuntimeException e ) {
e.printStackTrace();
System.out.println(e.getMessage());
System.out.println("Thread crashed! If possible, saving image to crashed_image.png");
if( imageCopy0 != null )
UtilImageIO.saveImage(imageCopy0,"crashed_image.png");
synchronized (processLock) {
processRunning = false;
}
}
}
}.start();
}
private BufferedImage checkCopyBuffered(BufferedImage src, BufferedImage dst) {
dst = conditionalDeclare(src, dst);
dst.createGraphics().drawImage(src,0,0,null);
return dst;
}
public static BufferedImage conditionalDeclare(BufferedImage template, BufferedImage output) {
if( output == null ||
output.getWidth() != template.getWidth() ||
output.getHeight() != template.getHeight() ) {
int type;
if( output == null ) {
type = template.getType();
if (type == 0) {
type = BufferedImage.TYPE_INT_RGB;
}
} else {
type = output.getType();
}
output = new BufferedImage(template.getWidth(),template.getHeight(),type);
}
return output;
}
public static BufferedImage conditionalDeclare(BufferedImage template, BufferedImage output, int type ) {
if( output == null ||
output.getWidth() != template.getWidth() ||
output.getHeight() != template.getHeight() ) {
output = new BufferedImage(template.getWidth(),template.getHeight(),type);
}
return output;
}
private void stopPreviousInput() {
switch ( inputMethod ) {
case WEBCAM: stopSequenceRunning("Shutting down webcam");break;
case VIDEO: stopSequenceRunning("Shutting down video");break;
}
}
private void stopSequenceRunning( String message ) {
sequenceThread.requestStop = true;
WaitingThread waiting = new WaitingThread(message);
waiting.start();
while( sequenceThread.running ) {
Thread.yield();
}
waiting.closeRequested = true;
}
/**
* Opens a file. First it will attempt to open it as an image. If that fails it will try opening it as a
* video. If all else fails tell the user it has failed. If a streaming source was running before it will
* be stopped.
*/
public void openFile(File file) {
// maybe it's an example file
if( !file.exists() ) {
file = new File(UtilIO.pathExample(file.getPath()));
}
if( !file.exists() ) {
System.err.println("Can't find file "+file.getPath());
return;
}
synchronized (waitingLock) {
if (waitingToOpenImage) {
System.out.println("Waiting to open an image");
return;
}
waitingToOpenImage = true;
}
stopPreviousInput();
String filePath = file.getPath();
inputFilePath = filePath;
// mjpegs can be opened up as images. so override the default behavior
BufferedImage buffered = filePath.endsWith("mjpeg") ? null : UtilImageIO.loadImage(filePath);
if( buffered == null ) {
if( !allowVideos ) {
showRejectDiaglog("Can't process video files");
return;
}
openVideo(filePath);
} else {
if( !allowImages ) {
showRejectDiaglog("Can't process images");
return;
}
inputMethod = InputMethod.IMAGE;
input.reshape(buffered.getWidth(),buffered.getHeight());
inputBuffered = buffered;
ConvertBufferedImage.convertFrom(buffered,input,true);
handleInputChange(inputMethod,buffered.getWidth(),buffered.getHeight());
processImageThread(buffered, input);
}
}
/**
* Before invoking this function make sure waitingToOpenImage is false AND that the previous input has beens topped
*/
private void openVideo(String filePath) {
SimpleImageSequence<T> sequence = media.openVideo(filePath, imageType);
if( sequence != null ) {
inputMethod = InputMethod.VIDEO;
streamPeriod = 33; // default to 33 FPS for a video
sequenceThread = new ImageSequenceThread(sequence);
sequenceThread.start();
} else {
inputMethod = InputMethod.NONE;
waitingToOpenImage = false;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JOptionPane.showMessageDialog(DemonstrationBase.this, "Can't open file");
}
});
}
}
/**
* waits until the processing thread is done.
*/
public void waitUntilDoneProcessing() {
while( processRunning ) {
Thread.yield();
}
}
public void openWebcam() {
if( !allowWebcameras ) {
showRejectDiaglog("Can't process webcams");
return;
}
synchronized (waitingLock) {
if (waitingToOpenImage)
return;
waitingToOpenImage = true;
}
stopPreviousInput();
inputFilePath = null;
inputMethod = InputMethod.WEBCAM;
streamPeriod = 0; // default to no delay in processing for a real time stream
new WaitingThread("Opening Webcam").start();
new Thread() {
public void run() {
SimpleImageSequence<T> sequence = media.openCamera(null,640,480,imageType);
if(sequence != null) {
sequenceThread = new ImageSequenceThread(sequence);
sequenceThread.start();
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JOptionPane.showMessageDialog(DemonstrationBase.this, "Failed to open webcam");
}
});
}
}
}.start();
}
/**
* Displays a dialog box letting the user know it can't perform the requested action
*/
private void showRejectDiaglog( String message ) {
JOptionPane.showMessageDialog(null, message);
}
private ActionListener createActionListener() {
return new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (menuFile == e.getSource()) {
int returnVal = fc.showOpenDialog(DemonstrationBase.this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
openFile(file);
} else {
}
} else if (menuWebcam == e.getSource()) {
openWebcam();
} else if (menuQuit == e.getSource()) {
System.exit(0);
}
}
};
}
class WaitingThread extends Thread {
ProgressMonitor progress;
public volatile boolean closeRequested = false;
public WaitingThread( String message ) {
progress = new ProgressMonitor(DemonstrationBase.this,message,"", 0, 100);
}
@Override
public void run() {
while( waitingToOpenImage && !closeRequested ) {
SwingUtilities.invokeLater(new Runnable() { @Override public void run() { progress.setProgress(0); } });
try {Thread.sleep(250);} catch (InterruptedException ignore) {}
}
SwingUtilities.invokeLater(
new Runnable() { @Override public void run() { progress.close(); } }
);
}
}
class ImageSequenceThread extends Thread {
boolean requestStop = false;
boolean running = true;
SimpleImageSequence<T> sequence;
public ImageSequenceThread(SimpleImageSequence<T> sequence) {
this.sequence = sequence;
}
@Override
public void run() {
handleInputChange(inputMethod,sequence.getNextWidth(),sequence.getNextHeight());
long before = System.currentTimeMillis();
while( !requestStop && sequence.hasNext() ) {
T input = sequence.next();
// if it's a webcam and paused, just don't process the video frame
boolean skipFrame = streamPaused && inputMethod == InputMethod.WEBCAM;
if( input == null ) {
break;
} else if( skipFrame ) {
// do nothing just don't process it
before = System.currentTimeMillis();
} else {
BufferedImage buffered = sequence.getGuiImage();
processImageThread(buffered,input);
if( streamPeriod > 0 ) {
long time = Math.max(0, streamPeriod -(System.currentTimeMillis()-before));
if( time > 0 ) {
try {Thread.sleep(time);} catch (InterruptedException ignore) {}
} else {
try {Thread.sleep(5);} catch (InterruptedException ignore) {}
}
} else {
try {Thread.sleep(5);} catch (InterruptedException ignore) {}
}
before = System.currentTimeMillis();
}
// If paused and is a video, do thing until unpaused or a stop is requested
if( streamPaused && inputMethod == InputMethod.VIDEO ) {
while( streamPaused && !requestStop ) {
try {Thread.sleep(5);} catch (InterruptedException ignore) {}
}
}
}
sequence.close();
running = false;
}
}
/**
* If just a single image was processed it will process it again. If it's a stream
* there is no need to reprocess, the next image will be handled soon enough.
*/
public void reprocessSingleImage() {
if( sequenceThread == null ) {
// hmm if it's reprocessing the last image in a sequence this might not work
processImageThread(inputBuffered, input);
}
}
/**
* If the current input source is a video it will reload it from the start
*/
public void replayVideo() {
if( inputMethod == InputMethod.VIDEO ) {
synchronized (waitingLock) {
if (waitingToOpenImage) {
System.out.println("Waiting to open an image");
return;
}
waitingToOpenImage = true;
}
stopPreviousInput();
openVideo(inputFilePath);
}
}
protected enum InputMethod {
NONE,
IMAGE,
VIDEO,
WEBCAM
}
}