Skip to content

Commit

Permalink
Take Snapshots from PhotonClient (#940)
Browse files Browse the repository at this point in the history
  • Loading branch information
srimanachanta authored Oct 17, 2023
1 parent 1aa6bc8 commit ededc4f
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 60 deletions.
74 changes: 62 additions & 12 deletions photon-client/src/components/app/photon-camera-stream.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import loadingImage from "@/assets/images/loading.svg";
import type { StyleValue } from "vue/types/jsx";
import CvIcon from "@/components/common/cv-icon.vue";
const props = defineProps<{
streamType: "Raw" | "Processed";
id?: string;
}>();
const src = computed<string>(() => {
const streamSrc = computed<string>(() => {
const port =
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
Expand All @@ -20,25 +21,74 @@ const src = computed<string>(() => {
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
});
const alt = computed<string>(() => `${props.streamType} Stream View`);
const style = computed<StyleValue>(() => {
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
const streamStyle = computed<StyleValue>(() => {
if (useStateStore().colorPickingMode) {
return { cursor: "crosshair" };
} else if (src.value !== loadingImage) {
return { cursor: "pointer" };
return { width: "100%", cursor: "crosshair" };
} else if (streamSrc.value !== loadingImage) {
return { width: "100%", cursor: "pointer" };
}
return {};
return { width: "100%" };
});
const handleClick = () => {
if (!useStateStore().colorPickingMode && src.value !== loadingImage) {
window.open(src.value);
const overlayStyle = computed<StyleValue>(() => {
if (useStateStore().colorPickingMode || streamSrc.value == loadingImage) {
return { display: "none" };
} else {
return {};
}
});
const handleStreamClick = () => {
if (!useStateStore().colorPickingMode && streamSrc.value !== loadingImage) {
window.open(streamSrc.value);
}
};
const handleCaptureClick = () => {
if (props.streamType === "Raw") {
useCameraSettingsStore().saveInputSnapshot();
} else {
useCameraSettingsStore().saveOutputSnapshot();
}
};
</script>

<template>
<img :id="id" crossorigin="anonymous" :src="src" :alt="alt" :style="style" @click="handleClick" />
<div class="stream-container">
<img
:id="id"
crossorigin="anonymous"
:src="streamSrc"
:alt="streamDesc"
:style="streamStyle"
@click="handleStreamClick"
/>
<div class="stream-overlay" :style="overlayStyle">
<cv-icon
icon-name="mdi-camera-image"
tooltip="Capture and save a frame of this stream"
class="ma-1 mr-2"
@click="handleCaptureClick"
/>
</div>
</div>
</template>

<style scoped>
.stream-container {
position: relative;
}
.stream-overlay {
opacity: 0;
transition: 0.1s ease;
position: absolute;
top: 0;
right: 0;
}
.stream-container:hover .stream-overlay {
opacity: 1;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ const endCalibration = () => {
color="secondary"
style="width: 100%"
:disabled="!settingsValid"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot(true) : startCalibration()"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}
</v-btn>
Expand Down
6 changes: 3 additions & 3 deletions photon-client/src/components/cameras/CamerasView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const fpsTooLow = computed<boolean>(() => {
</script>

<template>
<v-card class="mb-3 pr-6 pb-3 pa-4" color="primary" dark>
<v-card class="mb-3 pb-3 pa-4" color="primary" dark>
<v-card-title
class="pb-0 mb-2 pl-4 pt-1"
style="min-height: 50px; justify-content: space-between; align-content: center"
Expand Down Expand Up @@ -133,16 +133,16 @@ th {
.stream {
display: flex;
justify-content: center;
width: 100%;
}

@media only screen and (min-width: 512px) and (max-width: 960px) {
.stream-container {
flex-wrap: nowrap;
justify-content: center;
}

.stream {
width: 50%;
max-width: 50%;
}
}
</style>
29 changes: 26 additions & 3 deletions photon-client/src/stores/settings/CameraSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,35 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
/**
* Take a snapshot for the calibration processes
*
* @param takeSnapshot whether or not to take a snapshot. Defaults to true
* @param cameraIndex the index of the camera that is currently in the calibration process
*/
takeCalibrationSnapshot(takeSnapshot = true, cameraIndex: number = useStateStore().currentCameraIndex) {
takeCalibrationSnapshot(cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
takeCalibrationSnapshot: takeSnapshot,
takeCalibrationSnapshot: true,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Save a snapshot of the input frame of the camera.
*
* @param cameraIndex the index of the camera
*/
saveInputSnapshot(cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
saveInputSnapshot: true,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
},
/**
* Save a snapshot of the output frame of the camera.
*
* @param cameraIndex the index of the camera
*/
saveOutputSnapshot(cameraIndex: number = useStateStore().currentCameraIndex) {
const payload = {
saveOutputSnapshot: true,
cameraIndex: cameraIndex
};
useStateStore().websocket?.send(payload, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,58 +32,57 @@
import org.photonvision.vision.opencv.CVMat;

public class FileSaveFrameConsumer implements Consumer<CVMat> {
private final Logger logger = new Logger(FileSaveFrameConsumer.class, LogGroup.General);

// Formatters to generate unique, timestamped file names
private static final String FILE_PATH = ConfigManager.getInstance().getImageSavePath().toString();
private static final String FILE_EXTENSION = ".jpg";
private static final String NT_SUFFIX = "SaveImgCmd";

DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
DateFormat tf = new SimpleDateFormat("hhmmssSS");
private final String NT_SUFFIX = "SaveImgCmd";
private final String ntEntryName;
private NetworkTable subTable;

private final NetworkTable rootTable;
private final Logger logger;
private long imgSaveCountInternal = 0;
private String camNickname;
private final String fnamePrefix;
private IntegerEntry entry;
private NetworkTable subTable;
private final String ntEntryName;
private IntegerEntry saveFrameEntry;

private String cameraNickname;
private final String streamType;

private long savedImagesCount = 0;

public FileSaveFrameConsumer(String camNickname, String streamPrefix) {
this.fnamePrefix = camNickname + "_" + streamPrefix;
this.ntEntryName = streamPrefix + NT_SUFFIX;
this.cameraNickname = camNickname;
this.streamType = streamPrefix;

this.rootTable = NetworkTablesManager.getInstance().kRootTable;
updateCameraNickname(camNickname);
this.logger = new Logger(FileSaveFrameConsumer.class, this.camNickname, LogGroup.General);
}

public void accept(CVMat image) {
if (image != null && image.getMat() != null && !image.getMat().empty()) {
var curCommand = entry.get(); // default to just our current count
if (curCommand >= 0) {
// Only do something if we got a valid current command
if (imgSaveCountInternal < curCommand) {
// Save one frame.
// Create the filename
Date now = new Date();
String savefile =
FILE_PATH
+ File.separator
+ fnamePrefix
+ "_"
+ df.format(now)
+ "T"
+ tf.format(now)
+ FILE_EXTENSION;

// write to file
Imgcodecs.imwrite(savefile, image.getMat());

// Count one more image saved
imgSaveCountInternal++;
logger.info("Saved new image at " + savefile);

} else if (imgSaveCountInternal > curCommand) {
imgSaveCountInternal = curCommand;
}
long currentCount = saveFrameEntry.get();

// Await save request
if (currentCount == -1) return;

// The requested count is greater than the actual count
if (savedImagesCount < currentCount) {
Date now = new Date();

String fileName =
cameraNickname + "_" + streamType + "_" + df.format(now) + "T" + tf.format(now);
String saveFilePath = FILE_PATH + File.separator + fileName + FILE_EXTENSION;

Imgcodecs.imwrite(saveFilePath, image.getMat());

savedImagesCount++;
logger.info("Saved new image at " + saveFilePath);
} else if (savedImagesCount > currentCount) {
// Reset local value with NT value in case of de-sync
savedImagesCount = currentCount;
}
}
}
Expand All @@ -97,9 +96,14 @@ public void updateCameraNickname(String newCameraNickname) {
}

// Recreate and re-init network tables structure
this.camNickname = newCameraNickname;
this.subTable = rootTable.getSubTable(this.camNickname);
this.subTable.getEntry(ntEntryName).setInteger(imgSaveCountInternal);
this.entry = subTable.getIntegerTopic(ntEntryName).getEntry(-1); // Default negative
this.cameraNickname = newCameraNickname;
this.subTable = rootTable.getSubTable(this.cameraNickname);
this.subTable.getEntry(ntEntryName).setInteger(savedImagesCount);
this.saveFrameEntry = subTable.getIntegerTopic(ntEntryName).getEntry(-1); // Default negative
}

public void overrideTakeSnapshot() {
// Simulate NT change
saveFrameEntry.set(saveFrameEntry.get() + 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,14 @@ public void startCalibration(UICalibrationData data) {
setPipeline(PipelineManager.CAL_3D_INDEX);
}

public void saveInputSnapshot() {
inputFrameSaver.overrideTakeSnapshot();
}

public void saveOutputSnapshot() {
outputFrameSaver.overrideTakeSnapshot();
}

public void takeCalibrationSnapshot() {
pipelineManager.calibration3dPipeline.takeSnapshot();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ public void onDataChangeEvent(DataChangeEvent<?> event) {
parentModule.startCalibration(data);
parentModule.saveAndBroadcastAll();
return;
case "saveInputSnapshot":
parentModule.saveInputSnapshot();
return;
case "saveOutputSnapshot":
parentModule.saveOutputSnapshot();
return;
case "takeCalSnapshot":
parentModule.takeCalibrationSnapshot();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,30 @@ public void onBinaryMessage(WsBinaryMessageContext context) {
dcService.publishEvent(changePipelineEvent);
break;
}
case SMT_SAVEINPUTSNAPSHOT:
{
var takeInputSnapshotEvent =
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"saveInputSnapshot",
0,
cameraIndex,
context);
dcService.publishEvent(takeInputSnapshotEvent);
break;
}
case SMT_SAVEOUTPUTSNAPSHOT:
{
var takeOutputSnapshotEvent =
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"saveOutputSnapshot",
0,
cameraIndex,
context);
dcService.publishEvent(takeOutputSnapshotEvent);
break;
}
case SMT_TAKECALIBRATIONSNAPSHOT:
{
var takeCalSnapshotEvent =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public enum DataSocketMessageType {
SMT_PIPELINESETTINGCHANGE("changePipelineSetting"),
SMT_CURRENTPIPELINE("currentPipeline"),
SMT_STARTPNPCALIBRATION("startPnpCalibration"),
SMT_SAVEINPUTSNAPSHOT("saveInputSnapshot"),
SMT_SAVEOUTPUTSNAPSHOT("saveOutputSnapshot"),
SMT_TAKECALIBRATIONSNAPSHOT("takeCalibrationSnapshot"),
SMT_DUPLICATEPIPELINE("duplicatePipeline"),
SMT_CHANGEBRIGHTNESS("enabledLEDPercentage"),
Expand Down

0 comments on commit ededc4f

Please sign in to comment.