-
Notifications
You must be signed in to change notification settings - Fork 109
[V2] Add operation to save image snapshots to local disk. #599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
d3cc497
745ec6e
2eb2d38
d5a7d9d
e40464a
625710f
aae316c
0e95afb
cfe1ffa
7a38983
5d37d7f
f48fcc6
b352d9a
411718e
532bb07
fb8590b
3365bf5
cfbe494
3dbb57c
31d56ba
2a13575
67fe91f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package edu.wpi.grip.core; | ||
|
|
||
| /** | ||
| * Defines what methods a FileManager implementation must implement. | ||
| */ | ||
| public interface FileManager { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation |
||
|
|
||
| /** | ||
| * Saves an array of bytes to a file. | ||
| * | ||
| * @param image The image to save | ||
| * @param fileName The file name to save | ||
| */ | ||
| void saveImage(byte[] image, String fileName); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package edu.wpi.grip.core; | ||
|
|
||
| import com.google.common.io.Files; | ||
| import com.google.inject.Singleton; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| import static com.google.common.base.Preconditions.checkNotNull; | ||
|
|
||
| /** | ||
| * Implementation of {@code FileManager}. Saves files into a directory named GRIP in the user's | ||
| * home folder. | ||
| */ | ||
| @Singleton | ||
| public class GripFileManager implements FileManager { | ||
|
|
||
| private static final Logger logger = Logger.getLogger(GripFileManager.class.getName()); | ||
|
|
||
| public static final File GRIP_DIRECTORY | ||
| = new File(System.getProperty("user.home") + File.separator + "GRIP"); | ||
| public static final File IMAGE_DIRECTORY = new File(GRIP_DIRECTORY, "images"); | ||
|
|
||
| @Override | ||
| public void saveImage(byte[] image, String fileName) { | ||
| checkNotNull(image); | ||
| checkNotNull(fileName); | ||
|
||
|
|
||
| File file = new File(IMAGE_DIRECTORY, fileName); | ||
| Runnable runnable = () -> { | ||
| try { | ||
| IMAGE_DIRECTORY.mkdirs(); // If the user deletes the directory | ||
| Files.write(image, file); | ||
| } catch (IOException ex) { | ||
| logger.log(Level.WARNING, ex.getMessage(), ex); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this not pass the exception up to the caller?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point it is in a different thread. How could I handle that?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I suppose this doesn't make sense. |
||
| } | ||
| }; | ||
| new Thread(runnable).start(); | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package edu.wpi.grip.core; | ||
|
|
||
| import com.google.inject.AbstractModule; | ||
|
|
||
| public class GripFileModule extends AbstractModule { | ||
|
|
||
| @Override | ||
| protected void configure() { | ||
| bind(FileManager.class).to(GripFileManager.class); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| package edu.wpi.grip.core.operations.composite; | ||
|
|
||
| import edu.wpi.grip.core.FileManager; | ||
| import edu.wpi.grip.core.Operation; | ||
| import edu.wpi.grip.core.OperationDescription; | ||
| import edu.wpi.grip.core.sockets.InputSocket; | ||
| 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 com.google.common.base.Stopwatch; | ||
| import com.google.common.collect.ImmutableList; | ||
|
|
||
| import org.bytedeco.javacpp.BytePointer; | ||
| import org.bytedeco.javacpp.IntPointer; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.List; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| 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 final SocketHint<Mat> inputHint | ||
| = SocketHints.Inputs.createMatSocketHint("Input", false); | ||
| private final SocketHint<FileTypes> fileTypeHint | ||
| = SocketHints.createEnumSocketHint("File type", FileTypes.jpeg); | ||
| 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<Mat> outputHint = SocketHints.Outputs.createMatSocketHint("Output"); | ||
|
|
||
| private final InputSocket<Mat> inputSocket; | ||
| private final InputSocket<FileTypes> fileTypesSocket; | ||
| private final InputSocket<Number> qualitySocket; | ||
| private final InputSocket<Number> periodSocket; | ||
| private final InputSocket<Boolean> activeSocket; | ||
|
|
||
| private final OutputSocket<Mat> outputSocket; | ||
|
|
||
| private final FileManager fileManager; | ||
| private final BytePointer imagePointer = new BytePointer(); | ||
| private final Stopwatch stopwatch = Stopwatch.createStarted(); | ||
| private final DateTimeFormatter formatter | ||
| = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SSS"); | ||
|
|
||
| private enum FileTypes { | ||
| jpeg, png | ||
|
||
| } | ||
|
|
||
| @SuppressWarnings("JavadocMethod") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine |
||
| public SaveImageOperation(InputSocket.Factory inputSocketFactory, | ||
| OutputSocket.Factory outputSocketFactory, FileManager fileManager) { | ||
|
||
| this.fileManager = fileManager; | ||
|
|
||
| inputSocket = inputSocketFactory.create(inputHint); | ||
| fileTypesSocket = inputSocketFactory.create(fileTypeHint); | ||
| qualitySocket = inputSocketFactory.create(qualityHint); | ||
| periodSocket = inputSocketFactory.create(periodHint); | ||
| activeSocket = inputSocketFactory.create(activeHint); | ||
|
|
||
| outputSocket = outputSocketFactory.create(outputHint); | ||
| } | ||
|
|
||
| @Override | ||
| public List<InputSocket> getInputSockets() { | ||
| return ImmutableList.of( | ||
| inputSocket, | ||
| fileTypesSocket, | ||
| qualitySocket, | ||
| periodSocket, | ||
| activeSocket | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public List<OutputSocket> getOutputSockets() { | ||
| return ImmutableList.of( | ||
| outputSocket | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public void perform() { | ||
| if (!activeSocket.getValue().orElse(false)) { | ||
| return; | ||
| } | ||
|
|
||
| // don't save new image until period expires | ||
| if (stopwatch.elapsed(TimeUnit.MILLISECONDS) | ||
| < periodSocket.getValue().get().doubleValue() * 1000L) { | ||
| return; | ||
| } | ||
| stopwatch.reset(); | ||
| stopwatch.start(); | ||
|
|
||
| imencode("." + fileTypesSocket.getValue().get(), inputSocket.getValue().get(), imagePointer, | ||
| new IntPointer(CV_IMWRITE_JPEG_QUALITY, qualitySocket.getValue().get().intValue())); | ||
| byte[] buffer = new byte[128 * 1024]; | ||
| int bufferSize = imagePointer.limit(); | ||
| if (bufferSize > buffer.length) { | ||
| buffer = new byte[imagePointer.limit()]; | ||
| } | ||
| imagePointer.get(buffer, 0, bufferSize); | ||
|
|
||
| fileManager.saveImage(buffer, LocalDateTime.now().format(formatter) | ||
| + "." + fileTypesSocket.getValue().get()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package edu.wpi.grip.core.composite; | ||
|
|
||
|
|
||
| import edu.wpi.grip.core.FileManager; | ||
| import edu.wpi.grip.core.operations.composite.SaveImageOperation; | ||
| import edu.wpi.grip.core.sockets.InputSocket; | ||
| import edu.wpi.grip.core.sockets.OutputSocket; | ||
| import edu.wpi.grip.util.GripCoreTestModule; | ||
|
|
||
| import com.google.inject.Guice; | ||
| import com.google.inject.Inject; | ||
| import com.google.inject.Injector; | ||
|
|
||
| import org.junit.After; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
|
|
||
| import static org.junit.Assert.assertFalse; | ||
|
|
||
| public class SaveImageOperationTest { | ||
|
|
||
| private GripCoreTestModule testModule; | ||
|
|
||
| private InputSocket<Boolean> activeSocket; | ||
|
|
||
| @Inject | ||
| private InputSocket.Factory isf; | ||
|
|
||
| @Inject | ||
| private OutputSocket.Factory osf; | ||
|
|
||
| @Inject | ||
| private FileManager fileManager; | ||
|
|
||
| @Before | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine. |
||
| public void setUp() throws Exception { | ||
| testModule = new GripCoreTestModule(); | ||
| testModule.setUp(); | ||
|
|
||
| final Injector injector = Guice.createInjector(testModule); | ||
| injector.injectMembers(this); | ||
| SaveImageOperation operation = new SaveImageOperation(isf, osf, fileManager); | ||
| activeSocket = operation.getInputSockets().stream().filter( | ||
| o -> o.getSocketHint().getIdentifier().equals("Active") | ||
| && o.getSocketHint().getType().equals(Boolean.class)).findFirst().get(); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you really need dependency injection for this?? There is a MockInputSocketFactory and a MockOutputSocketFactory you can use.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe I am using a real FileManager. I will remove the injection |
||
|
|
||
| @After | ||
| public void tearDown() { | ||
| testModule.tearDown(); | ||
| } | ||
|
|
||
| @Test | ||
| public void testActiveButtonDefaultsDisabled() { | ||
| assertFalse("The active socket was not false (disabled).", activeSocket.getValue().get()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package edu.wpi.grip.core.util; | ||
|
|
||
| import edu.wpi.grip.core.FileManager; | ||
|
|
||
| public class MockFileManager implements FileManager { | ||
|
|
||
| @Override | ||
| public void saveImage(byte[] image, String fileName) { | ||
| // No body here because this is for testing only. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good! Thank you! Nice job! |
||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the definition of an interface. What is a FileManager though? That's what you are trying to explain here.