Compare commits

...

18 Commits

Author SHA1 Message Date
Matt
428f926ac2 Actually properly match cameras by name fr this time (#1237)
Our current code matches cameras in this order (which I think is objectively wrong and stupid)

- by-id (/dev/v4l/by-id/product-string)
- by path (/dev/videoN)
- product string/name, but ascii only
- asks cscore to reconnect to cameras using `path`, which on linux is actually /dev/videoN. This isn't guaranteed to stick to a camera if you replug them weirdly at runtime.

This is silly and does not consider the actual physical usb port. I propose instead, in this order:

- By physical usb port path and base name
- by physical usb port path and USB VID/PID
- By base name only (with a toggle switch to disable this, and create a new VisionModule instead)
- Give cscore /dev/video/by-path on Linux systems, pinning Photon USBCameras to a particular usb port once created.

This changes lots of things so stay paranoid!
2024-02-16 16:05:47 -05:00
Matt
4efeb3d412 Load libwinpthread-1.dll before libgcc_s_seh-1 (#1228) 2024-02-16 16:05:16 -05:00
Matt
6a2d83e19b Upload docs to VPS via SFTP (#1235)
Still in testing, might break our docs for now
2024-02-12 19:57:23 -05:00
Matt
1c0d92641f Check empty mean errors in calibration card (#1229)
Fixes calibration card disappearing if calibdb calibration was used
2024-02-12 15:55:31 -05:00
DeltaDizzy
9653c46bdb fix cpp and java photoncamera names (#1230) 2024-02-11 04:27:25 -05:00
Chris Gerth
3738e7821b fix latency calculation (#1227) 2024-02-09 18:45:38 -06:00
Tim Winters
0eb0a4e3c5 Store the last pose on update (#1207)
* Store the last pose on update

* Don't clear lastPose if pose isn't calculated

---------

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2024-02-05 09:50:36 -05:00
Chris Gerth
7666f152bb Fix chessboard gen for unique square sizes (#1217) 2024-02-05 09:48:39 -05:00
Craig Schardt
45a39f6609 Remove duplicate video modes (#1221)
(Fixes #1219)
2024-02-04 22:42:01 -05:00
Matt
bc55218739 Add NPU usage to metrics on supported platforms (#1215) 2024-02-03 12:31:31 -05:00
Matt
e616d93d59 Update CameraCalibrationInfoCard.vue (#1214) 2024-02-02 21:53:47 -05:00
Chris Gerth
5851509a9e Python tweaks (#1211)
* Increasing api parity with java/cpp by adding hasTargets

* type hints fixed up

* wpiFormat
2024-02-02 14:17:53 -06:00
james20902
ea1b701ba7 Add support for different RKNN YOLO models in the backend (#1205) 2024-02-01 23:48:02 -05:00
Matt
62112cd2fd Reduce initial connection bandwidth (#1200)
Reduces bandwidth requirements by being much lazier about how much calibration data is sent to the UI.
2024-02-01 21:42:54 -05:00
Gautam
c7508fea46 Add v4l-utils to install script (#1201)
adds about 2kb to our image
2024-01-27 09:46:50 -05:00
Matt
eca3cea82d Sort object detection results and reduce code duplication (#1173)
* Sort object detection results and reduce code dup.

* Filter objdet results by ratio and area

* Address code review

---------

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2024-01-23 14:10:31 -05:00
Craig Schardt
cbbfbda59d clean up debugging println (#1193) 2024-01-22 22:59:42 -05:00
Drew Williams
a3e1dda3aa Fixed cpp sim apriltag layout and cleaned up cpp sim example (#1190)
* Fixed cpp sim apriltag layout and cleaned up cpp sim example

* changed layout for photoncamerasim

---------

Co-authored-by: Drew Williams <DrewW@iARx.com>
2024-01-22 15:38:25 -05:00
63 changed files with 991 additions and 431 deletions

View File

@@ -68,10 +68,6 @@ jobs:
release:
needs: [build-client, run_docs]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-22.04
steps:
@@ -79,14 +75,12 @@ jobs:
- uses: actions/download-artifact@v4
- run: find .
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
- name: copy file via ssh password
uses: appleboy/scp-action@v0.1.7
with:
# Upload entire repository
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
host: ${{ secrets.WEBMASTER_SSH_HOST }}
username: ${{ secrets.WEBMASTER_SSH_USERNAME }}
password: ${{ secrets.WEBMASTER_SSH_KEY }}
port: ${{ secrets.WEBMASTER_SSH_PORT }}
source: "*"
target: /var/www/html/photonvision-docs/

View File

@@ -30,7 +30,7 @@ ext {
joglVersion = "2.4.0-rc-20200307"
javalinVersion = "5.6.2"
photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678"
rknnVersion = "dev-v2024.0.0-30-g001b5ec"
rknnVersion = "dev-v2024.0.0-64-gc0836a6"
frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";

View File

@@ -25,15 +25,10 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
if (calib !== undefined) {
// Is this the right formula for RMS error? who knows! not me!
const perViewSumSquareReprojectionError = calib.observations.flatMap((it) =>
it.reprojectionErrors.flatMap((it2) => [it2.x, it2.y])
);
// For each error, square it, sum the squares, and divide by total points N
format.mean = Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
if (calib.meanErrors.length)
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
else format.mean = NaN;
format.horizontalFOV =
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
@@ -109,7 +104,7 @@ const downloadCalibBoard = () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern
if ((xPos + yPos + 0.25) % 2 === 0) {
if (squareY % 2 != squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
}
}
@@ -263,7 +258,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
>
<td>{{ getResolutionString(value.resolution) }}</td>
<td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }}
{{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
</td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>

View File

@@ -1,51 +1,19 @@
<script setup lang="ts">
import type { BoardObservation, CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import type { CameraCalibrationResult, VideoFormat } from "@/types/SettingTypes";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { ref } from "vue";
import loadingImage from "@/assets/images/loading.svg";
import { computed, inject, ref } from "vue";
import { getResolutionString, parseJsonFile } from "@/lib/PhotonUtils";
const props = defineProps<{
videoFormat: VideoFormat;
}>();
const getMeanFromView = (o: BoardObservation) => {
// Is this the right formula for RMS error? who knows! not me!
const perViewSumSquareReprojectionError = o.reprojectionErrors.flatMap((it2) => [it2.x, it2.y]);
// For each error, square it, sum the squares, and divide by total points N
return Math.sqrt(
perViewSumSquareReprojectionError.map((it) => Math.pow(it, 2)).reduce((a, b) => a + b, 0) /
perViewSumSquareReprojectionError.length
);
const exportCalibration = ref();
const openExportCalibrationPrompt = () => {
exportCalibration.value.click();
};
// Import and export functions
const downloadCalibration = () => {
const calibData = useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution);
if (calibData === undefined) {
useStateStore().showSnackbarMessage({
color: "error",
message:
"Calibration data isn't available for the requested resolution, please calibrate the requested resolution first"
});
return;
}
const camUniqueName = useCameraSettingsStore().currentCameraSettings.uniqueName;
const filename = `photon_calibration_${camUniqueName}_${calibData.resolution.width}x${calibData.resolution.height}.json`;
const fileData = JSON.stringify(calibData);
const element = document.createElement("a");
element.style.display = "none";
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(fileData));
element.setAttribute("download", filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const importCalibrationFromPhotonJson = ref();
const openUploadPhotonCalibJsonPrompt = () => {
importCalibrationFromPhotonJson.value.click();
@@ -97,19 +65,28 @@ const importCalibration = async () => {
};
interface ObservationDetails {
snapshotSrc: any;
mean: number;
index: number;
}
const currentCalibrationCoeffs = computed<CameraCalibrationResult | undefined>(() =>
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)
);
const getObservationDetails = (): ObservationDetails[] | undefined => {
return useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.observations.map((o, i) => ({
index: i,
mean: parseFloat(getMeanFromView(o).toFixed(2)),
snapshotSrc: o.includeObservationInCalibration ? "data:image/png;base64," + o.snapshotData.data : loadingImage
}));
const coefficients = currentCalibrationCoeffs.value;
return coefficients?.meanErrors.map((m, i) => ({
index: i,
mean: parseFloat(m.toFixed(2))
}));
};
const exportCalibrationURL = computed<string>(() =>
useCameraSettingsStore().getCalJSONUrl(inject("backendHost") as string, props.videoFormat.resolution)
);
const calibrationImageURL = (index: number) =>
useCameraSettingsStore().getCalImageUrl(inject<string>("backendHost") as string, props.videoFormat.resolution, index);
</script>
<template>
@@ -140,19 +117,22 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<v-btn
color="secondary"
class="mt-4"
:disabled="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) === undefined"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
@click="downloadCalibration"
@click="openExportCalibrationPrompt"
>
<v-icon left>mdi-export</v-icon>
<span>Export</span>
</v-btn>
<a
ref="exportCalibration"
style="color: black; text-decoration: none; display: none"
:href="exportCalibrationURL"
target="_blank"
/>
</v-col>
</v-row>
<v-row
v-if="useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution) !== undefined"
class="pt-2"
>
<v-row v-if="currentCalibrationCoeffs" class="pt-2">
<v-card-subtitle>Calibration Details</v-card-subtitle>
<v-simple-table dense style="width: 100%" class="pl-2 pr-2">
<template #default>
@@ -231,7 +211,9 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
</tr>
<tr>
<td>Horizontal FOV</td>
<td>{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>
{{ videoFormat.horizontalFOV !== undefined ? videoFormat.horizontalFOV.toFixed(2) + "°" : "-" }}
</td>
</tr>
<tr>
<td>Vertical FOV</td>
@@ -242,11 +224,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
<!-- Board warp, only shown for mrcal-calibrated cameras -->
<tr
v-if="
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)?.calobjectWarp?.length === 2
"
>
<tr v-if="currentCalibrationCoeffs?.calobjectWarp?.length === 2">
<td>Board warp, X/Y</td>
<td>
{{
@@ -278,7 +256,7 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<template #expanded-item="{ headers, item }">
<td :colspan="headers.length">
<div style="display: flex; justify-content: center; width: 100%">
<img :src="item.snapshotSrc" alt="observation image" class="snapshot-preview pt-2 pb-2" />
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
</div>
</td>
</template>

View File

@@ -11,6 +11,16 @@ const currentPipelineSettings = computed<ActivePipelineSettings>(
() => useCameraSettingsStore().currentPipelineSettings
);
// TODO fix pv-range-slider so that store access doesn't need to be deferred
const contourArea = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
});
const contourRatio = computed<[number, number]>({
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
});
const interactiveCols = computed(() =>
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
@@ -32,5 +42,42 @@ const interactiveCols = computed(() =>
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
/>
<pv-range-slider
v-model="contourArea"
label="Area"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
/>
<pv-range-slider
v-model="contourRatio"
label="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
:min="0"
:max="100"
:slider-cols="interactiveCols"
:step="0.01"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
/>
<pv-select
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
label="Target Orientation"
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
:items="['Portrait', 'Landscape']"
:select-cols="interactiveCols"
@input="
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
"
/>
<pv-select
v-model="currentPipelineSettings.contourSortMode"
label="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="interactiveCols"
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
/>
</div>
</template>

View File

@@ -27,42 +27,54 @@ const generalMetrics = computed<MetricItem[]>(() => [
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
]);
const platformMetrics = computed<MetricItem[]>(() => [
{
header: "CPU Temp",
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
},
{
header: "GPU Memory Usage",
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
value: useSettingsStore().metrics.cpuThr || "Unknown"
},
{
header: "CPU Uptime",
value: useSettingsStore().metrics.cpuUptime || "Unknown"
},
{
header: "Disk Usage",
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
const platformMetrics = computed<MetricItem[]>(() => {
const stats = [
{
header: "CPU Temp",
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
},
{
header: "CPU Usage",
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
},
{
header: "CPU Memory Usage",
value:
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
},
{
header: "GPU Memory Usage",
value:
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
? "Unknown"
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
},
{
header: "CPU Throttling",
value: useSettingsStore().metrics.cpuThr || "Unknown"
},
{
header: "CPU Uptime",
value: useSettingsStore().metrics.cpuUptime || "Unknown"
},
{
header: "Disk Usage",
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
}
];
if (useSettingsStore().metrics.npuUsage) {
stats.push({
header: "NPU Usage",
value: useSettingsStore().metrics.npuUsage || "Unknown"
});
}
]);
return stats;
});
const metricsLastFetched = ref("Never");
const fetchMetrics = () => {

View File

@@ -59,7 +59,8 @@ const settingsHaveChanged = (): boolean => {
a.shouldPublishProto !== b.shouldPublishProto ||
a.networkManagerIface !== b.networkManagerIface ||
a.setStaticCommand !== b.setStaticCommand ||
a.setDHCPcommand !== b.setDHCPcommand
a.setDHCPcommand !== b.setDHCPcommand ||
a.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
);
};
@@ -77,6 +78,7 @@ const saveGeneralSettings = () => {
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
staticIp: tempSettingsStruct.value.staticIp
};
@@ -137,6 +139,8 @@ watchEffect(() => {
<template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Global Settings</v-card-title>
<v-divider />
<v-card-title>Networking</v-card-title>
<div class="ml-5">
<v-form ref="form" v-model="settingsValid">
@@ -254,6 +258,9 @@ watchEffect(() => {
>
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner>
<v-divider />
<v-card-title>Miscellaneous</v-card-title>
<pv-switch
v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf"
@@ -272,6 +279,14 @@ watchEffect(() => {
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode.
</v-banner>
<pv-switch
v-model="tempSettingsStruct.matchCamerasOnlyByPath"
label="Match cameras by-path ONLY"
tooltip="ONLY match cameras by the USB port they're plugged into + (basename or USB VID/PID), and never only by the device product string"
class="mt-3 mb-2"
:label-cols="4"
/>
<v-divider class="mb-3" />
</v-form>
<v-btn
color="accent"

View File

@@ -416,6 +416,23 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
cameraIndex: number = useStateStore().currentCameraIndex
): CameraCalibrationResult | undefined {
return this.cameras[cameraIndex].completeCalibrations.find((v) => resolutionsAreEqual(v.resolution, resolution));
},
getCalImageUrl(host: string, resolution: Resolution, idx: number, cameraIdx = useStateStore().currentCameraIndex) {
const url = new URL(`http://${host}/api/utils/getCalSnapshot`);
url.searchParams.set("width", Math.round(resolution.width).toFixed(0));
url.searchParams.set("height", Math.round(resolution.height).toFixed(0));
url.searchParams.set("snapshotIdx", Math.round(idx).toFixed(0));
url.searchParams.set("cameraIdx", Math.round(cameraIdx).toFixed(0));
return url.href;
},
getCalJSONUrl(host: string, resolution: Resolution, cameraIdx = useStateStore().currentCameraIndex) {
const url = new URL(`http://${host}/api/utils/getCalibrationJSON`);
url.searchParams.set("width", Math.round(resolution.width).toFixed(0));
url.searchParams.set("height", Math.round(resolution.height).toFixed(0));
url.searchParams.set("cameraIdx", Math.round(cameraIdx).toFixed(0));
return url.href;
}
}
});

View File

@@ -44,7 +44,9 @@ export const useSettingsStore = defineStore("settings", {
connName: "Example Wired Connection",
devName: "eth0"
}
]
],
networkingDisabled: false,
matchCamerasOnlyByPath: false
},
lighting: {
supported: true,
@@ -59,7 +61,8 @@ export const useSettingsStore = defineStore("settings", {
gpuMemUtil: undefined,
cpuThr: undefined,
cpuUptime: undefined,
diskUtilPct: undefined
diskUtilPct: undefined,
npuUsage: undefined
},
currentFieldLayout: {
field: {
@@ -91,7 +94,8 @@ export const useSettingsStore = defineStore("settings", {
gpuMemUtil: data.gpuMemUtil || undefined,
cpuThr: data.cpuThr || undefined,
cpuUptime: data.cpuUptime || undefined,
diskUtilPct: data.diskUtilPct || undefined
diskUtilPct: data.diskUtilPct || undefined,
npuUsage: data.npuUsage || undefined
};
},
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {

View File

@@ -20,6 +20,7 @@ export interface MetricData {
cpuThr?: string;
cpuUptime?: string;
diskUtilPct?: string;
npuUsage?: string;
}
export enum NetworkConnectionType {
@@ -46,6 +47,7 @@ export interface NetworkSettings {
setDHCPcommand?: string;
networkInterfaceNames: NetworkInterfaceType[];
networkingDisabled: boolean;
matchCamerasOnlyByPath: boolean;
}
export type ConfigurableNetworkSettings = Omit<
@@ -138,6 +140,9 @@ export interface CameraCalibrationResult {
distCoeffs: JsonMatOfDouble;
observations: BoardObservation[];
calobjectWarp?: number[];
// We might have to omit observations for bandwith, so backend will send us this
numSnapshots: number;
meanErrors: number[];
}
export enum ValidQuirks {
@@ -255,7 +260,9 @@ export const PlaceholderCameraSettings: CameraSettings = {
snapshotName: "img0.png",
snapshotData: { rows: 480, cols: 640, type: CvType.CV_8U, data: "" }
}
]
],
numSnapshots: 1,
meanErrors: [123.45]
}
],
pipelineNicknames: ["Placeholder Pipeline"],

View File

@@ -37,9 +37,8 @@ dependencies {
implementation 'org.zeroturnaround:zt-zip:1.14'
implementation "org.xerial:sqlite-jdbc:3.41.0.0"
def rknnjniversion = "dev-v2024.0.0-44-g8022c40"
implementation "org.photonvision:rknn_jni-jni:$rknnjniversion:linuxarm64"
implementation "org.photonvision:rknn_jni-java:$rknnjniversion"
implementation "org.photonvision:rknn_jni-jni:$rknnVersion:linuxarm64"
implementation "org.photonvision:rknn_jni-java:$rknnVersion"
implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion"

View File

@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
@@ -51,6 +52,12 @@ public class CameraConfiguration {
@JsonIgnore public String[] otherPaths = {};
@JsonProperty("usbVID")
public int usbVID = -1;
@JsonProperty("usbPID")
public int usbPID = -1;
public CameraType cameraType = CameraType.UsbCamera;
public double FOV = 70;
public final List<CameraCalibrationCoefficients> calibrations;
@@ -98,7 +105,9 @@ public class CameraConfiguration {
@JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
this.baseName = baseName;
this.uniqueName = uniqueName;
this.nickname = nickname;
@@ -108,6 +117,8 @@ public class CameraConfiguration {
this.cameraQuirks = cameraQuirks;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex;
this.usbPID = usbPID;
this.usbVID = usbVID;
logger.debug(
"Creating camera configuration for "
@@ -156,6 +167,17 @@ public class CameraConfiguration {
calibrations.add(calibration);
}
/**
* Get a unique descriptor of the USB port this camera is attached to. EG
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
*
* @return
*/
@JsonIgnore
public Optional<String> getUSBPath() {
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
}
@Override
public String toString() {
return "CameraConfiguration [baseName="

View File

@@ -20,6 +20,24 @@ package org.photonvision.common.configuration;
public class HardwareSettings {
public int ledBrightnessPercentage = 100;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ledBrightnessPercentage;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
HardwareSettings other = (HardwareSettings) obj;
if (ledBrightnessPercentage != other.ledBrightnessPercentage) return false;
return true;
}
@Override
public String toString() {
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";

View File

@@ -39,6 +39,12 @@ public class NetworkConfig {
public boolean shouldManage;
public boolean shouldPublishProto = false;
/**
* If we should ONLY match cameras by path, and NEVER only by base-name. For now default to false
* to preserve old matching logic
*/
public boolean matchCamerasOnlyByPath = false;
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
@@ -76,7 +82,8 @@ public class NetworkConfig {
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
@JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
@JsonProperty("setDHCPcommand") String setDHCPcommand,
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType;
this.staticIp = staticIp;
@@ -86,6 +93,7 @@ public class NetworkConfig {
this.networkManagerIface = networkManagerIface;
this.setStaticCommand = setStaticCommand;
this.setDHCPcommand = setDHCPcommand;
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
setShouldManage(shouldManage);
}

View File

@@ -25,12 +25,14 @@ import java.nio.file.Paths;
import java.util.List;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.rknn.RknnJNI;
public class NeuralNetworkModelManager {
private static NeuralNetworkModelManager INSTANCE;
private static final Logger logger = new Logger(NeuralNetworkModelManager.class, LogGroup.Config);
private final String MODEL_NAME = "note-640-640-yolov5s.rknn";
private final RknnJNI.ModelVersion modelVersion = RknnJNI.ModelVersion.YOLO_V5;
private File defaultModelFile;
private List<String> labels;
@@ -51,7 +53,7 @@ public class NeuralNetworkModelManager {
this.defaultModelFile = new File(modelsFolder, MODEL_NAME);
extractResource(modelResourcePath, defaultModelFile);
File labelsFile = new File(modelsFolder, "labels.txt");
File labelsFile = new File(modelsFolder, "labels_v5.txt");
var labelResourcePath = "/models/" + labelsFile.getName();
extractResource(labelResourcePath, labelsFile);
@@ -95,4 +97,8 @@ public class NeuralNetworkModelManager {
public List<String> getLabels() {
return labels;
}
public RknnJNI.ModelVersion getModelVersion() {
return modelVersion;
}
}

View File

@@ -31,7 +31,7 @@ import org.photonvision.common.util.SerializationUtils;
import org.photonvision.jni.RknnDetectorJNI;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.processes.VisionModule;
import org.photonvision.vision.processes.VisionModuleManager;
@@ -126,13 +126,6 @@ public class PhotonConfiguration {
settingsSubmap.put("networkSettings", netConfigMap);
map.put(
"cameraSettings",
VisionModuleManager.getInstance().getModules().stream()
.map(VisionModule::toUICameraConfig)
.map(SerializationUtils::objectToHashMap)
.collect(Collectors.toList()));
var lightingConfig = new UILightingConfig();
lightingConfig.brightness = hardwareSettings.ledBrightnessPercentage;
lightingConfig.supported = !hardwareConfig.ledPins.isEmpty();
@@ -181,7 +174,7 @@ public class PhotonConfiguration {
public HashMap<Integer, HashMap<String, Object>> videoFormatList;
public int outputStreamPort;
public int inputStreamPort;
public List<CameraCalibrationCoefficients> calibrations;
public List<UICameraCalibrationCoefficients> calibrations;
public boolean isFovConfigurable = true;
public QuirkyCamera cameraQuirks;
public boolean isCSICamera;

View File

@@ -26,6 +26,7 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.pipeline.result.CalibrationPipelineResult;
public class UIDataPublisher implements CVPipelineResultConsumer {
private static final Logger logger = new Logger(UIDataPublisher.class, LogGroup.VisionModule);
@@ -41,16 +42,22 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
public void accept(CVPipelineResult result) {
long now = System.currentTimeMillis();
// only update the UI at 15hz
// only update the UI at 10hz
if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return;
var dataMap = new HashMap<String, Object>();
dataMap.put("fps", result.fps);
dataMap.put("latency", result.getLatencyMillis());
var uiTargets = new ArrayList<HashMap<String, Object>>(result.targets.size());
for (var t : result.targets) {
uiTargets.add(t.toHashMap());
// We don't actually need to send targets during calibration and it can take up a lot (up to
// 1.2Mbps for 60 snapshots) of target results with no pitch/yaw/etc set
if (!(result instanceof CalibrationPipelineResult)) {
for (var t : result.targets) {
uiTargets.add(t.toHashMap());
}
}
dataMap.put("targets", uiTargets);
dataMap.put("classNames", result.objectDetectionClassNames);

View File

@@ -92,6 +92,10 @@ public class MetricsManager {
return safeExecute(cmds.cpuThrottleReasonCmd);
}
public String getNpuUsage() {
return safeExecute(cmds.npuUsageCommand);
}
private String gpuMemSave = null;
public String getGPUMemorySplit() {
@@ -128,6 +132,7 @@ public class MetricsManager {
metrics.put("ramUtil", this.getUsedRam());
metrics.put("gpuMemUtil", this.getMallocedMemory());
metrics.put("diskUtilPct", this.getUsedDiskPct());
metrics.put("npuUsage", this.getNpuUsage());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}

View File

@@ -29,6 +29,8 @@ public class CmdBase {
// GPU
public String gpuMemoryCommand = "";
public String gpuMemUsageCommand = "";
// NPU
public String npuUsageCommand = "";
// RAM
public String ramUsageCommand = "";
// Disk

View File

@@ -44,5 +44,7 @@ public class RK3588Cmds extends LinuxCmds {
*/
cpuTemperatureCommand =
"cat /sys/class/thermal/thermal_zone1/temp | awk '{printf \"%.1f\", $1/1000}'";
npuUsageCommand = "cat /sys/kernel/debug/rknpu/load | sed 's/NPU load://; s/^ *//; s/ *$//'";
}
}

View File

@@ -65,12 +65,11 @@ public class RknnDetectorJNI extends PhotonJNICommon {
long objPointer = -1;
private List<String> labels;
private final Object lock = new Object();
private static final CopyOnWriteArrayList<Long> detectors = new CopyOnWriteArrayList<>();
public RknnObjectDetector(String modelPath, List<String> labels) {
public RknnObjectDetector(String modelPath, List<String> labels, RknnJNI.ModelVersion version) {
synchronized (lock) {
objPointer = RknnJNI.create(modelPath, labels.size());
objPointer = RknnJNI.create(modelPath, labels.size(), version.ordinal(), -1);
detectors.add(objPointer);
System.out.println(
"Created " + objPointer + "! Detectors: " + Arrays.toString(detectors.toArray()));

View File

@@ -53,6 +53,7 @@ public class MrCalJNILoader extends PhotonJNICommon {
"libcolamd",
"libccolamd",
"openblas",
"libwinpthread-1",
"libgcc_s_seh-1",
"libquadmath-0",
"libgfortran-5",

View File

@@ -24,7 +24,7 @@ import java.util.List;
import org.opencv.core.Point;
import org.opencv.core.Point3;
public final class BoardObservation {
public final class BoardObservation implements Cloneable {
// Expected feature 3d location in the camera frame
@JsonProperty("locationInObjectSpace")
public List<Point3> locationInObjectSpace;
@@ -68,4 +68,33 @@ public final class BoardObservation {
this.snapshotName = snapshotName;
this.snapshotData = snapshotData;
}
@Override
public String toString() {
return "BoardObservation [locationInObjectSpace="
+ locationInObjectSpace
+ ", locationInImageSpace="
+ locationInImageSpace
+ ", reprojectionErrors="
+ reprojectionErrors
+ ", optimisedCameraToObject="
+ optimisedCameraToObject
+ ", includeObservationInCalibration="
+ includeObservationInCalibration
+ ", snapshotName="
+ snapshotName
+ ", snapshotData="
+ snapshotData
+ "]";
}
@Override
public BoardObservation clone() {
try {
return (BoardObservation) super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Guhhh clone buh");
return null;
}
}
}

View File

@@ -191,8 +191,8 @@ public class CameraCalibrationCoefficients implements Releasable {
+ cameraIntrinsics
+ ", distCoeffs="
+ distCoeffs
+ ", observations="
+ observations
+ ", observationslen="
+ observations.size()
+ ", calobjectWarp="
+ Arrays.toString(calobjectWarp)
+ ", intrinsicsArr="
@@ -201,4 +201,16 @@ public class CameraCalibrationCoefficients implements Releasable {
+ Arrays.toString(distCoeffsArr)
+ "]";
}
public UICameraCalibrationCoefficients cloneWithoutObservations() {
return new UICameraCalibrationCoefficients(
resolution,
cameraIntrinsics,
distCoeffs,
calobjectWarp,
observations,
calobjectSize,
calobjectSpacing,
lensmodel);
}
}

View File

@@ -76,4 +76,17 @@ public class JsonImageMat implements Releasable {
public void release() {
if (wrappedMat != null) wrappedMat.release();
}
@Override
public String toString() {
return "JsonImageMat [rows="
+ rows
+ ", cols="
+ cols
+ ", type="
+ type
+ ", datalen="
+ data.length()
+ "]";
}
}

View File

@@ -40,7 +40,7 @@ public class JsonMatOfDouble implements Releasable {
@JsonIgnore private Mat wrappedMat = null;
@JsonIgnore private Matrix wpilibMat = null;
private MatOfDouble wrappedMatOfDouble;
@JsonIgnore private MatOfDouble wrappedMatOfDouble;
public JsonMatOfDouble(int rows, int cols, double[] data) {
this(rows, cols, CvType.CV_64FC1, data);

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.calibration;
import java.util.List;
import java.util.stream.Collectors;
import org.opencv.core.Size;
public class UICameraCalibrationCoefficients extends CameraCalibrationCoefficients {
public int numSnapshots;
public List<Double> meanErrors;
public UICameraCalibrationCoefficients(
Size resolution,
JsonMatOfDouble cameraIntrinsics,
JsonMatOfDouble distCoeffs,
double[] calobjectWarp,
List<BoardObservation> observations,
Size calobjectSize,
double calobjectSpacing,
CameraLensModel lensmodel) {
// yeet observations, keep all else
super(
resolution,
cameraIntrinsics,
distCoeffs,
calobjectWarp,
List.of(),
calobjectSize,
calobjectSpacing,
lensmodel);
this.numSnapshots = observations.size();
this.meanErrors =
observations.stream()
.map(
it2 ->
it2.reprojectionErrors.stream()
.mapToDouble(it -> Math.sqrt(it.x * it.x + it.y * it.y))
.average()
.orElse(0))
.collect(Collectors.toList());
}
}

View File

@@ -19,6 +19,7 @@ package org.photonvision.vision.camera;
import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.Arrays;
import java.util.Optional;
public class CameraInfo extends UsbCameraInfo {
public final CameraType cameraType;
@@ -68,6 +69,16 @@ public class CameraInfo extends UsbCameraInfo {
return getBaseName().replaceAll(" ", "_");
}
/**
* Get a unique descriptor of the USB port this camera is attached to. EG
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
*
* @return
*/
public Optional<String> getUSBPath() {
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
@@ -79,4 +90,19 @@ public class CameraInfo extends UsbCameraInfo {
&& productId == other.productId
&& vendorId == other.vendorId;
}
@Override
public String toString() {
return "CameraInfo [cameraType="
+ cameraType
+ "baseName="
+ getBaseName()
+ ", vid="
+ vendorId
+ ", pid="
+ productId
+ ", otherPaths="
+ Arrays.toString(otherPaths)
+ "]";
}
}

View File

@@ -49,9 +49,17 @@ public class USBCameraSource extends VisionSource {
super(config);
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
camera = new UsbCamera(config.nickname, config.path);
// cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i
// swap cameras around, the same /dev/videoN ID will be assigned to that camera. So instead
// default to pinning to a particular USB port, or by "path" (appears to be a global identifier)
// on Windows.
camera = new UsbCamera(config.nickname, config.getUSBPath().orElse(config.path));
cvSink = CameraServer.getVideo(this.camera);
// set vid/pid if not done already for future matching
if (config.usbVID < 0) config.usbVID = this.camera.getInfo().vendorId;
if (config.usbPID < 0) config.usbPID = this.camera.getInfo().productId;
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(
@@ -395,6 +403,7 @@ public class USBCameraSource extends VisionSource {
// Sort by resolution
var sortedList =
videoModesList.stream()
.distinct() // remove redundant video mode entries
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
.collect(Collectors.toList());
Collections.reverse(sortedList);

View File

@@ -47,6 +47,16 @@ public class Contour implements Releasable {
this.mat = mat;
}
public Contour(Rect2d box) {
// no easy way to convert a Rect2d to Mat, diy it. Order is tl tr br bl
this.mat =
new MatOfPoint(
box.tl(),
new Point(box.x + box.width, box.y),
box.br(),
new Point(box.x, box.y + box.height));
}
public MatOfPoint2f getMat2f() {
if (mat2f == null) {
mat2f = new MatOfPoint2f(mat.toArray());

View File

@@ -25,15 +25,15 @@ public enum ContourSortMode {
Comparator.comparingDouble(PotentialTarget::getArea)
.reversed()), // reversed so that zero index has the largest size
Smallest(Largest.getComparator().reversed()),
Highest(Comparator.comparingDouble(rect -> rect.getMinAreaRect().center.y)),
Highest(Comparator.comparingDouble(tgt -> tgt.getMinAreaRect().center.y)),
Lowest(Highest.getComparator().reversed()),
Leftmost(Comparator.comparingDouble(target -> target.getMinAreaRect().center.x * -1)),
Leftmost(Comparator.comparingDouble(tgt -> tgt.getMinAreaRect().center.x * -1)),
Rightmost(Leftmost.getComparator().reversed()),
Centermost(
Comparator.comparingDouble(
rect ->
(Math.pow(rect.getMinAreaRect().center.y, 2)
+ Math.pow(rect.getMinAreaRect().center.x, 2))));
tgt ->
(Math.pow(tgt.getMinAreaRect().center.y, 2)
+ Math.pow(tgt.getMinAreaRect().center.x, 2))));
private final Comparator<PotentialTarget> m_comparator;

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.pipe.impl;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.pipe.CVPipe;
public class FilterObjectDetectionsPipe
extends CVPipe<
List<NeuralNetworkPipeResult>,
List<NeuralNetworkPipeResult>,
FilterObjectDetectionsPipe.FilterContoursParams> {
List<NeuralNetworkPipeResult> m_filteredContours = new ArrayList<>();
@Override
protected List<NeuralNetworkPipeResult> process(List<NeuralNetworkPipeResult> in) {
m_filteredContours.clear();
for (var contour : in) {
filterContour(contour);
}
return m_filteredContours;
}
private void filterContour(NeuralNetworkPipeResult contour) {
var boc = contour.box;
// Area filtering
double areaPercentage = boc.area() / params.getFrameStaticProperties().imageArea * 100.0;
double minAreaPercentage = params.getArea().getFirst();
double maxAreaPercentage = params.getArea().getSecond();
if (areaPercentage < minAreaPercentage || areaPercentage > maxAreaPercentage) return;
// Aspect ratio filtering; much simpler since always axis-aligned
double aspectRatio = boc.width / boc.height;
if (aspectRatio < params.getRatio().getFirst() || aspectRatio > params.getRatio().getSecond())
return;
m_filteredContours.add(contour);
}
public static class FilterContoursParams {
private final DoubleCouple m_area;
private final DoubleCouple m_ratio;
private final FrameStaticProperties m_frameStaticProperties;
public final boolean isLandscape;
public FilterContoursParams(
DoubleCouple area,
DoubleCouple ratio,
FrameStaticProperties camProperties,
boolean isLandscape) {
this.m_area = area;
this.m_ratio = ratio;
this.m_frameStaticProperties = camProperties;
this.isLandscape = isLandscape;
}
public DoubleCouple getArea() {
return m_area;
}
public DoubleCouple getRatio() {
return m_ratio;
}
public FrameStaticProperties getFrameStaticProperties() {
return m_frameStaticProperties;
}
}
}

View File

@@ -35,7 +35,8 @@ public class RknnDetectionPipe
this.detector =
new RknnObjectDetector(
NeuralNetworkModelManager.getInstance().getDefaultRknnModel().getAbsolutePath(),
NeuralNetworkModelManager.getInstance().getLabels());
NeuralNetworkModelManager.getInstance().getLabels(),
NeuralNetworkModelManager.getInstance().getModelVersion());
}
@Override

View File

@@ -42,6 +42,7 @@ public class SortContoursPipe
if (params.getSortMode() != ContourSortMode.Centermost) {
m_sortedContours.sort(params.getSortMode().getComparator());
} else {
// we need knowledge of camera properties to calculate this distance -- do it ourselves
m_sortedContours.sort(Comparator.comparingDouble(this::calcSquareCenterDistance));
}
}
@@ -50,10 +51,10 @@ public class SortContoursPipe
m_sortedContours.subList(0, Math.min(in.size(), params.getMaxTargets())));
}
private double calcSquareCenterDistance(PotentialTarget rect) {
private double calcSquareCenterDistance(PotentialTarget tgt) {
return Math.sqrt(
Math.pow(params.getCamProperties().centerX - rect.getMinAreaRect().center.x, 2)
+ Math.pow(params.getCamProperties().centerY - rect.getMinAreaRect().center.y, 2));
Math.pow(params.getCamProperties().centerX - tgt.getMinAreaRect().center.x, 2)
+ Math.pow(params.getCamProperties().centerY - tgt.getMinAreaRect().center.y, 2));
}
public static class SortContoursParams {

View File

@@ -26,6 +26,8 @@ import org.photonvision.vision.pipeline.result.CVPipelineResult;
public abstract class CVPipeline<R extends CVPipelineResult, S extends CVPipelineSettings>
implements Releasable {
static final int MAX_MULTI_TARGET_RESULTS = 10;
protected S settings;
protected FrameStaticProperties frameStaticProperties;
protected QuirkyCamera cameraQuirks;

View File

@@ -109,7 +109,7 @@ public class ColoredShapePipeline
SortContoursPipe.SortContoursParams sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? 5 : 1,
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
frameStaticProperties); // TODO don't hardcode?
sortContoursPipe.setParams(sortContoursParams);

View File

@@ -17,21 +17,26 @@
package org.photonvision.vision.pipeline;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
import org.photonvision.vision.pipe.impl.*;
import org.photonvision.vision.pipe.impl.RknnDetectionPipe.RknnDetectionPipeParams;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.PotentialTarget;
import org.photonvision.vision.target.TargetOrientation;
import org.photonvision.vision.target.TrackedTarget;
import org.photonvision.vision.target.TrackedTarget.TargetCalculationParameters;
public class ObjectDetectionPipeline
extends CVPipeline<CVPipelineResult, ObjectDetectionPipelineSettings> {
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
private final RknnDetectionPipe rknnPipe = new RknnDetectionPipe();
private final SortContoursPipe sortContoursPipe = new SortContoursPipe();
private final Collect2dTargetsPipe collect2dTargetsPipe = new Collect2dTargetsPipe();
private final FilterObjectDetectionsPipe filterContoursPipe = new FilterObjectDetectionsPipe();
private static final FrameThresholdType PROCESSING_TYPE = FrameThresholdType.NONE;
@@ -52,6 +57,38 @@ public class ObjectDetectionPipeline
params.confidence = settings.confidence;
params.nms = settings.nms;
rknnPipe.setParams(params);
DualOffsetValues dualOffsetValues =
new DualOffsetValues(
settings.offsetDualPointA,
settings.offsetDualPointAArea,
settings.offsetDualPointB,
settings.offsetDualPointBArea);
SortContoursPipe.SortContoursParams sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
frameStaticProperties);
sortContoursPipe.setParams(sortContoursParams);
var filterContoursParams =
new FilterObjectDetectionsPipe.FilterContoursParams(
settings.contourArea,
settings.contourRatio,
frameStaticProperties,
settings.contourTargetOrientation == TargetOrientation.Landscape);
filterContoursPipe.setParams(filterContoursParams);
Collect2dTargetsPipe.Collect2dTargetsParams collect2dTargetsParams =
new Collect2dTargetsPipe.Collect2dTargetsParams(
settings.offsetRobotOffsetMode,
settings.offsetSinglePoint,
dualOffsetValues,
settings.contourTargetOffsetPointEdge,
settings.contourTargetOrientation,
frameStaticProperties);
collect2dTargetsPipe.setParams(collect2dTargetsParams);
}
@Override
@@ -60,31 +97,35 @@ public class ObjectDetectionPipeline
// ***************** change based on backend ***********************
CVPipeResult<List<NeuralNetworkPipeResult>> ret = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += ret.nanosElapsed;
CVPipeResult<List<NeuralNetworkPipeResult>> rknnResult = rknnPipe.run(input_frame.colorImage);
sumPipeNanosElapsed += rknnResult.nanosElapsed;
List<NeuralNetworkPipeResult> targetList;
targetList = ret.output;
var names = rknnPipe.getClassNames();
input_frame.colorImage.getMat().copyTo(input_frame.processedImage.getMat());
// ***************** change based on backend ***********************
List<TrackedTarget> targets = new ArrayList<>();
var filterContoursResult = filterContoursPipe.run(rknnResult.output);
sumPipeNanosElapsed += filterContoursResult.nanosElapsed;
for (var t : targetList) {
targets.add(
new TrackedTarget(
t,
new TargetCalculationParameters(
false, null, null, null, null, frameStaticProperties)));
}
CVPipeResult<List<PotentialTarget>> sortContoursResult =
sortContoursPipe.run(
filterContoursResult.output.stream()
.map(shape -> new PotentialTarget(shape))
.collect(Collectors.toList()));
sumPipeNanosElapsed += sortContoursResult.nanosElapsed;
CVPipeResult<List<TrackedTarget>> collect2dTargetsResult =
collect2dTargetsPipe.run(sortContoursResult.output);
sumPipeNanosElapsed += collect2dTargetsResult.nanosElapsed;
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;
return new CVPipelineResult(sumPipeNanosElapsed, fps, targets, input_frame, names);
return new CVPipelineResult(
sumPipeNanosElapsed, fps, collect2dTargetsResult.output, input_frame, names);
}
@Override

View File

@@ -64,29 +64,6 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
settings.offsetDualPointB,
settings.offsetDualPointBArea);
// var rotateImageParams = new
// RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
// rotateImagePipe.setParams(rotateImageParams);
// if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && LibCameraJNI.isSupported()) {
// LibCameraJNI.setThresholds(
// settings.hsvHue.getFirst() / 180d,
// settings.hsvSaturation.getFirst() / 255d,
// settings.hsvValue.getFirst() / 255d,
// settings.hsvHue.getSecond() / 180d,
// settings.hsvSaturation.getSecond() / 255d,
// settings.hsvValue.getSecond() / 255d);
// // LibCameraJNI.setInvertHue(settings.hueInverted);
// LibCameraJNI.setRotation(settings.inputImageRotationMode.value);
// // LibCameraJNI.setShouldCopyColor(settings.inputShouldShow);
// } else {
// var hsvParams =
// new HSVPipe.HSVParams(
// settings.hsvHue, settings.hsvSaturation, settings.hsvValue,
// settings.hueInverted);
// hsvPipe.setParams(hsvParams);
// }
var findContoursParams = new FindContoursPipe.FindContoursParams();
findContoursPipe.setParams(findContoursParams);
@@ -113,7 +90,7 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
var sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
settings.outputShowMultipleTargets ? 8 : 1, // TODO don't hardcode?
settings.outputShowMultipleTargets ? MAX_MULTI_TARGET_RESULTS : 1,
frameStaticProperties);
sortContoursPipe.setParams(sortContoursParams);

View File

@@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.opencv.core.Size;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
@@ -536,7 +537,10 @@ public class VisionModule {
ret.outputStreamPort = this.outputStreamPort;
ret.inputStreamPort = this.inputStreamPort;
ret.calibrations = visionSource.getSettables().getConfiguration().calibrations;
ret.calibrations =
visionSource.getSettables().getConfiguration().calibrations.stream()
.map(CameraCalibrationCoefficients::cloneWithoutObservations)
.collect(Collectors.toList());
ret.isFovConfigurable =
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV()

View File

@@ -98,8 +98,7 @@ public class VisionRunner {
var pipelineResult = pipeline.run(frame, cameraQuirks);
pipelineResultConsumer.accept(pipelineResult);
} catch (Exception ex) {
logger.error("Exception on loop " + loopCount);
ex.printStackTrace();
logger.error("Exception on loop " + loopCount, ex);
}
loopCount++;

View File

@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
@@ -164,7 +165,7 @@ public class VisionSourceManager {
// Debug prints
for (var info : connectedCameras) {
logger.info("Adding local video device - \"" + info.name + "\" at \"" + info.path + "\"");
logger.info("Detected unmatched physical camera: " + info.toString());
}
if (!unmatchedLoadedConfigs.isEmpty())
@@ -216,6 +217,52 @@ public class VisionSourceManager {
return sources;
}
/**
* Get a predicate for checking cameras against a saved config.
*
* @param savedConfig The saved camera configuration to match against
* @param checkUSBPath If we should compare the USB port/bus IDs
* @param checkVidPid If we should compare USB VID and PID
* @param checkBaseName If we should compare {@link CameraInfo#getBaseName}
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
* ?/usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
* on Windows)
*/
private final Predicate<CameraInfo> getCameraMatcher(
final CameraConfiguration savedConfig,
boolean checkUSBPath,
boolean checkVidPid,
boolean checkBaseName,
boolean checkPath) {
if (checkUSBPath && savedConfig.getUSBPath().isEmpty()) {
logger.debug(
"WARN: Camera has empty USB path, but asked to match by name: "
+ camCfgToString(savedConfig));
}
return (CameraInfo physicalCamera) -> {
var matches = true;
if (checkUSBPath) {
var savedPath = savedConfig.getUSBPath();
matches &= (savedPath.isPresent() && physicalCamera.getUSBPath().equals(savedPath));
}
if (checkBaseName) {
matches &= physicalCamera.getBaseName().equals(savedConfig.baseName);
}
if (checkVidPid) {
matches &=
(physicalCamera.vendorId == savedConfig.usbVID
&& physicalCamera.productId == savedConfig.usbPID);
}
if (checkPath) {
matches &= (physicalCamera.path.equals(savedConfig.path));
}
return matches;
};
}
/**
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
* disk.
@@ -226,35 +273,111 @@ public class VisionSourceManager {
*/
public List<CameraConfiguration> matchCameras(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) {
return matchCameras(
detectedCamInfos,
loadedCamConfigs,
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath);
}
private static final String camCfgToString(CameraConfiguration c) {
return new StringBuilder()
.append("[baseName=")
.append(c.baseName)
.append(", uniqueName=")
.append(c.uniqueName)
.append(", otherPaths=")
.append(Arrays.toString(c.otherPaths))
.append(", vid=")
.append(c.usbVID)
.append(", pid=")
.append(c.usbPID)
.append("]")
.toString();
}
/**
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
* disk.
*
* @param detectedCamInfos Information about currently connected USB cameras.
* @param loadedCamConfigs The USB {@link CameraConfiguration}s loaded from disk.
* @param matchCamerasOnlyByPath If we should never try to match only by (base name, vid, pid)
* @return the matched configurations.
*/
public List<CameraConfiguration> matchCameras(
List<CameraInfo> detectedCamInfos,
List<CameraConfiguration> loadedCamConfigs,
boolean matchCamerasOnlyByPath) {
var detectedCameraList = new ArrayList<>(detectedCamInfos);
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
ArrayList<CameraConfiguration> unloadedConfigs =
new ArrayList<CameraConfiguration>(loadedCamConfigs);
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByPathByID(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByPath no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by usb port & name & USB VID/PID...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, true, false));
} else
logger.debug("Skipping match by usb port/name/vid/pid, no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByPath(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByPath no configs or cameras left to match");
// On windows, the v4l path is actually useful and tells us the port the camera is physically
// connected to which is neat
if (Platform.isWindows()) {
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by windows-path & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, true));
} else
logger.debug(
"Skipping matching by windiws-path/name/vid/pid, no configs or cameras left to match");
}
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByName(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByName no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by usb port & USB VID/PID...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, false, false));
} else logger.debug("Skipping match by port/vid/pid, no configs or cameras left to match");
if (detectedCameraList.size() > 0)
// handle disabling only-by-base-name matching
if (!matchCamerasOnlyByPath) {
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, false));
} else
logger.debug("Skipping match by base-name/viid/pid, no configs or cameras left to match");
} else logger.info("Skipping match by filepath/vid/pid, disabled by user");
if (detectedCameraList.size() > 0) {
cameraConfigurations.addAll(
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
}
logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
return cameraConfigurations;
}
// loop over all the configs loaded from disk, attempting to match each camera
// to a config by path-by-id on linux
private List<CameraConfiguration> matchByPathByID(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
/**
* Abstractly match cameras
*
* @param detectedCamInfos Physical cameras unmatched and attached to the device
* @param unloadedConfigs {@link CameraConfiguration}
* @param checkUSBPath If we should compare the USB port/bus IDs
* @param checkVidPid If we should compare USB VID and PID
* @param checkBaseName If we should check {@link CameraInfo::getBaseName}
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
* usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
* on Windows). Note that path may change based on order cameras are plugged in/unplugged on
* Linux, and should not be trusted to remain the same.
* @return All matched or created new configs
*/
private List<CameraConfiguration> matchCamerasByStrategy(
List<CameraInfo> detectedCamInfos,
List<CameraConfiguration> unloadedConfigs,
boolean checkUSBPath,
boolean checkVidPid,
boolean checkBaseName,
boolean checkPath) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs);
@@ -262,111 +385,43 @@ public class VisionSourceManager {
for (CameraConfiguration config : unloadedConfigsCopy) {
// Only run match path by id if the camera is not a CSI camera.
if (config.cameraType != CameraType.ZeroCopyPicam) {
CameraInfo cameraInfo;
if (config.otherPaths.length == 0) {
logger.debug("No valid path-by-id found for config with name " + config.baseName);
} else {
// attempt matching by path and basename
logger.debug(
"Trying to find a match for loaded camera "
+ config.baseName
+ " with path-by-id "
+ config.otherPaths[0]);
cameraInfo =
detectedCamInfos.stream()
.filter(
usbCameraInfo ->
usbCameraInfo.otherPaths.length != 0
&& usbCameraInfo.otherPaths[0].equals(config.otherPaths[0])
&& usbCameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
logger.debug(
String.format(
"Trying to find a match for loaded camera %s by strategy (path %s vid/pid %s basename %s path %s) with camera config: %s",
config.baseName,
checkUSBPath,
checkVidPid,
checkBaseName,
checkPath,
camCfgToString(config)));
// If we actually matched a camera to a config, remove that camera from the list
// and add it to the output
if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
// Get matcher and filter against it, picking out the first match
Predicate<CameraInfo> matches =
getCameraMatcher(config, checkUSBPath, checkVidPid, checkBaseName, checkPath);
var cameraInfo = detectedCamInfos.stream().filter(matches).findFirst().orElse(null);
// If we actually matched a camera to a config, remove that camera from the list
// and add it to the output
if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
} else {
logger.debug("No camera found for the config " + config.baseName);
}
}
}
return ret;
}
private List<CameraConfiguration> matchByPath(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs);
// now attempt to match the cameras and configs remaining by normal path
for (CameraConfiguration config : unloadedConfigsCopy) {
CameraInfo cameraInfo;
// attempt matching by path and basename
logger.debug(
"Trying to find a match for loaded camera "
+ config.baseName
+ " with path "
+ config.path);
cameraInfo =
detectedCamInfos.stream()
.filter(
usbCameraInfo ->
usbCameraInfo.path.equals(config.path)
&& usbCameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
// If we actually matched a camera to a config, remove that camera from the list
// and add it to the output
if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
}
return ret;
}
// Try matching cameras to configs by name.
private List<CameraConfiguration> matchByName(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs);
// if both path and ID based matching fails, attempt basename only match
for (CameraConfiguration config : unloadedConfigsCopy) {
CameraInfo cameraInfo;
logger.debug("Trying to find a match for loaded camera with name " + config.baseName);
cameraInfo =
detectedCamInfos.stream()
.filter(CameraInfo -> CameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
// If we actually matched a camera to a config, remove that camera from the list
// and add it to the output
if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
}
return ret;
}
// If we have any unmatched cameras left, create a new CameraConfiguration for
// them here.
/**
* Create new {@link CameraConfiguration}s for unmatched cameras, and assign them a unique name
* (unique in the set of (loaded configs, unloaded configs, loaded vision modules) at least)
*/
private List<CameraConfiguration> createConfigsForCameras(
List<CameraInfo> detectedCameraList,
List<CameraConfiguration> loadedCamConfigs,
List<CameraConfiguration> unloadedCamConfigs,
List<CameraConfiguration> loadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
logger.debug(
@@ -377,7 +432,9 @@ public class VisionSourceManager {
String uniqueName = info.getHumanReadableName();
int suffix = 0;
while (containsName(loadedConfigs, uniqueName) || containsName(uniqueName)) {
while (containsName(loadedConfigs, uniqueName)
|| containsName(uniqueName)
|| containsName(unloadedCamConfigs, uniqueName)) {
suffix++;
uniqueName = String.format("%s (%d)", uniqueName, suffix);
}
@@ -460,7 +517,7 @@ public class VisionSourceManager {
List<CameraConfiguration> camConfigs) {
var cameraSources = new ArrayList<VisionSource>();
for (var configuration : camConfigs) {
logger.debug("Creating VisionSource for " + configuration);
logger.debug("Creating VisionSource for " + camCfgToString(configuration));
boolean is_pi = Platform.isRaspberryPi();

View File

@@ -21,7 +21,9 @@ import java.util.List;
import org.opencv.core.RotatedRect;
import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
public class PotentialTarget implements Releasable {
@@ -29,6 +31,10 @@ public class PotentialTarget implements Releasable {
public final List<Contour> m_subContours;
public final CVShape shape;
// additional metadata about object detections we need to keep around
public final double confidence;
public final int clsId;
public PotentialTarget(Contour inputContour) {
this(inputContour, List.of());
}
@@ -41,12 +47,26 @@ public class PotentialTarget implements Releasable {
m_mainContour = inputContour;
m_subContours = new ArrayList<>(subContours);
this.shape = shape;
this.clsId = -1;
this.confidence = -1;
}
public PotentialTarget(Contour inputContour, CVShape shape) {
this(inputContour, List.of(), shape);
}
public PotentialTarget(NeuralNetworkPipeResult det) {
this.shape = new CVShape(new Contour(det.box), ContourShape.Quadrilateral);
this.m_mainContour = this.shape.getContour();
m_subContours = List.of();
this.clsId = det.classIdx;
this.confidence = det.confidence;
}
public PotentialTarget(CVShape cvShape) {
this(cvShape.getContour(), cvShape);
}
public RotatedRect getMinAreaRect() {
return m_mainContour.getMinAreaRect();
}
@@ -61,7 +81,7 @@ public class PotentialTarget implements Releasable {
for (var sc : m_subContours) {
sc.release();
}
m_subContours.clear();
if (!m_subContours.isEmpty()) m_subContours.clear();
if (shape != null) shape.release();
}
}

View File

@@ -27,7 +27,6 @@ import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect2d;
import org.opencv.core.RotatedRect;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.common.util.math.MathUtils;
@@ -39,7 +38,6 @@ import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
public class TrackedTarget implements Releasable {
public final Contour m_mainContour;
@@ -76,6 +74,9 @@ public class TrackedTarget implements Releasable {
this.m_subContours = origTarget.m_subContours;
this.m_shape = shape;
calculateValues(params);
this.m_classId = origTarget.clsId;
this.m_confidence = origTarget.confidence;
}
public TrackedTarget(
@@ -159,47 +160,6 @@ public class TrackedTarget implements Releasable {
m_robotOffsetPoint = new Point();
}
public TrackedTarget(
Rect2d box, int class_id, double confidence, TargetCalculationParameters params) {
m_targetOffsetPoint = new Point(box.x + box.width / 2.0, box.y + box.height / 2.0);
m_robotOffsetPoint = new Point();
var yawPitch =
TargetCalculations.calculateYawPitch(
params.cameraCenterPoint.x,
box.x + box.width / 2.0,
params.horizontalFocalLength,
params.cameraCenterPoint.y,
box.y + box.height / 2.0,
params.verticalFocalLength);
m_yaw = yawPitch.getFirst();
m_pitch = yawPitch.getSecond();
Point[] cornerPoints =
new Point[] {
// Box.x/y is the top-left corner, not the center
new Point(box.x, box.y), // tl
new Point(box.x + box.width, box.y), // tr
new Point(box.x + box.width, box.y + box.height), // br
new Point(box.x, box.y + box.height), // bl
};
m_targetCorners = List.of(cornerPoints);
MatOfPoint contourMat = new MatOfPoint(cornerPoints);
m_approximateBoundingPolygon = new MatOfPoint2f(cornerPoints);
m_mainContour = new Contour(contourMat);
m_area = m_mainContour.getArea() / params.imageArea * 100;
m_classId = class_id;
m_confidence = confidence;
}
public TrackedTarget(
NeuralNetworkPipeResult t, TargetCalculationParameters targetCalculationParameters) {
this(t.box, t.classIdx, t.confidence, targetCalculationParameters);
}
/**
* @return Returns the confidence of the detection ranging from 0 - 1.
*/

View File

@@ -139,8 +139,31 @@ public class ConfigTest {
writer.write(str);
writer.flush();
writer.close();
Assertions.assertDoesNotThrow(
() -> JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class));
CameraConfiguration result =
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
tempFile.delete();
}
@Test
public void testJacksonAddUSBVIDPID() throws IOException {
var str =
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"usbVID\":3, \"usbPID\":4, \"cameraLEDs\":[]}";
File tempFile = File.createTempFile("test", ".json");
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
try {
CameraConfiguration result =
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
String ser = JacksonUtils.serializeToString(result);
System.out.println(ser);
} catch (Exception e) {
e.printStackTrace();
}
tempFile.delete();
}

View File

@@ -84,7 +84,9 @@ public class SQLConfigTest {
CameraType.UsbCamera,
QuirkyCamera.getQuirkyCamera(-1, -1),
List.of(),
0);
0,
-1,
-1);
testcamcfg.pipelineSettings =
List.of(
new ReflectivePipelineSettings(),

View File

@@ -24,14 +24,20 @@ import java.util.ArrayList;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.vision.camera.CameraInfo;
import org.photonvision.vision.camera.CameraType;
public class VisionSourceManagerTest {
@Test
public void visionSourceTest() {
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
var inst = new VisionSourceManager();
var cameraInfos = new ArrayList<CameraInfo>();
ConfigManager.getInstance().clearConfig();
ConfigManager.getInstance().load();
inst.tryMatchCamImpl(cameraInfos);
@@ -43,6 +49,8 @@ public class VisionSourceManagerTest {
"thirdTestVideo",
"dev/video1",
new String[] {"by-id/123"});
config3.usbVID = 3;
config3.usbPID = 4;
var config4 =
new CameraConfiguration(
"fourthTestVideo",
@@ -50,6 +58,8 @@ public class VisionSourceManagerTest {
"fourthTestVideo",
"dev/video2",
new String[] {"by-id/321"});
config4.usbVID = 5;
config4.usbPID = 6;
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);

View File

@@ -19,7 +19,7 @@ class EstimatedRobotPose:
timestampSeconds: float
"""The estimated time the frame used to derive the robot pose was taken"""
targetsUsed: [PhotonTrackedTarget]
targetsUsed: list[PhotonTrackedTarget]
"""A list of the targets used to compute this pose"""
strategy: "PoseStrategy"

View File

@@ -4,7 +4,7 @@ import wpilib
class Packet:
def __init__(self, data: list[int]):
def __init__(self, data: bytes):
"""
* Constructs an empty packet.
*
@@ -30,7 +30,7 @@ class Packet:
matches the version of photonlib running in the robot code.
"""
def _getNextByte(self) -> int:
def _getNextByteAsInt(self) -> int:
retVal = 0x00
if not self.outOfBytes:
@@ -43,7 +43,7 @@ class Packet:
return retVal
def getData(self) -> list[int]:
def getData(self) -> bytes:
"""
* Returns the packet data.
*
@@ -51,7 +51,7 @@ class Packet:
"""
return self.packetData
def setData(self, data: list[int]):
def setData(self, data: bytes):
"""
* Sets the packet data.
*
@@ -65,7 +65,7 @@ class Packet:
# Read ints in from the data buffer
intList = []
for _ in range(numBytes):
intList.append(self._getNextByte())
intList.append(self._getNextByteAsInt())
# Interpret the bytes as a floating point number
value = struct.unpack(unpackFormat, bytes(intList))[0]

View File

@@ -4,7 +4,7 @@ from wpilib import Timer
import wpilib
from photonlibpy.packet import Packet
from photonlibpy.photonPipelineResult import PhotonPipelineResult
from photonlibpy.version import PHOTONVISION_VERSION, PHOTONLIB_VERSION
from photonlibpy.version import PHOTONVISION_VERSION, PHOTONLIB_VERSION # type: ignore[import-untyped]
class VisionLEDMode(Enum):
@@ -86,10 +86,11 @@ class PhotonCamera:
if len(byteList) < 1:
return retVal
else:
retVal.populateFromPacket(Packet(byteList))
pkt = Packet(byteList)
retVal.populateFromPacket(pkt)
# NT4 allows us to correct the timestamp based on when the message was sent
retVal.setTimestampSeconds(
timestamp / 1e-6 - retVal.getLatencyMillis() / 1e-3
timestamp / 1e6 - retVal.getLatencyMillis() / 1e3
)
return retVal

View File

@@ -38,3 +38,6 @@ class PhotonPipelineResult:
def getTargets(self) -> list[PhotonTrackedTarget]:
return self.targets
def hasTargets(self) -> bool:
return len(self.targets) > 0

View File

@@ -75,7 +75,7 @@ class PhotonPoseEstimator:
self._multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY
self._reportedErrors: set[int] = set()
self._poseCacheTimestampSeconds = -1
self._poseCacheTimestampSeconds = -1.0
self._lastPose: Optional[Pose3d] = None
self._referencePose: Optional[Pose3d] = None
@@ -143,7 +143,7 @@ class PhotonPoseEstimator:
self._multiTagFallbackStrategy = strategy
@property
def referencePose(self) -> Pose3d:
def referencePose(self) -> Optional[Pose3d]:
"""Return the reference position that is being used by the estimator.
:returns: the referencePose
@@ -163,7 +163,7 @@ class PhotonPoseEstimator:
self._referencePose = referencePose
@property
def lastPose(self) -> Pose3d:
def lastPose(self) -> Optional[Pose3d]:
return self._lastPose
@lastPose.setter
@@ -178,10 +178,10 @@ class PhotonPoseEstimator:
self._checkUpdate(self._lastPose, lastPose)
self._lastPose = lastPose
def _invalidatePoseCache(self):
self._poseCacheTimestampSeconds = -1
def _invalidatePoseCache(self) -> None:
self._poseCacheTimestampSeconds = -1.0
def _checkUpdate(self, oldObj, newObj):
def _checkUpdate(self, oldObj, newObj) -> None:
if oldObj != newObj and oldObj is not None and oldObj is not newObj:
self._invalidatePoseCache()
@@ -204,27 +204,27 @@ class PhotonPoseEstimator:
if not cameraResult:
if not self._camera:
wpilib.reportError("[PhotonPoseEstimator] Missing camera!", False)
return
return None
cameraResult = self._camera.getLatestResult()
if cameraResult.timestampSec < 0:
return
return None
# If the pose cache timestamp was set, and the result is from the same
# timestamp, return an
# empty result
if (
self._poseCacheTimestampSeconds > 0
self._poseCacheTimestampSeconds > 0.0
and abs(self._poseCacheTimestampSeconds - cameraResult.timestampSec) < 1e-6
):
return
return None
# Remember the timestamp of the current result used
self._poseCacheTimestampSeconds = cameraResult.timestampSec
# If no targets seen, trivial case -- return empty result
if not cameraResult.targets:
return
return None
return self._update(cameraResult, self._primaryStrategy)
@@ -239,7 +239,7 @@ class PhotonPoseEstimator:
wpilib.reportError(
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False
)
return
return None
if not estimatedPose:
self._lastPose = None
@@ -280,7 +280,7 @@ class PhotonPoseEstimator:
"""
lowestAmbiguityTarget = None
lowestAmbiguityScore = 10
lowestAmbiguityScore = 10.0
for target in result.targets:
targetPoseAmbiguity = target.poseAmbiguity
@@ -293,7 +293,7 @@ class PhotonPoseEstimator:
# Although there are confirmed to be targets, none of them may be fiducial
# targets.
if not lowestAmbiguityTarget:
return
return None
targetFiducialId = lowestAmbiguityTarget.fiducialId
@@ -301,7 +301,7 @@ class PhotonPoseEstimator:
if not targetPosition:
self._reportFiducialPoseError(targetFiducialId)
return
return None
return EstimatedRobotPose(
targetPosition.transformBy(

View File

@@ -408,8 +408,8 @@ public class PhotonPoseEstimator {
return Optional.empty();
}
if (estimatedPose.isEmpty()) {
lastPose = null;
if (estimatedPose.isPresent()) {
lastPose = estimatedPose.get().estimatedPose;
}
return estimatedPose;

View File

@@ -186,6 +186,9 @@ std::optional<EstimatedRobotPose> PhotonPoseEstimator::Update(
ret = std::nullopt;
}
if (ret) {
lastPose = ret.value().estimatedPose;
}
return ret;
}

View File

@@ -435,7 +435,7 @@ class PhotonCameraSim {
double minTargetAreaPercent;
frc::AprilTagFieldLayout tagLayout{
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
cs::CvSource videoSimRaw;
cv::Mat videoSimFrameRaw{};

View File

@@ -350,8 +350,7 @@ public class DataSocketHandler {
}
}
private void sendMessage(Object message, WsContext user) throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
private void sendMessage(ByteBuffer b, WsContext user) throws JsonProcessingException {
if (user.session.isOpen()) {
user.send(b);
}
@@ -359,16 +358,18 @@ public class DataSocketHandler {
public void broadcastMessage(Object message, WsContext userToSkip)
throws JsonProcessingException {
ByteBuffer b = ByteBuffer.wrap(objectMapper.writeValueAsBytes(message));
if (userToSkip == null) {
for (WsContext user : users) {
sendMessage(message, user);
sendMessage(b, user);
}
} else {
var skipUserPort = ((InetSocketAddress) userToSkip.session.getRemoteAddress()).getPort();
for (WsContext user : users) {
var userPort = ((InetSocketAddress) user.session.getRemoteAddress()).getPort();
if (userPort != skipUserPort) {
sendMessage(message, user);
sendMessage(b, user);
}
}
}

View File

@@ -31,6 +31,9 @@ import java.util.HashMap;
import java.util.Optional;
import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfInt;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeDestination;
@@ -580,6 +583,77 @@ public class RequestHandler {
ctx.status(204);
}
public static void onCalibrationSnapshotRequest(Context ctx) {
logger.info(ctx.queryString().toString());
int idx = Integer.parseInt(ctx.queryParam("cameraIdx"));
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
var observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx"));
CameraCalibrationCoefficients calList =
VisionModuleManager.getInstance()
.getModule(idx)
.getStateAsCameraConfig()
.calibrations
.stream()
.filter(
it ->
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null || calList.observations.size() < observationIdx) {
ctx.status(404);
return;
}
// encode as jpeg to save even more space. reduces size of a 1280p image from 300k to 25k
var jpegBytes = new MatOfByte();
Imgcodecs.imencode(
".jpg",
calList.observations.get(observationIdx).snapshotData.getAsMat(),
jpegBytes,
new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 60));
ctx.result(jpegBytes.toArray());
jpegBytes.release();
ctx.status(200);
}
public static void onCalibrationExportRequest(Context ctx) {
logger.info(ctx.queryString().toString());
int idx = Integer.parseInt(ctx.queryParam("cameraIdx"));
var width = Integer.parseInt(ctx.queryParam("width"));
var height = Integer.parseInt(ctx.queryParam("height"));
var cc = VisionModuleManager.getInstance().getModule(idx).getStateAsCameraConfig();
CameraCalibrationCoefficients calList =
cc.calibrations.stream()
.filter(
it ->
Math.abs(it.resolution.width - width) < 1e-4
&& Math.abs(it.resolution.height - height) < 1e-4)
.findFirst()
.orElse(null);
if (calList == null) {
ctx.status(404);
return;
}
var filename = "photon_calibration_" + cc.uniqueName + "_" + width + "x" + height + ".json";
ctx.contentType("application/zip");
ctx.header("Content-Disposition", "attachment; filename=\"" + filename + "\"");
ctx.json(calList);
ctx.status(200);
}
public static void onImageSnapshotsRequest(Context ctx) {
var snapshots = new ArrayList<HashMap<String, Object>>();
var cameraDirs = ConfigManager.getInstance().getImageSavePath().toFile().listFiles();

View File

@@ -130,6 +130,8 @@ public class Server {
app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest);
app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest);
app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest);
app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest);
app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest);
// Calibration
app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest);
@@ -138,7 +140,6 @@ public class Server {
app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest);
app.start(port);
System.out.println("hi");
}
/**

View File

@@ -39,8 +39,8 @@ class Robot : public frc::TimedRobot {
void TeleopPeriodic() override;
private:
// Change this to match the name of your camera
photon::PhotonCamera camera{"photonvision"};
// Change this to match the name of your camera as shown in the web UI
photon::PhotonCamera camera{"YOUR_CAMERA_NAME_HERE"};
// PID constants should be tuned per robot
frc::PIDController controller{.1, 0, 0};

View File

@@ -35,48 +35,48 @@
namespace constants {
namespace Vision {
static constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
static const frc::Transform3d kRobotToCam{
inline constexpr std::string_view kCameraName{"YOUR CAMERA NAME"};
inline const frc::Transform3d kRobotToCam{
frc::Translation3d{0.5_m, 0.0_m, 0.5_m},
frc::Rotation3d{0_rad, 0_rad, 0_rad}};
static const frc::AprilTagFieldLayout kTagLayout{
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp)};
inline const frc::AprilTagFieldLayout kTagLayout{
frc::LoadAprilTagLayoutField(frc::AprilTagField::k2024Crescendo)};
static const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
static const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
inline const Eigen::Matrix<double, 3, 1> kSingleTagStdDevs{4, 4, 8};
inline const Eigen::Matrix<double, 3, 1> kMultiTagStdDevs{0.5, 0.5, 1};
} // namespace Vision
namespace Swerve {
static constexpr units::meter_t kTrackWidth{18.5_in};
static constexpr units::meter_t kTrackLength{18.5_in};
static constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
static constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
static constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
static constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
static constexpr units::meter_t kWheelDiameter{4_in};
static constexpr units::meter_t kWheelCircumference{kWheelDiameter *
inline constexpr units::meter_t kTrackWidth{18.5_in};
inline constexpr units::meter_t kTrackLength{18.5_in};
inline constexpr units::meter_t kRobotWidth{25_in + 3.25_in * 2};
inline constexpr units::meter_t kRobotLength{25_in + 3.25_in * 2};
inline constexpr units::meters_per_second_t kMaxLinearSpeed{15.5_fps};
inline constexpr units::radians_per_second_t kMaxAngularSpeed{720_deg_per_s};
inline constexpr units::meter_t kWheelDiameter{4_in};
inline constexpr units::meter_t kWheelCircumference{kWheelDiameter *
std::numbers::pi};
static constexpr double kDriveGearRatio = 6.75;
static constexpr double kSteerGearRatio = 12.8;
inline constexpr double kDriveGearRatio = 6.75;
inline constexpr double kSteerGearRatio = 12.8;
static constexpr units::meter_t kDriveDistPerPulse =
inline constexpr units::meter_t kDriveDistPerPulse =
kWheelCircumference / 1024.0 / kDriveGearRatio;
static constexpr units::radian_t kSteerRadPerPulse =
inline constexpr units::radian_t kSteerRadPerPulse =
units::radian_t{2 * std::numbers::pi} / 1024.0;
static constexpr double kDriveKP = 1.0;
static constexpr double kDriveKI = 0.0;
static constexpr double kDriveKD = 0.0;
inline constexpr double kDriveKP = 1.0;
inline constexpr double kDriveKI = 0.0;
inline constexpr double kDriveKD = 0.0;
static constexpr double kSteerKP = 20.0;
static constexpr double kSteerKI = 0.0;
static constexpr double kSteerKD = 0.25;
inline constexpr double kSteerKP = 20.0;
inline constexpr double kSteerKI = 0.0;
inline constexpr double kSteerKD = 0.25;
static const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
inline const frc::SimpleMotorFeedforward<units::meters> kDriveFF{
0.25_V, 2.5_V / 1_mps, 0.3_V / 1_mps_sq};
static const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
inline const frc::SimpleMotorFeedforward<units::radians> kSteerFF{
0.5_V, 0.25_V / 1_rad_per_s, 0.01_V / 1_rad_per_s_sq};
struct ModuleConstants {
@@ -106,13 +106,13 @@ struct ModuleConstants {
centerOffset(frc::Translation2d{xOffset, yOffset}) {}
};
static const ModuleConstants FL_CONSTANTS{
inline const ModuleConstants FL_CONSTANTS{
1, 0, 0, 1, 1, 2, 3, 0, kTrackLength / 2, kTrackWidth / 2};
static const ModuleConstants FR_CONSTANTS{
inline const ModuleConstants FR_CONSTANTS{
2, 2, 4, 5, 3, 6, 7, 0, kTrackLength / 2, -kTrackWidth / 2};
static const ModuleConstants BL_CONSTANTS{
inline const ModuleConstants BL_CONSTANTS{
3, 4, 8, 9, 5, 10, 11, 0, -kTrackLength / 2, kTrackWidth / 2};
static const ModuleConstants BR_CONSTANTS{
inline const ModuleConstants BR_CONSTANTS{
4, 6, 12, 13, 7, 14, 15, 0, -kTrackLength / 2, -kTrackWidth / 2};
} // namespace Swerve
} // namespace constants

View File

@@ -61,7 +61,7 @@ class Vision {
cameraSim = std::make_shared<photon::PhotonCameraSim>(camera.get(),
*cameraProp.get());
visionSim->AddCamera(cameraSim.get(), robotToCam);
visionSim->AddCamera(cameraSim.get(), constants::Vision::kRobotToCam);
cameraSim->EnableDrawWireframe(true);
}
}
@@ -138,12 +138,10 @@ class Vision {
frc::Field2d& GetSimDebugField() { return visionSim->GetDebugField(); }
private:
frc::Transform3d robotToCam{frc::Translation3d{0.5_m, 0.5_m, 0.5_m},
frc::Rotation3d{}};
photon::PhotonPoseEstimator photonEstimator{
LoadAprilTagLayoutField(frc::AprilTagField::k2023ChargedUp),
constants::Vision::kTagLayout,
photon::PoseStrategy::MULTI_TAG_PNP_ON_COPROCESSOR,
photon::PhotonCamera{"photonvision"}, robotToCam};
photon::PhotonCamera{"photonvision"}, constants::Vision::kRobotToCam};
std::shared_ptr<photon::PhotonCamera> camera{photonEstimator.GetCamera()};
std::unique_ptr<photon::VisionSystemSim> visionSim;
std::unique_ptr<photon::SimCameraProperties> cameraProp;

View File

@@ -48,8 +48,8 @@ public class Robot extends TimedRobot {
// How far from the target we want to be
final double GOAL_RANGE_METERS = Units.feetToMeters(3);
// Change this to match the name of your camera
PhotonCamera camera = new PhotonCamera("photonvision");
// Change this to match the name of your camera as shown in the web UI
PhotonCamera camera = new PhotonCamera("YOUR_CAMERA_NAME_HERE");
// PID constants should be tuned per robot
final double LINEAR_P = 0.1;

View File

@@ -132,6 +132,10 @@ fi
echo "Installing additional math packages"
apt-get install --yes libcholmod3 liblapack3 libsuitesparseconfig5
echo "Installing v4l-utils..."
apt-get install --yes v4l-utils
echo "v4l-utils installation complete."
echo "Downloading latest stable release of PhotonVision..."
mkdir -p /opt/photonvision
cd /opt/photonvision