Skip to content

Commit 12ae9d6

Browse files
AustinShalitJLLeitschuh
authored andcommitted
[V2] Add operation to save image snapshots to local disk. (#599)
* Add operation to save image snapshots to local disk. The period and path prefix and suffix can be set, and the operation enabled and disabled at will (this is particularly useful in combination with PR #572).
1 parent 9ce19be commit 12ae9d6

File tree

11 files changed

+273
-4
lines changed

11 files changed

+273
-4
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package edu.wpi.grip.core;
2+
3+
/**
4+
* A FileManager saves images to disk.
5+
*/
6+
public interface FileManager {
7+
8+
/**
9+
* Saves an array of bytes to a file.
10+
*
11+
* @param image The image to save
12+
* @param fileName The file name to save
13+
*/
14+
void saveImage(byte[] image, String fileName);
15+
}

core/src/main/java/edu/wpi/grip/core/GripCoreModule.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ public class GripCoreModule extends AbstractModule {
5656
globalLogger.removeHandler(handler);
5757
}
5858

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

6163
//Set level to handler and logger
6264
fileHandler.setLevel(Level.FINE);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package edu.wpi.grip.core;
2+
3+
import com.google.common.io.Files;
4+
import com.google.inject.Singleton;
5+
6+
import java.io.File;
7+
import java.io.IOException;
8+
import java.util.logging.Level;
9+
import java.util.logging.Logger;
10+
11+
import static com.google.common.base.Preconditions.checkNotNull;
12+
13+
/**
14+
* Implementation of {@code FileManager}. Saves files into a directory named GRIP in the user's
15+
* home folder.
16+
*/
17+
@Singleton
18+
public class GripFileManager implements FileManager {
19+
20+
private static final Logger logger = Logger.getLogger(GripFileManager.class.getName());
21+
22+
public static final File GRIP_DIRECTORY
23+
= new File(System.getProperty("user.home") + File.separator + "GRIP");
24+
public static final File IMAGE_DIRECTORY = new File(GRIP_DIRECTORY, "images");
25+
26+
@Override
27+
public void saveImage(byte[] image, String fileName) {
28+
checkNotNull(image, "An image was not provided to the FileManager");
29+
checkNotNull(fileName, "A filename was not provided to the FileManager");
30+
31+
File file = new File(IMAGE_DIRECTORY, fileName);
32+
Thread thread = new Thread(() -> {
33+
try {
34+
IMAGE_DIRECTORY.mkdirs(); // If the user deletes the directory
35+
Files.write(image, file);
36+
} catch (IOException ex) {
37+
logger.log(Level.WARNING, ex.getMessage(), ex);
38+
}
39+
});
40+
thread.setDaemon(true);
41+
thread.start();
42+
}
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package edu.wpi.grip.core;
2+
3+
import com.google.inject.AbstractModule;
4+
5+
public class GripFileModule extends AbstractModule {
6+
7+
@Override
8+
protected void configure() {
9+
bind(FileManager.class).to(GripFileManager.class);
10+
}
11+
12+
}

core/src/main/java/edu/wpi/grip/core/operations/Operations.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package edu.wpi.grip.core.operations;
22

3+
import edu.wpi.grip.core.FileManager;
34
import edu.wpi.grip.core.OperationMetaData;
45
import edu.wpi.grip.core.events.OperationAddedEvent;
56
import edu.wpi.grip.core.operations.composite.BlobsReport;
@@ -21,6 +22,7 @@
2122
import edu.wpi.grip.core.operations.composite.PublishVideoOperation;
2223
import edu.wpi.grip.core.operations.composite.RGBThresholdOperation;
2324
import edu.wpi.grip.core.operations.composite.ResizeOperation;
25+
import edu.wpi.grip.core.operations.composite.SaveImageOperation;
2426
import edu.wpi.grip.core.operations.composite.SwitchOperation;
2527
import edu.wpi.grip.core.operations.composite.ThresholdMoving;
2628
import edu.wpi.grip.core.operations.composite.ValveOperation;
@@ -65,12 +67,14 @@ public class Operations {
6567
@Named("ntManager") MapNetworkPublisherFactory ntPublisherFactory,
6668
@Named("httpManager") MapNetworkPublisherFactory httpPublishFactory,
6769
@Named("rosManager") ROSNetworkPublisherFactory rosPublishFactory,
70+
FileManager fileManager,
6871
InputSocket.Factory isf,
6972
OutputSocket.Factory osf) {
7073
this.eventBus = checkNotNull(eventBus, "EventBus cannot be null");
7174
checkNotNull(ntPublisherFactory, "ntPublisherFactory cannot be null");
7275
checkNotNull(httpPublishFactory, "httpPublisherFactory cannot be null");
7376
checkNotNull(rosPublishFactory, "rosPublishFactory cannot be null");
77+
checkNotNull(fileManager, "fileManager cannot be null");
7478
this.operations = ImmutableList.of(
7579
// Composite operations
7680
new OperationMetaData(BlurOperation.DESCRIPTION,
@@ -105,6 +109,8 @@ public class Operations {
105109
() -> new ResizeOperation(isf, osf)),
106110
new OperationMetaData(RGBThresholdOperation.DESCRIPTION,
107111
() -> new RGBThresholdOperation(isf, osf)),
112+
new OperationMetaData(SaveImageOperation.DESCRIPTION,
113+
() -> new SaveImageOperation(isf, osf, fileManager)),
108114
new OperationMetaData(SwitchOperation.DESCRIPTION,
109115
() -> new SwitchOperation(isf, osf)),
110116
new OperationMetaData(ValveOperation.DESCRIPTION,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package edu.wpi.grip.core.operations.composite;
2+
3+
import edu.wpi.grip.core.FileManager;
4+
import edu.wpi.grip.core.Operation;
5+
import edu.wpi.grip.core.OperationDescription;
6+
import edu.wpi.grip.core.sockets.InputSocket;
7+
import edu.wpi.grip.core.sockets.OutputSocket;
8+
import edu.wpi.grip.core.sockets.SocketHint;
9+
import edu.wpi.grip.core.sockets.SocketHints;
10+
import edu.wpi.grip.core.util.Icon;
11+
12+
import com.google.common.base.Stopwatch;
13+
import com.google.common.collect.ImmutableList;
14+
15+
import org.bytedeco.javacpp.BytePointer;
16+
import org.bytedeco.javacpp.IntPointer;
17+
18+
import java.time.LocalDateTime;
19+
import java.time.format.DateTimeFormatter;
20+
import java.util.List;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import static org.bytedeco.javacpp.opencv_core.Mat;
24+
import static org.bytedeco.javacpp.opencv_imgcodecs.CV_IMWRITE_JPEG_QUALITY;
25+
import static org.bytedeco.javacpp.opencv_imgcodecs.imencode;
26+
27+
/**
28+
* Save JPEG files periodically to the local disk.
29+
*/
30+
public class SaveImageOperation implements Operation {
31+
32+
public static final OperationDescription DESCRIPTION =
33+
OperationDescription.builder()
34+
.name("Save Images to Disk")
35+
.summary("Save image periodically to local disk")
36+
.category(OperationDescription.Category.MISCELLANEOUS)
37+
.icon(Icon.iconStream("publish-video"))
38+
.build();
39+
40+
private final SocketHint<Mat> inputHint
41+
= SocketHints.Inputs.createMatSocketHint("Input", false);
42+
private final SocketHint<FileTypes> fileTypeHint
43+
= SocketHints.createEnumSocketHint("File type", FileTypes.JPEG);
44+
private final SocketHint<Number> qualityHint
45+
= SocketHints.Inputs.createNumberSliderSocketHint("Quality", 90, 0, 100);
46+
private final SocketHint<Number> periodHint
47+
= SocketHints.Inputs.createNumberSpinnerSocketHint("Period", 0.1);
48+
private final SocketHint<Boolean> activeHint
49+
= SocketHints.Inputs.createCheckboxSocketHint("Active", false);
50+
51+
private final SocketHint<Mat> outputHint = SocketHints.Outputs.createMatSocketHint("Output");
52+
53+
private final InputSocket<Mat> inputSocket;
54+
private final InputSocket<FileTypes> fileTypesSocket;
55+
private final InputSocket<Number> qualitySocket;
56+
private final InputSocket<Number> periodSocket;
57+
private final InputSocket<Boolean> activeSocket;
58+
59+
private final OutputSocket<Mat> outputSocket;
60+
61+
private final FileManager fileManager;
62+
private final BytePointer imagePointer = new BytePointer();
63+
private final Stopwatch stopwatch = Stopwatch.createStarted();
64+
private final DateTimeFormatter formatter
65+
= DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SSS");
66+
67+
private enum FileTypes {
68+
JPEG, PNG;
69+
70+
@Override
71+
public String toString() {
72+
return super.toString().toLowerCase();
73+
}
74+
}
75+
76+
@SuppressWarnings("JavadocMethod")
77+
public SaveImageOperation(InputSocket.Factory inputSocketFactory,
78+
OutputSocket.Factory outputSocketFactory,
79+
FileManager fileManager) {
80+
this.fileManager = fileManager;
81+
82+
inputSocket = inputSocketFactory.create(inputHint);
83+
fileTypesSocket = inputSocketFactory.create(fileTypeHint);
84+
qualitySocket = inputSocketFactory.create(qualityHint);
85+
periodSocket = inputSocketFactory.create(periodHint);
86+
activeSocket = inputSocketFactory.create(activeHint);
87+
88+
outputSocket = outputSocketFactory.create(outputHint);
89+
}
90+
91+
@Override
92+
public List<InputSocket> getInputSockets() {
93+
return ImmutableList.of(
94+
inputSocket,
95+
fileTypesSocket,
96+
qualitySocket,
97+
periodSocket,
98+
activeSocket
99+
);
100+
}
101+
102+
@Override
103+
public List<OutputSocket> getOutputSockets() {
104+
return ImmutableList.of(
105+
outputSocket
106+
);
107+
}
108+
109+
@Override
110+
public void perform() {
111+
if (!activeSocket.getValue().orElse(false)) {
112+
return;
113+
}
114+
115+
// don't save new image until period expires
116+
if (stopwatch.elapsed(TimeUnit.MILLISECONDS)
117+
< periodSocket.getValue().get().doubleValue() * 1000L) {
118+
return;
119+
}
120+
stopwatch.reset();
121+
stopwatch.start();
122+
123+
imencode("." + fileTypesSocket.getValue().get(), inputSocket.getValue().get(), imagePointer,
124+
new IntPointer(CV_IMWRITE_JPEG_QUALITY, qualitySocket.getValue().get().intValue()));
125+
byte[] buffer = new byte[128 * 1024];
126+
int bufferSize = imagePointer.limit();
127+
if (bufferSize > buffer.length) {
128+
buffer = new byte[imagePointer.limit()];
129+
}
130+
imagePointer.get(buffer, 0, bufferSize);
131+
132+
fileManager.saveImage(buffer, LocalDateTime.now().format(formatter)
133+
+ "." + fileTypesSocket.getValue().get());
134+
}
135+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package edu.wpi.grip.core.composite;
2+
3+
4+
import edu.wpi.grip.core.operations.composite.SaveImageOperation;
5+
import edu.wpi.grip.core.sockets.InputSocket;
6+
import edu.wpi.grip.core.sockets.MockInputSocketFactory;
7+
import edu.wpi.grip.core.sockets.MockOutputSocketFactory;
8+
import edu.wpi.grip.core.util.MockFileManager;
9+
10+
import com.google.common.eventbus.EventBus;
11+
12+
import org.junit.Before;
13+
import org.junit.Test;
14+
15+
import static org.junit.Assert.assertFalse;
16+
17+
public class SaveImageOperationTest {
18+
19+
private InputSocket<Boolean> activeSocket;
20+
21+
@Before
22+
public void setUp() throws Exception {
23+
EventBus eventBus = new EventBus();
24+
SaveImageOperation operation = new SaveImageOperation(new MockInputSocketFactory(eventBus),
25+
new MockOutputSocketFactory(eventBus), new MockFileManager());
26+
activeSocket = operation.getInputSockets().stream().filter(
27+
o -> o.getSocketHint().getIdentifier().equals("Active")
28+
&& o.getSocketHint().getType().equals(Boolean.class)).findFirst().get();
29+
}
30+
31+
@Test
32+
public void testActiveButtonDefaultsDisabled() {
33+
assertFalse("The active socket was not false (disabled).", activeSocket.getValue().get());
34+
}
35+
}

core/src/test/java/edu/wpi/grip/core/operations/OperationsFactory.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package edu.wpi.grip.core.operations;
22

33

4+
import edu.wpi.grip.core.FileManager;
45
import edu.wpi.grip.core.operations.network.MapNetworkPublisherFactory;
56
import edu.wpi.grip.core.operations.network.MockMapNetworkPublisher;
67
import edu.wpi.grip.core.operations.network.ros.JavaToMessageConverter;
@@ -10,6 +11,7 @@
1011
import edu.wpi.grip.core.sockets.MockInputSocketFactory;
1112
import edu.wpi.grip.core.sockets.MockOutputSocketFactory;
1213
import edu.wpi.grip.core.sockets.OutputSocket;
14+
import edu.wpi.grip.core.util.MockFileManager;
1315

1416
import com.google.common.eventbus.EventBus;
1517

@@ -22,6 +24,7 @@ public static Operations create(EventBus eventBus) {
2224
MockMapNetworkPublisher::new,
2325
MockMapNetworkPublisher::new,
2426
MockROSMessagePublisher::new,
27+
new MockFileManager(),
2528
new MockInputSocketFactory(eventBus),
2629
new MockOutputSocketFactory(eventBus));
2730
}
@@ -30,9 +33,10 @@ public static Operations create(EventBus eventBus,
3033
MapNetworkPublisherFactory mapFactory,
3134
MapNetworkPublisherFactory httpFactory,
3235
ROSNetworkPublisherFactory rosFactory,
36+
FileManager fileManager,
3337
InputSocket.Factory isf,
3438
OutputSocket.Factory osf) {
35-
return new Operations(eventBus, mapFactory, httpFactory, rosFactory, isf, osf);
39+
return new Operations(eventBus, mapFactory, httpFactory, rosFactory, fileManager, isf, osf);
3640
}
3741

3842
public static CVOperations createCV(EventBus eventBus) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package edu.wpi.grip.core.util;
2+
3+
import edu.wpi.grip.core.FileManager;
4+
5+
public class MockFileManager implements FileManager {
6+
7+
@Override
8+
public void saveImage(byte[] image, String fileName) {
9+
// No body here because this is for testing only.
10+
}
11+
12+
}

core/src/test/java/edu/wpi/grip/util/GripCoreTestModule.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package edu.wpi.grip.util;
22

33

4+
import edu.wpi.grip.core.FileManager;
45
import edu.wpi.grip.core.GripCoreModule;
56
import edu.wpi.grip.core.http.GripServer;
67
import edu.wpi.grip.core.http.GripServerTest;
78
import edu.wpi.grip.core.sources.CameraSource;
89
import edu.wpi.grip.core.sources.MockFrameGrabberFactory;
10+
import edu.wpi.grip.core.util.MockFileManager;
911

1012
import com.google.common.eventbus.SubscriberExceptionContext;
1113

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

0 commit comments

Comments
 (0)