Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 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
15 changes: 15 additions & 0 deletions core/src/main/java/edu/wpi/grip/core/FileManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.wpi.grip.core;

/**
* A FileManager saves images to disk.
*/
Copy link
Member

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.

public interface FileManager {
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
4 changes: 3 additions & 1 deletion core/src/main/java/edu/wpi/grip/core/GripCoreModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ public class GripCoreModule extends AbstractModule {
globalLogger.removeHandler(handler);
}

final Handler fileHandler = new FileHandler("%h/GRIP.log"); //Log to the file "GRIPlogger.log"
GripFileManager.GRIP_DIRECTORY.mkdirs();
final Handler fileHandler
= new FileHandler(GripFileManager.GRIP_DIRECTORY.getPath() + "/GRIP.log");

//Set level to handler and logger
fileHandler.setLevel(Level.FINE);
Expand Down
43 changes: 43 additions & 0 deletions core/src/main/java/edu/wpi/grip/core/GripFileManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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, "An image was not provided to the FileManager");
checkNotNull(fileName, "A filename was not provided to the FileManager");

File file = new File(IMAGE_DIRECTORY, fileName);
Thread thread = new Thread(() -> {
try {
IMAGE_DIRECTORY.mkdirs(); // If the user deletes the directory
Files.write(image, file);
} catch (IOException ex) {
logger.log(Level.WARNING, ex.getMessage(), ex);
Copy link
Member

Choose a reason for hiding this comment

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

Why does this not pass the exception up to the caller?

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I suppose this doesn't make sense.

}
});
thread.setDaemon(true);
thread.start();
}
}
12 changes: 12 additions & 0 deletions core/src/main/java/edu/wpi/grip/core/GripFileModule.java
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
@@ -1,5 +1,6 @@
package edu.wpi.grip.core.operations;

import edu.wpi.grip.core.FileManager;
import edu.wpi.grip.core.OperationMetaData;
import edu.wpi.grip.core.events.OperationAddedEvent;
import edu.wpi.grip.core.operations.composite.BlobsReport;
Expand All @@ -21,6 +22,7 @@
import edu.wpi.grip.core.operations.composite.PublishVideoOperation;
import edu.wpi.grip.core.operations.composite.RGBThresholdOperation;
import edu.wpi.grip.core.operations.composite.ResizeOperation;
import edu.wpi.grip.core.operations.composite.SaveImageOperation;
import edu.wpi.grip.core.operations.composite.SwitchOperation;
import edu.wpi.grip.core.operations.composite.ThresholdMoving;
import edu.wpi.grip.core.operations.composite.ValveOperation;
Expand Down Expand Up @@ -65,12 +67,14 @@ public class Operations {
@Named("ntManager") MapNetworkPublisherFactory ntPublisherFactory,
@Named("httpManager") MapNetworkPublisherFactory httpPublishFactory,
@Named("rosManager") ROSNetworkPublisherFactory rosPublishFactory,
FileManager fileManager,
InputSocket.Factory isf,
OutputSocket.Factory osf) {
this.eventBus = checkNotNull(eventBus, "EventBus cannot be null");
checkNotNull(ntPublisherFactory, "ntPublisherFactory cannot be null");
checkNotNull(httpPublishFactory, "httpPublisherFactory cannot be null");
checkNotNull(rosPublishFactory, "rosPublishFactory cannot be null");
checkNotNull(fileManager, "fileManager cannot be null");
this.operations = ImmutableList.of(
// Composite operations
new OperationMetaData(BlurOperation.DESCRIPTION,
Expand Down Expand Up @@ -105,6 +109,8 @@ public class Operations {
() -> new ResizeOperation(isf, osf)),
new OperationMetaData(RGBThresholdOperation.DESCRIPTION,
() -> new RGBThresholdOperation(isf, osf)),
new OperationMetaData(SaveImageOperation.DESCRIPTION,
() -> new SaveImageOperation(isf, osf, fileManager)),
new OperationMetaData(SwitchOperation.DESCRIPTION,
() -> new SwitchOperation(isf, osf)),
new OperationMetaData(ValveOperation.DESCRIPTION,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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
Copy link
Member

Choose a reason for hiding this comment

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

Usually enums are all caps. If you want to have the spinner value show up differently then override toString and it should work.

}

@SuppressWarnings("JavadocMethod")
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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();
}
Copy link
Member

Choose a reason for hiding this comment

The 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.
Also, why are you using a real file manager in a test? I really don't want tests writing to the file system.

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 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
@@ -1,6 +1,7 @@
package edu.wpi.grip.core.operations;


import edu.wpi.grip.core.FileManager;
import edu.wpi.grip.core.operations.network.MapNetworkPublisherFactory;
import edu.wpi.grip.core.operations.network.MockMapNetworkPublisher;
import edu.wpi.grip.core.operations.network.ros.JavaToMessageConverter;
Expand All @@ -10,6 +11,7 @@
import edu.wpi.grip.core.sockets.MockInputSocketFactory;
import edu.wpi.grip.core.sockets.MockOutputSocketFactory;
import edu.wpi.grip.core.sockets.OutputSocket;
import edu.wpi.grip.core.util.MockFileManager;

import com.google.common.eventbus.EventBus;

Expand All @@ -22,6 +24,7 @@ public static Operations create(EventBus eventBus) {
MockMapNetworkPublisher::new,
MockMapNetworkPublisher::new,
MockROSMessagePublisher::new,
new MockFileManager(),
new MockInputSocketFactory(eventBus),
new MockOutputSocketFactory(eventBus));
}
Expand All @@ -30,9 +33,10 @@ public static Operations create(EventBus eventBus,
MapNetworkPublisherFactory mapFactory,
MapNetworkPublisherFactory httpFactory,
ROSNetworkPublisherFactory rosFactory,
FileManager fileManager,
InputSocket.Factory isf,
OutputSocket.Factory osf) {
return new Operations(eventBus, mapFactory, httpFactory, rosFactory, isf, osf);
return new Operations(eventBus, mapFactory, httpFactory, rosFactory, fileManager, isf, osf);
}

public static CVOperations createCV(EventBus eventBus) {
Expand Down
12 changes: 12 additions & 0 deletions core/src/test/java/edu/wpi/grip/core/util/MockFileManager.java
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.
Copy link
Member

Choose a reason for hiding this comment

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

Good! Thank you! Nice job!

}

}
3 changes: 3 additions & 0 deletions core/src/test/java/edu/wpi/grip/util/GripCoreTestModule.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package edu.wpi.grip.util;


import edu.wpi.grip.core.FileManager;
import edu.wpi.grip.core.GripCoreModule;
import edu.wpi.grip.core.http.GripServer;
import edu.wpi.grip.core.http.GripServerTest;
import edu.wpi.grip.core.sources.CameraSource;
import edu.wpi.grip.core.sources.MockFrameGrabberFactory;
import edu.wpi.grip.core.util.MockFileManager;

import com.google.common.eventbus.SubscriberExceptionContext;

Expand Down Expand Up @@ -75,6 +77,7 @@ protected void configure() {
assert setUp : "The GripCoreTestModule handler was not set up. Call 'setUp' before passing "
+ "the injector";
bind(CameraSource.FrameGrabberFactory.class).to(MockFrameGrabberFactory.class);
bind(FileManager.class).to(MockFileManager.class);
super.configure();
// HTTP server injection bindings
bind(GripServer.JettyServerFactory.class).to(GripServerTest.TestServerFactory.class);
Expand Down
Loading