Skip to content
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

Take Snapshots from PhotonClient #940

Merged
merged 16 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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();
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make more sense to pass in the correct callback through a prop?

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 would rather compartmentalize as much logic into the stream viewer component as possible. This icon would only serve this function so adding it as a callback just duplicates code a lot.

} 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()"
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yeah this fun hack. We should probably not have one button doing two things

Copy link
Member Author

Choose a reason for hiding this comment

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

nah its chill, the logic makes sense now with better variable names

@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