Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d3cc497
Add operation to save image snapshots to local disk.
PeterJohnson Apr 23, 2016
745ec6e
Fix up SaveImageOperation based on PR comments.
PeterJohnson May 7, 2016
2eb2d38
Merge remote-tracking branch 'origin/master' into image-snapshots
AustinShalit May 28, 2016
d5a7d9d
Add FileManager
AustinShalit Jun 9, 2016
e40464a
Update GRIP log directory
AustinShalit Jun 9, 2016
625710f
Update SaveImageOperation to use FileManager
AustinShalit Jun 9, 2016
aae316c
Fix failing tests
AustinShalit Jun 9, 2016
0e95afb
Merge remote-tracking branch 'origin/master' into image-snapshots
AustinShalit Jun 9, 2016
cfe1ffa
Remove deprecated objects
AustinShalit Jun 9, 2016
7a38983
Add documentation
AustinShalit Jun 9, 2016
5d37d7f
Use Milliseconds
AustinShalit Jun 9, 2016
f48fcc6
Update Log Directory
AustinShalit Jun 9, 2016
b352d9a
Add SaveImageOperationTests
AustinShalit Jun 23, 2016
411718e
Merge remote-tracking branch 'origin/master' into image-snapshots
AustinShalit Jun 23, 2016
532bb07
Update field scope
AustinShalit Jun 25, 2016
fb8590b
Reverse commit "Update field scope"
AustinShalit Jun 25, 2016
3365bf5
add filetype selector
AustinShalit Jun 25, 2016
cfbe494
Merge remote-tracking branch 'origin/master' into image-snapshots
AustinShalit Jun 25, 2016
3dbb57c
Changes based on comments
AustinShalit Jun 27, 2016
31d56ba
Merge remote-tracking branch 'origin/master' into image-snapshots
AustinShalit Jul 1, 2016
2a13575
Remove injection from test
AustinShalit Jul 1, 2016
67fe91f
Change enum styling
AustinShalit Jul 1, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class Operations {
new OperationMetaData(PublishVideoOperation.DESCRIPTION, () -> new PublishVideoOperation(isf)),
new OperationMetaData(ResizeOperation.DESCRIPTION, () -> new ResizeOperation(isf, osf)),
new OperationMetaData(RGBThresholdOperation.DESCRIPTION, () -> new RGBThresholdOperation(isf, osf)),
new OperationMetaData(SaveImageOperation.DESCRIPTION, () -> new SaveImageOperation(isf, osf)),
new OperationMetaData(SwitchOperation.DESCRIPTION, () -> new SwitchOperation(isf, osf)),
new OperationMetaData(ValveOperation.DESCRIPTION, () -> new ValveOperation(isf, osf)),
new OperationMetaData(WatershedOperation.DESCRIPTION, () -> new WatershedOperation(isf, osf)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package edu.wpi.grip.core.operations.composite;

import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import edu.wpi.grip.core.OperationDescription;
import edu.wpi.grip.core.sockets.InputSocket;
import edu.wpi.grip.core.Operation;
import edu.wpi.grip.core.sockets.OutputSocket;
import edu.wpi.grip.core.sockets.SocketHint;
import edu.wpi.grip.core.sockets.SocketHints;
import edu.wpi.grip.core.util.Icon;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.IntPointer;

import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.bytedeco.javacpp.opencv_core.Mat;
import static org.bytedeco.javacpp.opencv_imgcodecs.CV_IMWRITE_JPEG_QUALITY;
import static org.bytedeco.javacpp.opencv_imgcodecs.imencode;

/**
* Save JPEG files periodically to the local disk.
*/
public class SaveImageOperation implements Operation {

public static final OperationDescription DESCRIPTION =
OperationDescription.builder()
.name("Save Images to Disk")
.summary("Save image periodically to local disk")
.category(OperationDescription.Category.MISCELLANEOUS)
.icon(Icon.iconStream("publish-video"))
.build();

private static final Logger logger = Logger.getLogger(SaveImageOperation.class.getName());
private final SocketHint<Mat> inputHint = SocketHints.Inputs.createMatSocketHint("Input", false);
private final SocketHint<Number> qualityHint = SocketHints.Inputs.createNumberSliderSocketHint("Quality", 90, 0, 100);
private final SocketHint<Number> periodHint = SocketHints.Inputs.createNumberSpinnerSocketHint("Period", 0.1);
private final SocketHint<Boolean> activeHint = SocketHints.Inputs.createCheckboxSocketHint("Active", false);
private final SocketHint<String> prefixHint = SocketHints.Inputs.createTextSocketHint("Prefix", "./");
private final SocketHint<String> suffixHint = SocketHints.Inputs.createTextSocketHint("Suffix", ".jpg");

private final SocketHint<Mat> outputHint = SocketHints.Outputs.createMatSocketHint("Output");

private final InputSocket<Mat> inputSocket;
private final InputSocket<Number> qualitySocket;
private final InputSocket<Number> periodSocket;
private final InputSocket<Boolean> activeSocket;
private final InputSocket<String> prefixSocket;
private final InputSocket<String> suffixSocket;

private final OutputSocket<Mat> outputSocket;

private final Object imageLock = new Object();
private final BytePointer imagePointer = new BytePointer();
private Optional<Thread> saveThread = Optional.empty();
private Stopwatch stopwatch = Stopwatch.createStarted();
private int numSteps = 0;
private SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd_HHmmss.SSS");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java.util.Date is deprecated, so use java.time.format.DateTimeFormatter instead, e.g.
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyMMdd_HHmmss.SSS");

That format also looks a little wonky (I"m seeing stuff like 20160608_224640.606).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it to yyyyMMddHHmmssSSS. Let me know if you have a style you prefer.

private String prefix = "./";
private String suffix = ".jpg";

public SaveImageOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) {
inputSocket = inputSocketFactory.create(inputHint);
qualitySocket = inputSocketFactory.create(qualityHint);
periodSocket = inputSocketFactory.create(periodHint);
activeSocket = inputSocketFactory.create(activeHint);
prefixSocket = inputSocketFactory.create(prefixHint);
suffixSocket = inputSocketFactory.create(suffixHint);

outputSocket = outputSocketFactory.create(outputHint);

numSteps++;
if (!saveThread.isPresent()) {
saveThread = Optional.of(new Thread(runServer, "Image Saver"));
saveThread.get().setDaemon(true);
saveThread.get().start();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't start threads in the constructor.

}

/**
* Listens for incoming connections on port 1180 and writes JPEG data whenever there's a new frame.
*/
private final Runnable runServer = () -> {
// Loop forever (or at least until the thread is interrupted).
logger.info("Starting image saver");
byte[] buffer = new byte[128 * 1024];
int bufferSize;
while (!Thread.currentThread().isInterrupted()) {
try {
// Wait for the main thread to put a new image.
synchronized (imageLock) {
imageLock.wait();

// Copy the image data into a pre-allocated buffer, growing it if necessary
bufferSize = imagePointer.limit();
if (bufferSize > buffer.length) buffer = new byte[imagePointer.limit()];
imagePointer.get(buffer, 0, bufferSize);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}

// Write the file
try (FileOutputStream out = new FileOutputStream(prefix + format.format(new Date()) + suffix)) {
out.write(buffer, 0, bufferSize);
} catch (IOException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
logger.info("Shutting down image saver");
saveThread = Optional.empty();
};

@Override
public synchronized void cleanUp() {
if (--numSteps == 0) {
saveThread.ifPresent(Thread::interrupt);
}
}

@Override
public List<InputSocket> getInputSockets() {
return ImmutableList.of(
inputSocket,
qualitySocket,
periodSocket,
activeSocket,
prefixSocket,
suffixSocket
);
}

@Override
public List<OutputSocket> getOutputSockets() {
return ImmutableList.of(
outputSocket
);
}

@Override
public void perform() {
if (!activeSocket.getValue().orElse(false)) {
return;
}

if (!inputSocket.getValue().isPresent()) {
throw new IllegalArgumentException("Input image must not be empty");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary.
Step does this already.


// don't save new image until period expires
if (stopwatch.elapsed(TimeUnit.NANOSECONDS) < periodSocket.getValue().get().doubleValue()*1000000000L) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using nanoseconds? There's no real point if the file name has only millisecond precision.

return;
}
stopwatch.reset();
stopwatch.start();

synchronized (imageLock) {
imencode(".jpeg", inputSocket.getValue().get(), imagePointer, new IntPointer(CV_IMWRITE_JPEG_QUALITY,
qualitySocket.getValue().get().intValue()));
prefix = prefixSocket.getValue().get();
suffix = suffixSocket.getValue().get();
imageLock.notify();
}
}
}