Compare commits

...

15 Commits

Author SHA1 Message Date
Sam Freund
9f6d8caf48 Fix calibration resolution default bug (#2156) 2025-10-23 23:42:04 -05:00
Rikhil Chilka
3cbac8117e Merge rknn conversion scripts into notebook (#2157) 2025-10-23 23:41:49 -05:00
Rikhil Chilka
8e88a9a780 Update notebook links in docs to point to docs version (#2155)
Co-authored-by: Sam Freund <samf.236@proton.me>
2025-10-23 16:35:40 -05:00
Rikhil Chilka
7cb3b7a37b Add downgrade fix for ONNX error during RKNN conversion (#2136) 2025-10-23 17:17:31 +00:00
Alan
054ed8b6a1 Add camera mismatch banner to dashboard (#1921)
## Description

Detects if a camera mismatch is present in any camera and displays a
banner in the dashboard for better visibility to the user. All detection
occurs in the backend, and is sent to the frontend via use of a mismatch
boolean included in each vision module.

<img width="1235" alt="image"
src="https://github.com/user-attachments/assets/19219a56-c366-4c56-8c4b-cb5a36fe4a04"
/>

Closes #1920

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [x] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [x] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Sam Freund <techguy763@gmail.com>
Co-authored-by: samfreund <samf.236@proton.me>
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2025-10-21 20:53:22 -04:00
Riley Brewer
d44480ddad Fix typo: s/Specifc/Specific (#2143)
## Description

Simple typo fix:
s/Specifc/Specific

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [x] If this PR changes behavior or adds a feature, user documentation
is updated
- [x] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [x] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [x] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [x] If this PR addresses a bug, a regression test for it is added
2025-10-22 00:18:01 +00:00
Sam Freund
c71921c41e Add documentation for forcing OD UI (#2018) 2025-10-21 19:09:42 -05:00
Michael Jansen
ee4501f1d6 Add Luma P1 support (#2135)
## Description

Adds support for building images for the Luma P1. This bumps the image
modifier pin to v2025.0.4. This pulls in:

* Allow users to install any release via install.sh by @crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/49
* Exit install script if run on systemcore by @crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/58
* Fix --list-versions in install.sh by @crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/59
* Remove large folders of firmware that (probably) isn't needed by
@crschardt in
https://github.com/PhotonVision/photon-image-modifier/pull/41
* Cancel in progress runs by @spacey-sooty in
https://github.com/PhotonVision/photon-image-modifier/pull/65
* Add limelight 4 support by @spacey-sooty in
https://github.com/PhotonVision/photon-image-modifier/pull/52

**Full Changelog**:
https://github.com/PhotonVision/photon-image-modifier/compare/v2025.0.3...v2025.0.4


## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-10-21 10:20:42 -07:00
Gold856
d9b86a718e Revert "Make HardwareConfig a record" (#2142) 2025-10-21 10:56:17 -05:00
Sam Freund
1ac185c247 Fix versioning helper (#2141)
## Description

When we tagged `v2026.0.0-alpha-1`, we broke the versioning-helper
logic. It doesn't expect `alpha` in the version string. This PR adds
matching for lowercase alphanumeric to the versioning helper, which
resolves that issue.

Also turns out `getProviders()` is now broken as a gradle method. Sad.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-10-21 01:14:57 -04:00
Sam Freund
7170c29efe Version rubik pi image (#2129) 2025-10-21 04:43:33 +00:00
Gold856
4f549ba579 Use the tool plugin to include photon-targeting into photon-core (#2137)
## Description

This allows photon-targeting to be loaded using the same mechanism as
the rest of the WPILib libraries, fixing issues with libraries not being
able to find and load their dependent libraries.

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [x] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with settings back to v2025.3.2
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
2025-10-20 07:45:54 -07:00
ElectricTurtle32
b531fe6b81 Made steam overlay buttons primary color (#2139) 2025-10-19 00:59:16 +00:00
Jade
373ed2ff05 Fix most gradle deprecation warnings (#2093) 2025-10-15 22:22:55 -04:00
Jade
115bc09f2e Update LimeLight installation documentation (#2133) 2025-10-15 17:51:43 +00:00
58 changed files with 1291 additions and 1196 deletions

View File

@@ -10,7 +10,7 @@ concurrency:
cancel-in-progress: true
env:
IMAGE_VERSION: v2026.0.3
IMAGE_VERSION: v2026.0.4
jobs:
@@ -445,6 +445,12 @@ jobs:
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_limelight4.img.xz
cpu: cortex-a76
image_additional_mb: 0
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: luma_p1
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_luma_p1.img.xz
cpu: cortex-a76
image_additional_mb: 0
- os: ubuntu-24.04
artifact-name: LinuxArm64
image_suffix: orangepi5
@@ -538,6 +544,12 @@ jobs:
wget https://raw.githubusercontent.com/PhotonVision/photon-image-modifier/refs/tags/$IMAGE_VERSION/mount_rubikpi3.sh
chmod +x mount_rubikpi3.sh
./mount_rubikpi3.sh https://github.com/PhotonVision/photon-image-modifier/releases/download/$IMAGE_VERSION/photonvision_rubikpi3.tar.xz /tmp/build/scripts/armrunner.sh
- name: Compress image
run: |
new_jar=$(realpath $(find . -name photonvision\*-linuxarm64.jar))
new_image_name=$(basename "${new_jar/.jar/_rubikpi3.img}")
mv photonvision_rubikpi3 $new_image_name
tar -I 'xz -T0' -cf ${new_image_name}.tar.xz $new_image_name --checkpoint=10000 --checkpoint-action=echo='%T'
- uses: actions/upload-artifact@v4
name: Upload image
with:

View File

@@ -8,7 +8,7 @@ plugins {
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.3' apply false
id 'edu.wpi.first.GradleJni' version '1.1.0'
id "org.ysb33r.doxygen" version "1.0.4" apply false
id "org.ysb33r.doxygen" version "2.0.0" apply false
id 'com.gradleup.shadow' version '8.3.4' apply false
id "com.github.node-gradle.node" version "7.0.1" apply false
}
@@ -101,7 +101,7 @@ spotless {
}
wrapper {
gradleVersion '8.14.3'
gradleVersion = '8.14.3'
}
ext.getCurrentArch = {

View File

@@ -21,6 +21,29 @@ project = "PhotonVision"
copyright = "2024, PhotonVision"
author = "Banks Troutman, Matt Morley"
# -- Git configuration -----------------------------------------------------
import subprocess
try:
# Use closest tag
git_tag_ref = (
subprocess.check_output(
[
"git",
"describe",
"--tags",
],
stderr=subprocess.DEVNULL,
)
.strip()
.decode()
)
except subprocess.CalledProcessError:
# Couldn't find closest tag, fallback to main
git_tag_ref = "main"
myst_substitutions = {"git_tag_ref": git_tag_ref}
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
@@ -158,4 +181,4 @@ if token:
linkcheck_auth = [(R"https://github.com/.+", token)]
# MyST configuration (https://myst-parser.readthedocs.io/en/latest/configuration.html)
myst_enable_extensions = ["colon_fence"]
myst_enable_extensions = ["colon_fence", "substitution"]

View File

@@ -1,4 +1,4 @@
# Camera-Specifc Configuration
# Camera-Specific Configuration
```{toctree}
:maxdepth: 2

View File

@@ -275,3 +275,9 @@ Using the [GitHub CLI](https://cli.github.com/), we can download artifacts from
MacOS builds are not published to releases as MacOS is not an officially
supported platform. However, MacOS builds are still available from the MacOS
build action, which can be found [here](https://github.com/PhotonVision/photonvision/actions/workflows/build.yml).
#### Forcing Object Detection in the UI
In order to force the Object Detection interface to be visible, it's necessary to hardcode the platform that `Platform.java` returns. This can be done by changing the function that detects the RK3588S/QCS6490 platform to always return true, and changing the `getCurrentPlatform()` function to always return the RK3588S/QCS6490 architecture.
Alternatively, it's possible to modify the frontend code by changing all instances of `useSettingsStore().general.supportedBackends.length > 0` to `true`, which will force the card to render.
Make sure to revert these changes before submitting a Pull Request.

View File

@@ -14,6 +14,6 @@ PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOL
Only quantized models are supported, so take care when exporting to select the option for quantization.
:::
PhotonVision now ships with a [Python Notebook](https://github.com/PhotonVision/photonvision/blob/main/scripts/rknn-convert-tool/rknn_conversion.ipynb) that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rknn_conversion.ipynb` notebook without needing to manually download anything.
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rknn_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rknn_conversion.ipynb` notebook without needing to manually download anything.
Please ensure that the model you are attempting to convert is among the {ref}`supported models <docs/objectDetection/opi:Supported Models>` and using the PyTorch format.

View File

@@ -14,7 +14,7 @@ PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv8 and YOLOv11 mode
Only quantized models are supported, so take care when exporting to select the option for quantization.
:::
PhotonVision now ships with a [Python Notebook](https://github.com/PhotonVision/photonvision/blob/main/scripts/rubik_conversion.ipynb) that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rubik_conversion.ipynb` notebook without needing to manually download anything.
PhotonVision now ships with a {{ '[Python Notebook](https://github.com/PhotonVision/photonvision/blob/{}/scripts/rubik_conversion.ipynb)'.format(git_tag_ref) }} that you can use in [Google Colab](https://colab.research.google.com) or in a local environment. In Google Colab, you can simply paste the PhotonVision GitHub URL into the "GitHub" tab and select the `rubik_conversion.ipynb` notebook without needing to manually download anything.
Please ensure that the model you are attempting to convert is among the {ref}`supported models <docs/objectDetection/rubik:Supported Models>` and using the PyTorch format.

View File

@@ -34,14 +34,17 @@ Balena Etcher can also be used, but historically has had issues such as bootloop
## Limelight Installation
:::{note}
In order to mount the Limelight 4 on your computer, it's necessary to use `rpiboot`. To do this, follow the instructions [here](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-4#4-updating-limelightos).
:::
In order to flash your Limelight you should follow the instructions on the Limelight documentation for the relevant version. Make sure to replace the Limelight OS image with the relevant PhotonVision image.
Limelights have a different installation processes. Simply connect the limelight to your computer using the proper usb cable. Select the compute module in the [Raspberry Pi Imager](https://www.raspberrypi.com/software/). If it doesnt show up after 30s try using another USB port, initialization may take a while. If prompted, install the recommended missing drivers. Select the image, and flash.
| Limelight Version | Limelight Documentation | PhotonVision Image | |
| ----------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --- |
| 2 | [Updating Limelight 2 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-2#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight2.img.xz | |
| 3 | [Updating Limelight 3 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-3#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight3.img.xz | |
| 3G | [Updating Limelight 3G OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-3g#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight3g.img.xz | |
| 4 | [Updating Limelight 4 OS](https://docs.limelightvision.io/docs/docs-limelight/getting-started/limelight-4#4-updating-limelightos) | photonvision-{version}-linuxarm64_limelight4.img.xz | |
:::{note}
Limelight 2, 2+, and 3 will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for lighting to work.
Limelight models will need a [custom hardware config file](https://github.com/PhotonVision/photonvision/tree/main/docs/source/docs/advanced-installation/sw_install/files) for LEDs or other hardware features to work.
:::
## Rubik Pi 3 Installation

View File

@@ -105,18 +105,21 @@ onBeforeUnmount(() => {
/>
<div class="stream-overlay" :style="overlayStyle">
<pv-icon
color="primary"
icon-name="mdi-camera-image"
tooltip="Capture and save a frame of this stream"
class="ma-1 mr-2"
@click="handleCaptureClick"
/>
<pv-icon
color="primary"
icon-name="mdi-fullscreen"
tooltip="Open this stream in fullscreen"
class="ma-1 mr-2"
@click="handleFullscreenRequest"
/>
<pv-icon
color="primary"
icon-name="mdi-open-in-new"
tooltip="Open this stream in a new window"
class="ma-1 mr-2"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { CalibrationBoardTypes, CalibrationTagFamilies, type VideoFormat } from "@/types/SettingTypes";
import MonoLogo from "@/assets/images/logoMono.png";
@@ -79,6 +79,18 @@ const calibrationDivisors = computed(() =>
})
);
const uniqueVideoResolutionString = ref("");
// Use a watchEffect so the value is populated/reacts when the stores become available or update.
// This avoids trying to index into an array that may be empty during page reload.
watchEffect(() => {
const currentIndex = useCameraSettingsStore().currentVideoFormat.index ?? 0;
useStateStore().calibrationData.videoFormatIndex = currentIndex;
const names = useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
getResolutionString(f.resolution)
);
uniqueVideoResolutionString.value = names[currentIndex] ?? names[0] ?? "";
});
const squareSizeIn = ref(1);
const markerSizeIn = ref(0.75);
const patternWidth = ref(8);
@@ -279,13 +291,16 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
: 'MrCal failed to load, check journalctl logs for details.'
"
/>
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
v-model="uniqueVideoResolutionString"
label="Resolution"
:select-cols="8"
:disabled="isCalibrating"
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
@update:model-value="
useStateStore().calibrationData.videoFormatIndex =
getUniqueVideoResolutionStrings().find((v) => v.value === $event)?.value || 0
"
:items="getUniqueVideoResolutionStrings()"
/>
<pv-select
@@ -527,9 +542,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-card-text>
Camera has been successfully calibrated for
{{
getUniqueVideoResolutionStrings().find(
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
)?.name
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map((f) =>
getResolutionString(f.resolution)
)[useStateStore().calibrationData.videoFormatIndex]
}}!
</v-card-text>
</template>

View File

@@ -142,7 +142,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
matchedCameraInfo: d.matchedCameraInfo,
isConnected: d.isConnected,
hasConnected: d.hasConnected
hasConnected: d.hasConnected,
mismatch: d.mismatch
};
return acc;
}, {});

View File

@@ -266,6 +266,7 @@ export interface UiCameraConfiguration {
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;
}
export interface CameraSettingsChangeRequest {
@@ -388,7 +389,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
PVUsbCameraInfo: undefined
},
isConnected: true,
hasConnected: true
hasConnected: true,
mismatch: false
};
export enum CalibrationBoardTypes {

View File

@@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;
}
export interface WebsocketNTUpdate {
connected: boolean;

View File

@@ -168,64 +168,7 @@ const deleteThisCamera = (cameraName: string) => {
});
};
const camerasMatch = (camera1: PVCameraInfo, camera2: PVCameraInfo) => {
if (camera1.PVUsbCameraInfo && camera2.PVUsbCameraInfo)
return (
camera1.PVUsbCameraInfo.name === camera2.PVUsbCameraInfo.name &&
camera1.PVUsbCameraInfo.vendorId === camera2.PVUsbCameraInfo.vendorId &&
camera1.PVUsbCameraInfo.productId === camera2.PVUsbCameraInfo.productId &&
camera1.PVUsbCameraInfo.uniquePath === camera2.PVUsbCameraInfo.uniquePath
);
else if (camera1.PVCSICameraInfo && camera2.PVCSICameraInfo)
return (
camera1.PVCSICameraInfo.uniquePath === camera2.PVCSICameraInfo.uniquePath &&
camera1.PVCSICameraInfo.baseName === camera2.PVCSICameraInfo.baseName
);
else if (camera1.PVFileCameraInfo && camera2.PVFileCameraInfo)
return (
camera1.PVFileCameraInfo.uniquePath === camera2.PVFileCameraInfo.uniquePath &&
camera1.PVFileCameraInfo.name === camera2.PVFileCameraInfo.name
);
else return false;
};
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/**
* Find the PVCameraInfo currently occupying the same uniquepath as the the given module
*/
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
};
}
return (
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}
);
};
const cameraCononected = (uniquePath: string): boolean => {
const cameraConnected = (uniquePath: string): boolean => {
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
@@ -252,8 +195,8 @@ const activeVisionModules = computed(() =>
// Display connected cameras first
.sort(
(first, second) =>
(cameraCononected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraCononected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
)
);
@@ -274,6 +217,45 @@ const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettin
cameraToDelete.value = camera;
};
const yesDeleteMySettingsText = ref("");
/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/**
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
*/
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
};
}
return (
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}
);
};
</script>
<template>
@@ -290,14 +272,11 @@ const yesDeleteMySettingsText = ref("");
>
<v-card color="surface" class="rounded-12">
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
>
<v-card-subtitle
v-else-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)
"
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
>Status: <span class="active-status">Active</span></v-card-subtitle
>
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
@@ -306,7 +285,7 @@ const yesDeleteMySettingsText = ref("");
<tbody>
<tr
v-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
@@ -348,7 +327,7 @@ const yesDeleteMySettingsText = ref("");
</tbody>
</v-table>
<div
v-if="cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
:id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center mt-3"
style="height: 250px"
@@ -370,7 +349,7 @@ const yesDeleteMySettingsText = ref("");
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
@@ -441,7 +420,7 @@ const yesDeleteMySettingsText = ref("");
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
</tbody>
</v-table>
@@ -456,7 +435,7 @@ const yesDeleteMySettingsText = ref("");
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
@@ -562,7 +541,13 @@ const yesDeleteMySettingsText = ref("");
<v-card-text v-if="!viewingCamera[1]">
<PvCameraInfoCard :camera="viewingCamera[0]" />
</v-card-text>
<v-card-text v-else-if="!camerasMatch(getMatchedDevice(viewingCamera[0]), viewingCamera[0])">
<v-card-text
v-else-if="
activeVisionModules.find(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
)?.mismatch
"
>
<v-alert
class="mb-3"
color="buttonActive"

View File

@@ -10,6 +10,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
import { useTheme } from "vuetify";
const theme = useTheme();
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
const cameraViewType = computed<number[]>({
get: (): number[] => {
@@ -54,6 +55,17 @@ const arducamWarningShown = computed<boolean>(() => {
);
});
const cameraMismatchWarningShown = computed<boolean>(() => {
return (
Object.values(useCameraSettingsStore().cameras)
// Ignore placeholder camera
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
.some((camera) => {
return camera.mismatch;
})
);
});
const conflictingHostnameShown = computed<boolean>(() => {
return useSettingsStore().general.conflictingHostname;
});
@@ -104,6 +116,21 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
{{ useSettingsStore().general.conflictingCameras }}!
</span>
</v-alert>
<v-banner
v-if="cameraMismatchWarningShown"
v-model="cameraMismatchWarningShown"
rounded
color="error"
dark
class="mb-3"
icon="mdi-alert-circle-outline"
>
<span
>Camera Mismatch Detected! Visit the <a href="#/cameraConfigs">Camera Matching</a> page for more information.
Note: Camera matching is done by USB port. Ensure cameras are plugged into the same USB ports as when they were
activated.
</span>
</v-banner>
<v-row no-gutters>
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
<CamerasCard v-model="cameraViewType" />

View File

@@ -8,24 +8,26 @@ apply from: "${rootDir}/shared/common.gradle"
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()
def nativeConfigName = 'wpilibNatives'
def nativeConfig = configurations.create(nativeConfigName)
configurations {
wpilibNatives
}
def nativeTasks = wpilibTools.createExtractionTasks {
configurationName = nativeConfigName
}
nativeTasks.addToSourceSetResources(sourceSets.main)
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
dependencies {
wpilibNatives project(path: ':photon-targeting', configuration: 'wpilibNatives')
wpilibNatives wpilibTools.deps.wpilib("wpimath")
wpilibNatives wpilibTools.deps.wpilib("wpinet")
wpilibNatives wpilibTools.deps.wpilib("wpiutil")
wpilibNatives wpilibTools.deps.wpilib("ntcore")
wpilibNatives wpilibTools.deps.wpilib("cscore")
wpilibNatives wpilibTools.deps.wpilib("apriltag")
wpilibNatives wpilibTools.deps.wpilib("hal")
wpilibNatives wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
// Zip
implementation 'org.zeroturnaround:zt-zip:1.14'

View File

@@ -21,57 +21,102 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.ArrayList;
@JsonIgnoreProperties(ignoreUnknown = true)
public record HardwareConfig(
String deviceName,
String deviceLogoPath,
String supportURL,
// LED control
public class HardwareConfig {
public final String deviceName;
public final String deviceLogoPath;
public final String supportURL;
ArrayList<Integer> ledPins,
String ledSetCommand,
boolean ledsCanDim,
ArrayList<Integer> ledBrightnessRange,
String ledDimCommand,
String ledBlinkCommand,
ArrayList<Integer> statusRGBPins,
// Metrics
// LED control
public final ArrayList<Integer> ledPins;
public final String ledSetCommand;
public final boolean ledsCanDim;
public final ArrayList<Integer> ledBrightnessRange;
public final String ledDimCommand;
public final String ledBlinkCommand;
public final ArrayList<Integer> statusRGBPins;
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
String cpuThrottleReasonCmd,
String cpuUptimeCommand,
String gpuMemoryCommand,
String ramUtilCommand,
String gpuMemUsageCommand,
String diskUsageCommand,
// Device stuff
String restartHardwareCommand,
double vendorFOV) { // -1 for unmanaged
// Metrics
public final String cpuTempCommand;
public final String cpuMemoryCommand;
public final String cpuUtilCommand;
public final String cpuThrottleReasonCmd;
public final String cpuUptimeCommand;
public final String gpuMemoryCommand;
public final String ramUtilCommand;
public final String gpuMemUsageCommand;
public final String diskUsageCommand;
// Device stuff
public final String restartHardwareCommand;
public final double vendorFOV; // -1 for unmanaged
public HardwareConfig(
String deviceName,
String deviceLogoPath,
String supportURL,
ArrayList<Integer> ledPins,
String ledSetCommand,
boolean ledsCanDim,
ArrayList<Integer> ledBrightnessRange,
String ledDimCommand,
String ledBlinkCommand,
ArrayList<Integer> statusRGBPins,
String cpuTempCommand,
String cpuMemoryCommand,
String cpuUtilCommand,
String cpuThrottleReasonCmd,
String cpuUptimeCommand,
String gpuMemoryCommand,
String ramUtilCommand,
String gpuMemUsageCommand,
String diskUsageCommand,
String restartHardwareCommand,
double vendorFOV) {
this.deviceName = deviceName;
this.deviceLogoPath = deviceLogoPath;
this.supportURL = supportURL;
this.ledPins = ledPins;
this.ledSetCommand = ledSetCommand;
this.ledsCanDim = ledsCanDim;
this.ledBrightnessRange = ledBrightnessRange;
this.ledDimCommand = ledDimCommand;
this.ledBlinkCommand = ledBlinkCommand;
this.statusRGBPins = statusRGBPins;
this.cpuTempCommand = cpuTempCommand;
this.cpuMemoryCommand = cpuMemoryCommand;
this.cpuUtilCommand = cpuUtilCommand;
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
this.cpuUptimeCommand = cpuUptimeCommand;
this.gpuMemoryCommand = gpuMemoryCommand;
this.ramUtilCommand = ramUtilCommand;
this.gpuMemUsageCommand = gpuMemUsageCommand;
this.diskUsageCommand = diskUsageCommand;
this.restartHardwareCommand = restartHardwareCommand;
this.vendorFOV = vendorFOV;
}
public HardwareConfig() {
this(
"", // deviceName
"", // deviceLogoPath
"", // supportURL
new ArrayList<>(), // ledPins
"", // ledSetCommand
false, // ledsCanDim
new ArrayList<>(), // ledBrightnessRange
"", // ledDimCommand
"", // ledBlinkCommand
new ArrayList<>(), // statusRGBPins
"", // cpuTempCommand
"", // cpuMemoryCommand
"", // cpuUtilCommand
"", // cpuThrottleReasonCmd
"", // cpuUptimeCommand
"", // gpuMemoryCommand
"", // ramUtilCommand
"", // gpuMemUsageCommand
"", // diskUsageCommand
"", // restartHardwareCommand
-1); // vendorFOV
deviceName = "";
deviceLogoPath = "";
supportURL = "";
ledPins = new ArrayList<>();
ledSetCommand = "";
ledsCanDim = false;
ledBrightnessRange = new ArrayList<>();
ledDimCommand = "";
ledBlinkCommand = "";
statusRGBPins = new ArrayList<>();
cpuTempCommand = "";
cpuMemoryCommand = "";
cpuUtilCommand = "";
cpuThrottleReasonCmd = "";
cpuUptimeCommand = "";
gpuMemoryCommand = "";
ramUtilCommand = "";
gpuMemUsageCommand = "";
diskUsageCommand = "";
restartHardwareCommand = "";
vendorFOV = -1;
}
/**
@@ -96,4 +141,51 @@ public record HardwareConfig(
|| gpuMemUsageCommand != ""
|| diskUsageCommand != "";
}
@Override
public String toString() {
return "HardwareConfig[deviceName="
+ deviceName
+ ", deviceLogoPath="
+ deviceLogoPath
+ ", supportURL="
+ supportURL
+ ", ledPins="
+ ledPins
+ ", ledSetCommand="
+ ledSetCommand
+ ", ledsCanDim="
+ ledsCanDim
+ ", ledBrightnessRange="
+ ledBrightnessRange
+ ", ledDimCommand="
+ ledDimCommand
+ ", ledBlinkCommand="
+ ledBlinkCommand
+ ", statusRGBPins="
+ statusRGBPins
+ ", cpuTempCommand="
+ cpuTempCommand
+ ", cpuMemoryCommand="
+ cpuMemoryCommand
+ ", cpuUtilCommand="
+ cpuUtilCommand
+ ", cpuThrottleReasonCmd="
+ cpuThrottleReasonCmd
+ ", cpuUptimeCommand="
+ cpuUptimeCommand
+ ", gpuMemoryCommand="
+ gpuMemoryCommand
+ ", ramUtilCommand="
+ ramUtilCommand
+ ", gpuMemUsageCommand="
+ gpuMemUsageCommand
+ ", diskUsageCommand="
+ diskUsageCommand
+ ", restartHardwareCommand="
+ restartHardwareCommand
+ ", vendorFOV="
+ vendorFOV
+ "]";
}
}

View File

@@ -70,6 +70,8 @@ public class NetworkTablesManager {
// Creating the alert up here since it should be persistent
private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
private final Alert mismatchAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
public boolean conflictingHostname = false;
public String conflictingCameras = "";
private String currentMacAddress;
@@ -95,6 +97,7 @@ public class NetworkTablesManager {
// This should start as false, since we don't know if there's a conflict yet
conflictAlert.set(false);
mismatchAlert.set(false);
// Get the UI state in sync with the backend. NT should fire a callback when it
// first connects to the robot
@@ -115,6 +118,14 @@ public class NetworkTablesManager {
return INSTANCE;
}
public void setMismatchAlert(boolean on, String message) {
if (mismatchAlert != null) {
mismatchAlert.set(on);
mismatchAlert.setText(message);
SmartDashboard.updateValues();
}
}
private void logNtMessage(NetworkTableEvent event) {
String levelmsg = "DEBUG";
LogLevel pvlevel = LogLevel.DEBUG;

View File

@@ -25,7 +25,6 @@ import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.TimeSyncClient;
import org.photonvision.jni.TimeSyncServer;
@@ -43,10 +42,6 @@ public class TimeSyncManager {
IntegerPublisher m_lastPongTimePub;
public TimeSyncManager(NetworkTable kRootTable) {
if (!PhotonTargetingJniLoader.isWorking) {
logger.error("PhotonTargetingJNI was not loaded! Cannot do time-sync");
}
this.ntInstance = kRootTable.getInstance();
// Need this subtable to be unique per coprocessor. TODO: consider using MAC address or
@@ -65,18 +60,10 @@ public class TimeSyncManager {
// Since we're spinning off tasks in a new thread, be careful and start it seperately
public void start() {
if (!PhotonTargetingJniLoader.isWorking) {
logger.error("PhotonTargetingJNI was not loaded! Cannot start");
}
TimedTaskManager.getInstance().addTask("TimeSyncManager::tick", this::tick, 1000);
}
public synchronized long getOffset() {
if (!PhotonTargetingJniLoader.isWorking) {
return 0;
}
// if we're a client, return the offset to server time
if (m_client != null) return m_client.getOffset();
// if we're a server, our time (nt::Now) is the same as network time
@@ -88,10 +75,6 @@ public class TimeSyncManager {
}
synchronized void setConfig(NetworkConfig config) {
if (!PhotonTargetingJniLoader.isWorking) {
return;
}
if (m_client == null && m_server == null) {
throw new RuntimeException("Neither client nor server are null?");
}

View File

@@ -52,6 +52,7 @@ public class UICameraConfiguration {
public double minWhiteBalanceTemp;
public double maxWhiteBalanceTemp;
public PVCameraInfo matchedCameraInfo;
public boolean mismatch;
// Status for if the underlying device is present and such
public boolean isConnected;

View File

@@ -49,7 +49,7 @@ public class UIPhotonConfiguration {
NetworkManager.getInstance().networkingIsDisabled),
new UILightingConfig(
c.getHardwareSettings().ledBrightnessPercentage,
!c.getHardwareConfig().ledPins().isEmpty()),
!c.getHardwareConfig().ledPins.isEmpty()),
new UIGeneralSettings(
PhotonVersion.versionString,
// TODO add support for other types of GPU accel
@@ -57,9 +57,9 @@ public class UIPhotonConfiguration {
MrCalJNILoader.getInstance().isLoaded(),
c.neuralNetworkPropertyManager().getModels(),
NeuralNetworkModelManager.getInstance().getSupportedBackends(),
c.getHardwareConfig().deviceName().isEmpty()
c.getHardwareConfig().deviceName.isEmpty()
? Platform.getHardwareModel()
: c.getHardwareConfig().deviceName(),
: c.getHardwareConfig().deviceName,
Platform.getPlatformName(),
NetworkTablesManager.getInstance().conflictingHostname,
NetworkTablesManager.getInstance().conflictingCameras),

View File

@@ -92,8 +92,8 @@ public class CustomGPIO extends GPIOBase {
public static void setConfig(HardwareConfig config) {
if (Platform.isRaspberryPi()) return;
commands.replace("setState", config.ledSetCommand());
commands.replace("dim", config.ledDimCommand());
commands.replace("blink", config.ledBlinkCommand());
commands.replace("setState", config.ledSetCommand);
commands.replace("dim", config.ledDimCommand);
commands.replace("blink", config.ledBlinkCommand);
}
}

View File

@@ -97,22 +97,22 @@ public class HardwareManager {
}
statusLED =
hardwareConfig.statusRGBPins().size() == 3
? new StatusLED(hardwareConfig.statusRGBPins())
hardwareConfig.statusRGBPins.size() == 3
? new StatusLED(hardwareConfig.statusRGBPins)
: null;
if (statusLED != null) {
TimedTaskManager.getInstance().addTask("StatusLEDUpdate", this::statusLEDUpdate, 150);
}
var hasBrightnessRange = hardwareConfig.ledBrightnessRange().size() == 2;
var hasBrightnessRange = hardwareConfig.ledBrightnessRange.size() == 2;
visionLED =
hardwareConfig.ledPins().isEmpty()
hardwareConfig.ledPins.isEmpty()
? null
: new VisionLED(
hardwareConfig.ledPins(),
hasBrightnessRange ? hardwareConfig.ledBrightnessRange().get(0) : 0,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange().get(1) : 100,
hardwareConfig.ledPins,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(0) : 0,
hasBrightnessRange ? hardwareConfig.ledBrightnessRange.get(1) : 100,
pigpioSocket,
ledModeState::set);
@@ -161,7 +161,7 @@ public class HardwareManager {
}
}
try {
return shellExec.executeBashCommand(hardwareConfig.restartHardwareCommand()) == 0;
return shellExec.executeBashCommand(hardwareConfig.restartHardwareCommand) == 0;
} catch (IOException e) {
logger.error("Could not restart device!", e);
return false;

View File

@@ -22,18 +22,18 @@ import org.photonvision.common.configuration.HardwareConfig;
public class FileCmds extends CmdBase {
@Override
public void initCmds(HardwareConfig config) {
cpuTemperatureCommand = config.cpuTempCommand();
cpuUtilizationCommand = config.cpuUtilCommand();
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd();
cpuTemperatureCommand = config.cpuTempCommand;
cpuUtilizationCommand = config.cpuUtilCommand;
cpuThrottleReasonCmd = config.cpuThrottleReasonCmd;
ramMemCommand = config.cpuMemoryCommand();
ramUtilCommand = config.ramUtilCommand();
ramMemCommand = config.cpuMemoryCommand;
ramUtilCommand = config.ramUtilCommand;
gpuMemCommand = config.gpuMemoryCommand();
gpuMemUtilCommand = config.gpuMemUsageCommand();
gpuMemCommand = config.gpuMemoryCommand;
gpuMemUtilCommand = config.gpuMemUsageCommand;
diskUsageCommand = config.diskUsageCommand();
diskUsageCommand = config.diskUsageCommand;
uptimeCommand = config.cpuUptimeCommand();
uptimeCommand = config.cpuUptimeCommand;
}
}

View File

@@ -26,14 +26,14 @@ import java.io.IOException;
import java.nio.file.Path;
import org.opencv.core.Mat;
import org.opencv.highgui.HighGui;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TrackedTarget;
public class TestUtils {
public static boolean loadLibraries() {
return WpilibLoader.loadLibraries();
return LibraryLoader.loadWpiLibraries() && LibraryLoader.loadTargeting();
}
@SuppressWarnings("unused")

View File

@@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.Arrays;
import java.util.Objects;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -70,8 +71,15 @@ public sealed interface PVCameraInfo {
CameraType type();
/**
* Default equals implementation that delegates to the implementing class's equals method. This
* method checks type compatibility first, then delegates to the actual implementation.
*/
default boolean equals(PVCameraInfo other) {
return uniquePath().equals(other.uniquePath());
if (other == null) return false;
if (this.type() != other.type()) return false;
// Delegate to the actual equals(Object) implementation of this instance
return this.equals((Object) other);
}
@JsonTypeName("PVUsbCameraInfo")
@@ -125,7 +133,17 @@ public sealed interface PVCameraInfo {
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
return obj instanceof PVCameraInfo info && equals(info);
if (!(obj instanceof PVUsbCameraInfo info)) return false;
return super.name.equals(info.name)
&& super.vendorId == info.vendorId
&& super.productId == info.productId
&& uniquePath().equals(info.uniquePath());
}
@Override
public int hashCode() {
return Objects.hash(super.name, super.vendorId, super.productId, uniquePath());
}
@Override
@@ -191,7 +209,14 @@ public sealed interface PVCameraInfo {
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
return obj instanceof PVCameraInfo info && equals(info);
if (!(obj instanceof PVCSICameraInfo info)) return false;
return baseName.equals(info.baseName) && path.equals(info.path);
}
@Override
public int hashCode() {
return Objects.hash(baseName, path);
}
@Override
@@ -248,7 +273,14 @@ public sealed interface PVCameraInfo {
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
return obj instanceof PVFileCameraInfo info && equals(info);
if (!(obj instanceof PVFileCameraInfo info)) return false;
return name.equals(info.name) && path.equals(info.path);
}
@Override
public int hashCode() {
return Objects.hash(name, path);
}
@Override

View File

@@ -100,7 +100,7 @@ public class LibcameraGpuSource extends VisionSource {
@Override
public boolean hasLEDs() {
return (ConfigManager.getInstance().getConfig().getHardwareConfig().ledPins().size() > 0);
return (ConfigManager.getInstance().getConfig().getHardwareConfig().ledPins.size() > 0);
}
@Override

View File

@@ -93,6 +93,8 @@ public class VisionModule {
MJPGFrameConsumer inputVideoStreamer;
MJPGFrameConsumer outputVideoStreamer;
boolean mismatch;
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource) {
logger =
new Logger(
@@ -100,6 +102,8 @@ public class VisionModule {
visionSource.getSettables().getConfiguration().nickname,
LogGroup.VisionModule);
mismatch = false;
cameraQuirks = visionSource.getCameraConfiguration().cameraQuirks;
if (visionSource.getCameraConfiguration().cameraQuirks == null)
@@ -160,7 +164,7 @@ public class VisionModule {
// Set vendor FOV
if (isVendorCamera()) {
var fov = ConfigManager.getInstance().getConfig().getHardwareConfig().vendorFOV();
var fov = ConfigManager.getInstance().getConfig().getHardwareConfig().vendorFOV;
logger.info("Setting FOV of vendor camera to " + fov);
visionSource.getSettables().setFOV(fov);
}
@@ -568,6 +572,8 @@ public class VisionModule {
ret.deactivated = config.deactivated;
ret.mismatch = this.mismatch;
// TODO refactor into helper method
var temp = new HashMap<Integer, HashMap<String, Object>>();
var videoModes = visionSource.getSettables().getAllVideoModes();

View File

@@ -31,6 +31,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
import org.photonvision.common.hardware.Platform;
@@ -311,16 +312,118 @@ public class VisionSourceManager {
.forEach(cameraInfos::add);
}
// FileVisionSources are a bit quirky. They aren't enumerated by the above, but i still want my
// FileVisionSources are a bit quirky. They aren't enumerated by the above, but I still want my
// UI to look like it ought to work
vmm.getModules().stream()
.map(it -> it.getCameraConfiguration().matchedCameraInfo)
.filter(info -> info instanceof PVCameraInfo.PVFileCameraInfo)
.forEach(cameraInfos::add);
checkMismatches(cameraInfos);
return cameraInfos;
}
/**
* Check for mismatches between connected cameras and saved camera configurations.
*
* <p>Note that if the information for a camera spontaneously changes without it being
* disconnected/unplugged and reconnected/replugged, we may experience unexpected behavior.
*
* @param cameraInfos List of currently connected camera infos, checked against saved configs
*/
protected void checkMismatches(List<PVCameraInfo> cameraInfos) {
// from the listed physical camera infos, match them to the camera configs and check for
// mismatches
for (VisionModule module : vmm.getModules()) {
PVCameraInfo matchedCameraInfo = module.getCameraConfiguration().matchedCameraInfo;
// We use unique paths to determine if the module has a camera in the port. If no unique path
// is found that matches the module, it's removed from the mismatched set as a disconnected
// camera cannot be mismatched.
if (!cameraInfos.stream()
.map(PVCameraInfo::uniquePath)
.toList()
.contains(matchedCameraInfo.uniquePath())) {
module.mismatch = false;
continue;
}
for (PVCameraInfo info : cameraInfos) {
// if the unique path doesn't match, skip cause it's not in the same port
if (!matchedCameraInfo.uniquePath().equals(info.uniquePath())) {
continue;
}
// If the camera info doesn't match, log an error
if (!matchedCameraInfo.equals(info) && !module.mismatch) {
logger.error("Camera mismatch error!");
logger.error("Camera config mismatch for " + matchedCameraInfo.name());
logCameraInfoDiff(matchedCameraInfo, info);
module.mismatch = true;
}
}
}
// Set the NetworkTables mismatch alert
if (vmm.getModules().stream().anyMatch(m -> m.mismatch)) {
NetworkTablesManager.getInstance()
.setMismatchAlert(
true,
"Camera mismatch error! See logs for details. ("
+ vmm.getModules().stream()
.filter(m -> m.mismatch)
.map(m -> m.getCameraConfiguration().nickname)
.toList()
.toString()
.replaceAll("[\\[\\]()]", "")
+ " affected)");
} else {
NetworkTablesManager.getInstance().setMismatchAlert(false, "");
}
}
/** Log the differences between two PVCameraInfo objects. */
private static void logCameraInfoDiff(PVCameraInfo saved, PVCameraInfo current) {
String expected = "Expected: Name: " + saved.name();
String actual = "Actual: Name: " + current.name();
if (saved instanceof PVCameraInfo.PVCSICameraInfo savedCsi
&& current instanceof PVCameraInfo.PVCSICameraInfo currentCsi) {
expected += " Base Name: " + savedCsi.baseName;
actual += " Base Name: " + currentCsi.baseName;
}
expected += " Type: " + saved.type().toString();
actual += " Type: " + current.type().toString();
if (saved instanceof PVCameraInfo.PVUsbCameraInfo savedUsb
&& current instanceof PVCameraInfo.PVUsbCameraInfo currentUsb) {
expected +=
" Device Number: "
+ savedUsb.dev
+ " Vendor ID: "
+ savedUsb.vendorId
+ " Product ID: "
+ savedUsb.productId;
actual +=
" Device Number: "
+ currentUsb.dev
+ " Vendor ID: "
+ currentUsb.vendorId
+ " Product ID: "
+ currentUsb.productId;
}
expected += " Path: " + saved.path();
actual += " Path: " + current.path();
expected += " Unique Path: " + saved.uniquePath();
actual += " Unique Path: " + current.uniquePath();
expected += " Other Paths: " + Arrays.toString(saved.otherPaths());
actual += " Other Paths: " + Arrays.toString(current.otherPaths());
logger.error(expected);
logger.error(actual);
}
private static List<PVCameraInfo> filterAllowedDevices(List<PVCameraInfo> allDevices) {
Platform platform = Platform.getCurrentPlatform();
ArrayList<PVCameraInfo> filteredDevices = new ArrayList<>();

View File

@@ -34,10 +34,13 @@ public class HardwareConfigTest {
System.out.println("Loading Hardware configs...");
var config =
new ObjectMapper().readValue(TestUtils.getHardwareConfigJson(), HardwareConfig.class);
assertEquals(config.deviceName(), "PhotonVision");
assertEquals(config.deviceLogoPath(), "photonvision.png");
assertEquals(config.supportURL(), "https://support.photonvision.com");
assertArrayEquals(config.ledPins().stream().mapToInt(i -> i).toArray(), new int[] {2, 13});
assertEquals(config.deviceName, "PhotonVision");
assertEquals(config.deviceLogoPath, "photonvision.png");
assertEquals(config.supportURL, "https://support.photonvision.com");
// Ensure defaults are not null
assertEquals(config.cpuThrottleReasonCmd, "");
assertEquals(config.diskUsageCommand, "");
assertArrayEquals(config.ledPins.stream().mapToInt(i -> i).toArray(), new int[] {2, 13});
CustomGPIO.setConfig(config);
} catch (IOException e) {

View File

@@ -20,7 +20,6 @@ package org.photonvision.hardware;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.photonvision.common.hardware.GPIO.CustomGPIO;
import org.photonvision.common.hardware.GPIO.GPIOBase;
@@ -28,17 +27,11 @@ import org.photonvision.common.hardware.GPIO.pi.PigpioPin;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.hardware.metrics.MetricsManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonTargetingJniLoader;
public class HardwareTest {
@Test
public void testHardware() {
try {
TestUtils.loadLibraries();
PhotonTargetingJniLoader.load();
} catch (UnsatisfiedLinkError | IOException e) {
e.printStackTrace();
}
TestUtils.loadLibraries();
MetricsManager mm = new MetricsManager();
if (!Platform.isRaspberryPi()) return;

View File

@@ -41,24 +41,20 @@ import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.vision.frame.provider.FileFrameProvider;
public class FileSaveFrameConsumerTest {
NetworkTableInstance inst = null;
@BeforeAll
public static void init() throws UnsatisfiedLinkError, IOException {
if (!WpilibLoader.loadLibraries()) {
public static void init() throws IOException {
if (!LibraryLoader.loadWpiLibraries()) {
fail();
}
try {
if (!PhotonTargetingJniLoader.load()) fail();
} catch (UnsatisfiedLinkError | IOException e) {
e.printStackTrace();
fail(e);
if (!LibraryLoader.loadTargeting()) {
fail();
}
}

View File

@@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import edu.wpi.first.cscore.VideoMode;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -32,7 +31,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
@@ -48,12 +47,7 @@ public class VisionModuleManagerTest {
System.out.print(classpathStr);
TestUtils.loadLibraries();
try {
if (!PhotonTargetingJniLoader.load()) fail();
} catch (UnsatisfiedLinkError | IOException e) {
e.printStackTrace();
fail(e);
}
if (!LibraryLoader.loadTargeting()) fail();
}
private static class TestSource extends VisionSource {

View File

@@ -17,8 +17,8 @@
package org.photonvision.vision.processes;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.cscore.UsbCameraInfo;
@@ -33,7 +33,6 @@ import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.file.JacksonUtils;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.vision.camera.PVCameraInfo;
public class VisionSourceManagerTest {
@@ -59,9 +58,7 @@ public class VisionSourceManagerTest {
@BeforeAll
public static void loadLibraries() {
TestUtils.loadLibraries();
assertDoesNotThrow(PhotonTargetingJniLoader::load);
assertTrue(PhotonTargetingJniLoader.isWorking);
assertTrue(TestUtils.loadLibraries());
// Broadcast all still calls into configmanager (ew) so set that up here
ConfigManager.getInstance().load();
@@ -277,4 +274,55 @@ public class VisionSourceManagerTest {
assertEquals(2, vsm.getVsmState().disabledConfigs.size());
assertEquals(1, vsm.vmm.getModules().size());
}
@Test
public void testMismatch() throws InterruptedException {
var vsm = new TestVsm();
// Create a saved camera configuration that expects a device at /dev/video0 with a name
PVCameraInfo savedInfo =
PVCameraInfo.fromUsbCameraInfo(
new UsbCameraInfo(
0, "/dev/video0", "CamA", new String[] {"/dev/v4l/by-path/1"}, 111, 222));
CameraConfiguration savedConf = new CameraConfiguration(savedInfo);
savedConf.deactivated = false;
savedConf.nickname = "SavedCam";
// Register the saved config so VSM creates a VisionModule
vsm.registerLoadedConfigs(List.of(savedConf));
// Now simulate a connected camera at same uniquePath but with a different name (mismatch)
List<PVCameraInfo> currentInfo =
List.of(
PVCameraInfo.fromUsbCameraInfo(
new UsbCameraInfo(
0,
"/dev/video0",
"CamDifferent",
new String[] {"/dev/v4l/by-path/1"},
111,
222)));
// Trigger state evaluation
vsm.checkMismatches(currentInfo);
// The module should have detected a mismatch
assertTrue(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
// Now simulate the device being disconnected
currentInfo = List.of();
vsm.checkMismatches(currentInfo);
// Mismatch should be cleared when device is disconnected
assertFalse(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
// Test with a matching camera info
currentInfo = List.of(savedInfo);
vsm.checkMismatches(currentInfo);
// The mismatch should be cleared
assertFalse(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
vsm.teardown();
}
}

View File

@@ -39,13 +39,10 @@ doxygen {
}
}
doxygen {
option 'generate_html', true
option 'html_extra_stylesheet', 'theme.css'
doxygen.sourceSets.main {
cppProjectZips.each {
dependsOn it
source it.source
doxygenDox.dependsOn it
sources it.source
it.ext.includeDirs.each {
cppIncludeRoots.add(it.absolutePath)
}
@@ -100,7 +97,7 @@ tasks.register("zipCppDocs", Zip) {
// Java
configurations {
javaSource {
transitive false
transitive = false
}
}
@@ -158,22 +155,22 @@ publishing {
artifact zipJavaDocs
artifactId = "${baseArtifactIdJava}"
groupId artifactGroupIdJava
version pubVersion
groupId = artifactGroupIdJava
version = pubVersion
}
cpp(MavenPublication) {
artifact zipCppDocs
artifactId = "${baseArtifactIdCpp}"
groupId artifactGroupIdCpp
version pubVersion
groupId = artifactGroupIdCpp
version = pubVersion
}
}
repositories {
maven {
// Just throw everything into build/maven
url(localMavenURL)
url = localMavenURL
}
}
}

View File

@@ -282,7 +282,7 @@ if (!project.hasProperty('copyOfflineArtifacts')) {
artifactId = "${nativeName}-json"
groupId = "org.photonvision"
version "1.0"
version = "1.0"
}
}
}
@@ -344,8 +344,8 @@ publishing {
artifact combinedHeadersZip
artifactId = "${nativeName}-combinedcpp"
groupId artifactGroupId
version pubVersion
groupId = artifactGroupId
version = pubVersion
}
}
}
@@ -363,6 +363,9 @@ def nativeTasks = wpilibTools.createExtractionTasks {
nativeTasks.addToSourceSetResources(sourceSets.test)
dependencies {
wpilibNatives project(":photon-targeting")
}
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")

View File

@@ -24,8 +24,8 @@
package org.photonvision.timesync;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.TimeSyncServer;
/** Helper to hold a single TimeSyncServer instance with some default config */
@@ -35,12 +35,11 @@ public class TimeSyncSingleton {
public static boolean load() {
if (INSTANCE == null) {
try {
if (!PhotonTargetingJniLoader.load()) {
return false;
}
} catch (UnsatisfiedLinkError | IOException e) {
RuntimeLoader.loadLibrary("photontargetingJNI");
} catch (IOException e) {
// Don't want to return early. We want to create the TimeSyncServer so the program crashes
// because we need it in order to function.
e.printStackTrace();
return false;
}
INSTANCE = new TimeSyncServer(5810);

View File

@@ -35,6 +35,7 @@ import edu.wpi.first.hal.HAL;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.NetworkTablesJNI;
import edu.wpi.first.util.RuntimeLoader;
import edu.wpi.first.wpilibj.DataLogManager;
import edu.wpi.first.wpilibj.Timer;
import edu.wpi.first.wpilibj.simulation.SimHooks;
@@ -55,9 +56,8 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.jni.TimeSyncClient;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.simulation.PhotonCameraSim;
import org.photonvision.targeting.PhotonPipelineMetadata;
import org.photonvision.targeting.PhotonPipelineResult;
@@ -68,8 +68,9 @@ class PhotonCameraTest {
NetworkTableInstance inst = null;
@BeforeAll
public static void load_wpilib() {
WpilibLoader.loadLibraries();
public static void load() throws IOException {
LibraryLoader.loadWpiLibraries();
RuntimeLoader.loadLibrary("photontargetingJNI");
}
@BeforeEach
@@ -111,9 +112,6 @@ class PhotonCameraTest {
@Test
@Order(3)
public void testTimeSyncServerWithPhotonCamera() throws InterruptedException, IOException {
load_wpilib();
PhotonTargetingJniLoader.load();
inst.stopClient();
inst.startServer();

View File

@@ -48,6 +48,7 @@ import edu.wpi.first.math.geometry.Transform3d;
import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -59,8 +60,7 @@ import org.junit.jupiter.api.Test;
import org.photonvision.PhotonPoseEstimator.ConstrainedSolvepnpParams;
import org.photonvision.PhotonPoseEstimator.PoseStrategy;
import org.photonvision.estimation.TargetModel;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.simulation.PhotonCameraSim;
import org.photonvision.simulation.SimCameraProperties;
import org.photonvision.simulation.VisionTargetSim;
@@ -76,13 +76,11 @@ class PhotonPoseEstimatorTest {
@AutoClose final PhotonCameraInjector cameraOne = new PhotonCameraInjector();
@BeforeAll
public static void init() throws UnsatisfiedLinkError, IOException {
if (!WpilibLoader.loadLibraries()) {
fail();
}
if (!PhotonTargetingJniLoader.load()) {
public static void init() throws IOException {
if (!LibraryLoader.loadWpiLibraries()) {
fail();
}
RuntimeLoader.loadLibrary("photontargetingJNI");
HAL.initialize(1000, 0);

View File

@@ -28,7 +28,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
import edu.wpi.first.apriltag.AprilTag;
@@ -44,6 +43,7 @@ import edu.wpi.first.math.geometry.Translation2d;
import edu.wpi.first.math.geometry.Translation3d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.util.RuntimeLoader;
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
import java.io.IOException;
import java.util.ArrayList;
@@ -59,8 +59,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.photonvision.estimation.TargetModel;
import org.photonvision.estimation.VisionEstimation;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.simulation.PhotonCameraSim;
import org.photonvision.simulation.VisionSystemSim;
import org.photonvision.simulation.VisionTargetSim;
@@ -72,15 +71,9 @@ class VisionSystemSimTest {
NetworkTableInstance inst;
@BeforeAll
public static void setUp() {
assertTrue(WpilibLoader.loadLibraries());
try {
assertTrue(PhotonTargetingJniLoader.load());
} catch (UnsatisfiedLinkError | IOException e) {
e.printStackTrace();
fail(e);
}
public static void setUp() throws IOException {
assertTrue(LibraryLoader.loadWpiLibraries());
RuntimeLoader.loadLibrary("photontargetingJNI");
OpenCvLoader.forceStaticLoad();
}

View File

@@ -14,8 +14,8 @@ dependencies {
implementation "org.slf4j:slf4j-simple:2.0.7"
}
group 'org.photonvision'
version versionString + (project.hasProperty('pionly') ? "-raspi" : "")
group = 'org.photonvision'
version = versionString + (project.hasProperty('pionly') ? "-raspi" : "")
application {
mainClass = 'org.photonvision.Main'

View File

@@ -38,7 +38,7 @@ import org.photonvision.common.logging.Logger;
import org.photonvision.common.logging.PvCSCoreLogger;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.jni.RknnDetectorJNI;
import org.photonvision.jni.RubikDetectorJNI;
import org.photonvision.mrcal.MrCalJNILoader;
@@ -205,7 +205,7 @@ public class Main {
logger.info("WPI JNI libraries loaded.");
try {
boolean success = PhotonTargetingJniLoader.load();
boolean success = LibraryLoader.loadTargeting();
if (!success) {
logger.error("Failed to load native libraries! Giving up :(");

View File

@@ -23,20 +23,6 @@ nativeUtils {
sourceSets.main.java.srcDir "${projectDir}/src/generated/main/java"
// Folder whose contents will be included in the final jar
def outputsFolder = file("$buildDir/extra_resources")
// Sync task: like the copy task, but all files that exist in the destination directory will be deleted before copying files
task syncOutputsFolder(type: Sync) {
into outputsFolder
}
// And package our outputs folder into the final jar
jar {
from outputsFolder
dependsOn syncOutputsFolder
}
model {
components {
"${nativeName}"(NativeLibrarySpec) {
@@ -115,21 +101,6 @@ model {
platName = "osxuniversal";
realWpilibName = "osxuniversal";
}
if (binary.targetPlatform.name == platName) {
// only include release binaries (hard coded for now)
def isDebug = binary.buildType.name.contains('debug')
if (!isDebug) {
syncOutputsFolder {
// Just shove the shared library into the root of the jar output by photon-targeting:jar
from(binary.sharedLibraryFile) {
into "nativelibraries/${realWpilibName}/"
}
// And (not sure if this is a hack) make the jar task depend on the build task
dependsOn binary.identifier.projectScopedName
}
}
}
}
}
}
@@ -202,27 +173,6 @@ model {
apply from: "${rootDir}/shared/javacpp/publish.gradle"
// quickly hack our raw jar into publishing
def rawjavaJar = tasks.register("rawjavaJar", Jar) {
dependsOn classes
includeEmptyDirs = false
from sourceSets.main.output
archiveClassifier = 'raw'
}
publishing {
publications {
rawjava(MavenPublication) {
artifact (rawjavaJar) {
classifier = null
}
artifactId = "${nativeName}-rawjava"
groupId artifactGroupId
version pubVersion
}
}
}
// Add photon serde headers to our published sources
cppHeadersZip {
from('src/generated/main/native/include') {
@@ -230,12 +180,6 @@ cppHeadersZip {
}
}
// make sure native libraries can be loaded in tests
test {
classpath += files(outputsFolder)
dependsOn syncOutputsFolder
}
// setup wpilib bundled native libs
wpilibTools.deps.wpilibVersion = wpi.versions.wpilibVersion.get()

View File

@@ -29,11 +29,12 @@ import edu.wpi.first.util.WPIUtilJNI;
import java.io.IOException;
import org.opencv.core.Core;
public class WpilibLoader {
private static boolean has_loaded = false;
public class LibraryLoader {
private static boolean hasWpiLoaded = false;
private static boolean hasTargetingLoaded = false;
public static boolean loadLibraries() {
if (has_loaded) return true;
public static boolean loadWpiLibraries() {
if (hasWpiLoaded) return true;
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
@@ -45,10 +46,10 @@ public class WpilibLoader {
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
try {
// Need to load wpiutil first before checking if the MSVC runtime is valid
CombinedRuntimeLoader.loadLibraries(WpilibLoader.class, "wpiutiljni");
CombinedRuntimeLoader.loadLibraries(LibraryLoader.class, "wpiutiljni");
WPIUtilJNI.checkMsvcRuntime();
CombinedRuntimeLoader.loadLibraries(
WpilibLoader.class,
LibraryLoader.class,
"wpimathjni",
"ntcorejni",
"wpinetjni",
@@ -56,13 +57,25 @@ public class WpilibLoader {
"cscorejni",
"apriltagjni");
CombinedRuntimeLoader.loadLibraries(WpilibLoader.class, Core.NATIVE_LIBRARY_NAME);
has_loaded = true;
CombinedRuntimeLoader.loadLibraries(LibraryLoader.class, Core.NATIVE_LIBRARY_NAME);
hasWpiLoaded = true;
} catch (IOException e) {
e.printStackTrace();
has_loaded = false;
hasWpiLoaded = false;
}
return has_loaded;
return hasWpiLoaded;
}
public static boolean loadTargeting() {
if (hasTargetingLoaded) return true;
try {
CombinedRuntimeLoader.loadLibraries(LibraryLoader.class, "photontargetingJNI");
hasTargetingLoaded = true;
} catch (IOException e) {
e.printStackTrace();
hasTargetingLoaded = false;
}
return hasTargetingLoaded;
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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.jni;
import edu.wpi.first.util.RuntimeLoader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import org.photonvision.common.hardware.Platform;
public class PhotonTargetingJniLoader {
public static boolean isWorking = false;
public static boolean load() throws IOException, UnsatisfiedLinkError {
if (isWorking) return true;
isWorking = load_();
return isWorking;
}
public static boolean load_() throws IOException, UnsatisfiedLinkError {
// We always extract the shared object (we could hash each so, but that's a lot
// of work)
String arch_name = Platform.getNativeLibraryFolderName();
var clazz = PhotonTargetingJniLoader.class;
for (var libraryName : List.of("photontargeting", "photontargetingJNI")) {
try {
RuntimeLoader.loadLibrary(libraryName);
continue;
} catch (Exception e) {
System.out.println("Direct library load failed; falling back to extraction");
}
var nativeLibName = System.mapLibraryName(libraryName);
var path = "/nativelibraries/" + arch_name + "/" + nativeLibName;
var in = clazz.getResourceAsStream(path);
if (in == null) {
System.err.println("Could not get resource at path " + path);
return false;
}
// It's important that we don't mangle the names of these files on Windows at
// least
var tempfolder = Files.createTempDirectory("nativeextract");
File temp = new File(tempfolder.toAbsolutePath().toString(), nativeLibName);
System.out.println(temp.getAbsolutePath().toString());
FileOutputStream fos = new FileOutputStream(temp);
int read = -1;
byte[] buffer = new byte[1024];
while ((read = in.read(buffer)) != -1) {
fos.write(buffer, 0, read);
}
fos.close();
in.close();
try {
System.load(temp.getAbsolutePath());
} catch (Throwable t) {
System.err.println("Unable to System.load " + temp.getName() + " : " + t.getMessage());
t.printStackTrace();
return false;
}
System.out.println("Successfully loaded shared object " + temp.getName());
}
return true;
}
}

View File

@@ -21,24 +21,22 @@ import static org.junit.jupiter.api.Assertions.fail;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.math.MatBuilder;
import edu.wpi.first.math.Nat;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import java.util.Arrays;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.jni.ConstrainedSolvepnpJni;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.jni.LibraryLoader;
public class ConstrainedSolvepnpTest {
@BeforeAll
public static void load_wpilib() throws UnsatisfiedLinkError, IOException {
if (!WpilibLoader.loadLibraries()) {
fail();
}
if (!PhotonTargetingJniLoader.load()) {
public static void load() throws IOException {
if (!LibraryLoader.loadWpiLibraries()) {
fail();
}
RuntimeLoader.loadLibrary("photontargetingJNI");
HAL.initialize(1000, 0);
}

View File

@@ -28,23 +28,21 @@ import edu.wpi.first.cscore.UsbCamera;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.util.PixelFormat;
import edu.wpi.first.util.RawFrame;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.opencv.core.Mat;
import org.photonvision.jni.CscoreExtras;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.WpilibLoader;
import org.photonvision.jni.LibraryLoader;
public class CscoreExtrasTest {
@BeforeAll
public static void load_wpilib() throws UnsatisfiedLinkError, IOException {
if (!WpilibLoader.loadLibraries()) {
fail();
}
if (!PhotonTargetingJniLoader.load()) {
public static void load() throws IOException {
if (!LibraryLoader.loadWpiLibraries()) {
fail();
}
RuntimeLoader.loadLibrary("photontargetingJNI");
HAL.initialize(1000, 0);
}

View File

@@ -21,24 +21,22 @@ import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.common.hardware.Platform;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.jni.QueuedFileLogger;
import org.photonvision.jni.WpilibLoader;
public class FileLoggerTest {
@BeforeAll
public static void load_wpilib() throws UnsatisfiedLinkError, IOException {
if (!WpilibLoader.loadLibraries()) {
fail();
}
if (!PhotonTargetingJniLoader.load()) {
public static void load() throws IOException {
if (!LibraryLoader.loadWpiLibraries()) {
fail();
}
RuntimeLoader.loadLibrary("photontargetingJNI");
HAL.initialize(1000, 0);
}

View File

@@ -17,25 +17,21 @@
package net;
import static org.junit.jupiter.api.Assertions.fail;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.util.RuntimeLoader;
import java.io.IOException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.photonvision.jni.PhotonTargetingJniLoader;
import org.photonvision.jni.LibraryLoader;
import org.photonvision.jni.TimeSyncClient;
import org.photonvision.jni.TimeSyncServer;
import org.photonvision.jni.WpilibLoader;
public class TimeSyncTest {
@BeforeAll
public static void load_wpilib() throws UnsatisfiedLinkError, IOException {
WpilibLoader.loadLibraries();
if (!PhotonTargetingJniLoader.load()) {
fail();
}
public static void load() throws IOException {
LibraryLoader.loadWpiLibraries();
RuntimeLoader.loadLibrary("photontargetingJNI");
HAL.initialize(1000, 0);
}

View File

@@ -1,7 +0,0 @@
# Notebook
In the first cell of the RKNN conversion notebook, the installation script uses a structured list of dictionaries to define the download URLs and filenames for required scripts. Each dictionary includes a `url` (a permalink to a specific commit) and the corresponding `filename`.
Please ensure that all URLs in this array use permalinks—that is, links pointing to a specific commit hash rather than a branch name (e.g., main). This guarantees that the correct version of each script is always fetched, and prevents unexpected changes if the repository is updated in the future.
You typically wont need to update these permalinks unless one of the referenced scripts is modified. In that case, update the commit hash in the URLs accordingly.

View File

@@ -1,179 +0,0 @@
import argparse
import os.path
import subprocess
import sys
# This will work for all models that don't use anchors (e.g. all YOLO models except YOLOv5/v7)
# This includes YOLOv5u
yolo_non_anchor_repo = "https://github.com/airockchip/ultralytics_yolo11"
# For original YOLOv5 models
yolov5_repo = "https://github.com/airockchip/yolov5"
valid_yolo_versions = ["yolov5", "yolov8", "yolov11"]
comma_sep_yolo_versions = ", ".join(valid_yolo_versions)
ultralytics_folder_name_yolov5 = "airockchip_yolo_pkg_yolov5"
ultralytics_default_folder_name = "airockchip_yolo_pkg"
bad_model_msg = """
This is usually due to passing in the wrong model version.
Please make sure you have the right model version and try again.
"""
def print_bad_model_msg(cause):
print(f"{cause}{bad_model_msg}")
def run_and_exit_with_error(cmd, error_msg, enable_error_output=True):
try:
if enable_error_output:
subprocess.run(
cmd,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
universal_newlines=True,
).check_returncode()
else:
subprocess.run(cmd).check_returncode()
except subprocess.CalledProcessError as e:
print(error_msg)
if enable_error_output:
print(e.stdout)
sys.exit(1)
def check_git_installed():
run_and_exit_with_error(
["git", "--version"],
"""Git is not installed or not found in your PATH.
Please install Git from https://git-scm.com/downloads and try again.""",
)
def check_or_clone_rockchip_repo(repo_url, repo_name=ultralytics_default_folder_name):
if os.path.exists(repo_name):
print(
f'Existing Rockchip repo "{repo_name}" detected, skipping installation...'
)
else:
print(f'Cloning Rockchip repo to "{repo_name}"')
run_and_exit_with_error(
["git", "clone", repo_url, repo_name],
"Failed to clone Rockchip repo, please see error output",
)
def run_pip_install_or_else_exit(args):
print("Running pip install...")
run_and_exit_with_error(
["pip", "install"] + args,
"Pip install rockchip repo failed, please see error output",
)
def run_onnx_conversion_yolov5(model_path):
check_or_clone_rockchip_repo(yolov5_repo, ultralytics_folder_name_yolov5)
run_pip_install_or_else_exit(
[
"-r",
os.path.join(ultralytics_folder_name_yolov5, "requirements.txt"),
"torch<2.6.0",
"onnx",
]
)
model_abspath = os.path.abspath(model_path)
try:
subprocess.run(
[
"python",
f"{ultralytics_folder_name_yolov5}/export.py",
"--weights",
model_abspath,
"--rknpu",
"--include",
"onnx",
],
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
universal_newlines=True,
).check_returncode()
except subprocess.CalledProcessError as e:
print("Failed to run YOLOv5 export, please see error output")
if "ModuleNotFoundError" in e.stdout and "ultralytics" in e.stdout:
print_bad_model_msg(
"It seems the YOLOv5 repo could not find an ultralytics installation."
)
elif "AttributeError" in e.stdout and "_register_detect_seperate" in e.stdout:
print_bad_model_msg("It seems that you received a model attribute error.")
else:
print("Unknown Error when converting:")
print(e.stdout)
sys.exit(1)
def run_onnx_conversion_no_anchor(model_path):
check_or_clone_rockchip_repo(yolo_non_anchor_repo)
run_pip_install_or_else_exit(["-e", ultralytics_default_folder_name, "onnx"])
sys.path.insert(0, os.path.abspath(ultralytics_default_folder_name))
model_abs_path = os.path.abspath(model_path)
from ultralytics import YOLO
try:
model = YOLO(model_abs_path)
model.export(format="rknn")
except TypeError as e:
if "originally trained" in str(e):
print_bad_model_msg(
"Ultralytics has detected that this model is a YOLOv5 model."
)
else:
raise e
sys.exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate valid ONNX file for yolo model"
)
parser.add_argument(
"-m",
"--model_path",
required=True,
help=(f"Path to YOLO model"),
)
parser.add_argument(
"-v",
"--version",
required=True,
choices=valid_yolo_versions,
help=(f"Model version, must be one of: {comma_sep_yolo_versions}"),
)
args = parser.parse_args()
check_git_installed()
try:
if args.version.lower() == "yolov5":
run_onnx_conversion_yolov5(args.model_path)
else:
run_onnx_conversion_no_anchor(args.model_path)
print(
"Model export finished. Please use the generated ONNX file to convert to RKNN."
)
except SystemExit:
print("Model export failed. Please see output above.")

View File

@@ -1,221 +0,0 @@
import argparse
import os
import random
import sys
from rknn.api import RKNN
image_extensions = (".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp")
DEFAULT_PLATFORM = "rk3588"
def list_img_dir(img_dir):
return [
os.path.abspath(os.path.join(img_dir, f))
for f in os.listdir(img_dir)
if f.lower().endswith(image_extensions)
]
def sample_imgs(num, img_list):
if len(img_list) < num:
return img_list
else:
return random.sample(img_list, num)
def get_image_list_from_dataset(num_imgs, yaml_dir):
print(f"Dataset detected with {yaml_dir} file")
img_raw_paths = []
with open(yaml_dir, "r") as yaml_file:
for line in yaml_file:
line = line.strip()
if (
line.startswith("train:")
or line.startswith("val:")
or line.startswith("test:")
):
img_raw_paths.append(line.split(":", 1)[1].strip())
no_yaml_dir = yaml_dir.replace(
"data.yaml", "dummy_dir"
) # data.yaml sets dirs one level up
img_set_paths = []
for img_raw_path in img_raw_paths:
p = (
img_raw_path
if os.path.isabs(img_raw_path)
else os.path.realpath(os.path.join(no_yaml_dir, img_raw_path))
)
if os.path.exists(p):
img_set_paths.append(p)
if len(img_set_paths) < 1:
return None
all_imgs = [list_img_dir(path) for path in img_set_paths]
for imgs in all_imgs:
print(len(imgs))
total_imgs = sum(len(group) for group in all_imgs)
sampled_imgs = [
sample_imgs(round((len(group) / total_imgs) * num_imgs), group)
for group in all_imgs
]
return [img for group in sampled_imgs for img in group]
def get_image_list_from_img_dir(num_imgs, img_dir):
return sample_imgs(num_imgs, list_img_dir(img_dir))
def get_image_list(num_imgs, image_dir):
yaml_path = os.path.join(image_dir, "data.yaml")
if os.path.exists(yaml_path):
return get_image_list_from_dataset(num_imgs, yaml_path)
else:
return get_image_list_from_img_dir(num_imgs, image_dir)
def run_rknn_conversion(
img_list_txt, disable_quant, model_path, rknn_output, verbose_logging
):
rknn = RKNN(
verbose=verbose_logging,
verbose_file=("rknn_convert.log" if verbose_logging else None),
)
rknn.config(
mean_values=[[0, 0, 0]],
std_values=[[255, 255, 255]],
target_platform=DEFAULT_PLATFORM,
)
print("Attempted RKNN load")
ret = rknn.load_onnx(model=model_path)
if ret != 0:
print("Loading model failed!")
exit(ret)
print("Attempting RKNN build")
ret = rknn.build(do_quantization=(not disable_quant), dataset=img_list_txt)
if ret != 0:
print("Building model failed!")
exit(ret)
print("Build succeeded! Starting export...")
ret = rknn.export_rknn(rknn_output)
if ret != 0:
print("Exporting model failed!")
exit(ret)
print("Finished export!")
# Release
rknn.release()
print(f'Your model is in "{rknn_output}" and ready to use!')
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate valid ONNX file for yolo model"
)
parser.add_argument(
"-ni",
"--num_imgs",
type=int,
default=300,
help="Number of images to use for calibration (default: 300)",
)
parser.add_argument(
"-d",
"--img_dir",
help="Directory where your dataset is located (must have data.yaml), or images are located",
)
parser.add_argument(
"-m",
"--model_path",
required=True,
help=(f"Path to generated ONNX model"),
)
parser.add_argument(
"-dq",
"--disable_quantize",
action="store_true",
help="Whether to skip quantization (default: False)",
)
parser.add_argument(
"-o",
"--rknn_output",
default="out.rknn",
help="Where the rknn model should be outputted (default: ./out.rknn)",
)
parser.add_argument(
"-ds",
"--img_dataset_txt",
default="imgs.txt",
help="Where the list of images used for quantization should be outputted (default: ./imgs.txt)",
)
parser.add_argument(
"-vb",
"--verbose",
action="store_true",
help="Whether to enable verbose logging",
)
args = parser.parse_args()
if not args.rknn_output.endswith(".rknn"):
print("RKNN output path must end in .rknn!")
sys.exit(1)
if not args.disable_quantize:
if args.img_dir == None or len(args.img_dir) < 1:
print(f"Must specify list of images to use with --img_dir")
sys.exit(1)
img_dir_abs = os.path.abspath(args.img_dir)
img_list = get_image_list(args.num_imgs, img_dir_abs)
img_list_len = 0 if img_list is None else len(img_list)
if img_list_len == 0:
print(f"No images found in {img_dir_abs}")
sys.exit(1)
elif img_list_len < args.num_imgs:
print(
f"Not enough images in your dataset/directory, you have {img_list_len} images, but need {args.num_imgs}"
)
sys.exit(1)
if not args.img_dataset_txt.endswith(".txt"):
print(f"Image dataset text file path must end in .txt")
sys.exit(1)
with open(args.img_dataset_txt, "w") as set_file:
set_file.writelines(f"{img}\n" for img in img_list)
try:
run_rknn_conversion(
args.img_dataset_txt,
args.disable_quantize,
args.model_path,
args.rknn_output,
args.verbose,
)
except SystemExit:
print("RKNN Conversion failed, see output above")

View File

@@ -1,310 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "bb5367ce",
"metadata": {},
"source": [
"# RKNN Conversion Guide\n",
"\n",
"----------------------------\n",
"\n",
"### Before you start\n",
"\n",
"If you are not using Google Colab, it is recommended to create a separate [Python virtual environment](https://docs.python.org/3/library/venv.html) before you run the scripts or the Python notebook from this project. This ensures that packages installed for the conversion process do not conflict with other packages you may already have setup."
]
},
{
"cell_type": "markdown",
"id": "5f42d0a144caceb6",
"metadata": {},
"source": [
"### Preinstallation\n",
"\n",
"This notebook requires the use of external Python scripts. Please run the installation script below to import these external scripts if you do not have them already."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7903189e",
"metadata": {},
"outputs": [],
"source": [
"import subprocess\n",
"\n",
"# Define scripts with URLs and inferred filenames\n",
"# DO NOT modify the filenames\n",
"scripts = [\n",
" {\n",
" \"url\": \"https://raw.githubusercontent.com/PhotonVision/photonvision/2bf166bc3f377fa8af9d9d38ee46e0db978a4036/scripts/rknn-convert-tool/create_onnx.py\",\n",
" \"filename\": \"create_onnx.py\" # CREATE_ONNX_SCRIPT\n",
" },\n",
" {\n",
" \"url\": \"https://raw.githubusercontent.com/PhotonVision/photonvision/2bf166bc3f377fa8af9d9d38ee46e0db978a4036/scripts/rknn-convert-tool/create_rknn.py\",\n",
" \"filename\": \"create_rknn.py\" # CREATE_RKNN_SCRIPT\n",
" }\n",
"]\n",
"\n",
"# Download each script\n",
"for script in scripts:\n",
" try:\n",
" subprocess.run([\"wget\", script[\"url\"], \"-O\", script[\"filename\"]]).check_returncode()\n",
" print(f\"Successfully downloaded: {script['filename']}\")\n",
" except subprocess.CalledProcessError as e:\n",
" print(f\"Failed to download script from URL: {script['url']}\")\n",
" print(e)\n"
]
},
{
"cell_type": "markdown",
"id": "d68be4aba4d3022b",
"metadata": {},
"source": [
"#### *Numpy Fix* - Important for Google Colab Users\n",
"\n",
"Google Colab comes with an incompatible version of Numpy installed. To fix this, please run the following cells below and **restart your session** when prompted."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "de0310a3e4401233",
"metadata": {},
"outputs": [],
"source": [
"%pip uninstall numpy -y\n",
"%pip install \"numpy>=1.23.0,<2.0.0\""
]
},
{
"cell_type": "markdown",
"id": "d498ed79",
"metadata": {},
"source": [
"### Step 1: Convert to ONNX\n",
"\n",
"To convert to ONNX, simply run the `create_onnx.py` script, providing the path to your model weights and specifying the model version, as shown below."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0659e15f",
"metadata": {},
"outputs": [],
"source": [
"# where version is either yolov5, yolov8, or yolov11, and model_path is the path to your weights file (.pt)\n",
"%run create_onnx.py --version yolov8 --model_path myyolov8model.pt"
]
},
{
"cell_type": "markdown",
"id": "86ff07e6",
"metadata": {},
"source": [
"### Step 2: Download RKNN API\n",
"\n",
"You can either use `pip` below to automatically detect and install the correct RKNN API Python library for you, or install it manually.\n",
"\n",
"#### Automatic installation\n",
"\n",
"Please run `pip` below. If it does not work, refer to the instructions for manual installation.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7ec11f96",
"metadata": {},
"outputs": [],
"source": [
"%pip install rknn-toolkit2"
]
},
{
"cell_type": "markdown",
"id": "8b57fe4d",
"metadata": {},
"source": [
"#### Manual Installation (If Automatic Installation Fails)\n",
"Visit the [RKNN Toolkit 2](https://github.com/airockchip/rknn-toolkit2) Github repository, then click on rknn-toolkit2, followed by packages.\n",
"If you are running an x86_64 CPU (e.g., most Intel and AMD processors), select that option; otherwise, choose arm64 for ARM-based computers (e.g., M-series Macs or Snapdragon processors). If you're unsure which CPU you're using, check your system settings for processor architecture information.\n",
"\n",
"Once you've selected the correct CPU architecture, you'll see multiple packages. The file names will look something like:\n",
"`rknn_toolkit2-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`.\n",
"The numbers after cp correspond to your Python version. For example, if you're using Python 3.10, look for a package with cp310 in the name. For Python 3.8, look for cp38; for Python 3.7, cp37, and so on.\n",
"\n",
"Once you've found the correct package, click the \"Raw\" button to download the .whl file. Then, run the following command in your terminal, replacing rknn_toolkit2.whl with the actual path to the file you downloaded:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7414b120",
"metadata": {},
"outputs": [],
"source": [
"%pip install rknn_toolkit2.whl"
]
},
{
"cell_type": "markdown",
"id": "c1db5ef0",
"metadata": {},
"source": [
"### Step 3: Convert to RKNN\n",
"\n",
"Please review the notes about quantization before running the RKNN conversion."
]
},
{
"cell_type": "markdown",
"id": "5e56b2f64bf6e85f",
"metadata": {},
"source": [
"### Quantization\n",
"\n",
"When performing quantization, it is critical to provide representative images of the objects or scenes you are trying to detect. These images are used to calibrate the models internal activations and greatly influence the final performance.\n",
"\n",
"It is recommended to use 300500 representative images that reflect the real-world input your model will encounter. As the old saying goes, its quality over quantity — having a diverse, relevant set matters more than simply having many images.\n",
"\n",
"Quantization will cause some loss in model accuracy. However, if your calibration images are chosen wisely, this accuracy drop should be minimal and acceptable. If the sampled images are too uniform or unrelated, your quantized model's performance may worsen significantly.\n",
"\n",
"The script will automatically sample representative images randomly from the provided dataset. While this usually works well, please verify that the dataset contains diverse and relevant examples of your target objects. As a reminder, the images used to quantize the model are stored in the text file specified by `--img_dataset_txt`.\n"
]
},
{
"cell_type": "markdown",
"id": "93e0d0622df170e",
"metadata": {},
"source": [
"### Optional: Download a dataset from Roboflow for quantization\n",
"\n",
"If you do not already have a dataset or set of images containing the objects you want to detect, follow the steps below to download one from Roboflow Universe.\n",
"\n",
"#### **Step 1: Search for a Dataset**\n",
"\n",
"Go to [Roboflow Universe](https://universe.roboflow.com) and use the search bar to locate a dataset relevant to what you want to detect.\n",
"**Note:** The dataset must include the classes or object types you intend to detect.\n",
"\n",
"#### **Step 2: Access the Dataset Tab**\n",
"\n",
"After selecting a suitable project, navigate to the **Dataset** tab. Click the **\"Download Dataset\"** button. A prompt will appear with several options, including:\n",
"\n",
"- Train a model with this dataset\n",
"- Train from a portion of this dataset\n",
"- Download dataset\n",
"\n",
"Select **Download dataset**.\n",
"\n",
"#### **Step 3: Choose Format and View Download Code**\n",
"\n",
"- Under **Image and Annotation Format**, choose the version of YOLO you are using:\n",
" - For **YOLOv5**, choose `YOLOv5 PyTorch`\n",
" - For **YOLOv8**, choose `YOLOv8`\n",
" - For **YOLOv11**, choose `YOLOv11`\n",
"- If multiple annotation formats are listed for your model, always select the one ending in **\"PyTorch\"**.\n",
"\n",
"Then, under **Download Options**, click **\"Show Download Code\"** and continue.\n",
"\n",
"In the resulting screen, you will see three tabs:\n",
"- **Jupyter**\n",
"- **Terminal**\n",
"- **Raw URL**\n",
"\n",
"Select the **Terminal** tab and copy the provided command.\n",
"\n",
"#### **Step 4: Paste and Run**\n",
"\n",
"Paste the copied command into the notebook cell below and run it. This will download and extract the dataset into your environment, making it ready for use in the quantization process.\n",
"\n",
"Make sure to prefix the command with \"`!`\" so it executes properly in this Jupyter Notebook environment."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8bf75c9dcb328c84",
"metadata": {},
"outputs": [],
"source": [
"!curl -L \"https://universe.roboflow.com/ds/FaF3HbDmF7?key=iMoJR25O9H\" > roboflow.zip; unzip roboflow.zip; rm roboflow.zip"
]
},
{
"cell_type": "markdown",
"id": "72bad9cac670f1ab",
"metadata": {},
"source": [
"### RKNN Conversion Script\n",
"To get started, run the `create_rknn.py` script below, replacing the arguments with your own values. Refer to the table below for detailed information on each arguments purpose and usage. The `--model_path` argument should point to your exported ONNX model from Step 1, and `--img_dir` must reference a valid directory containing either a dataset or a set of images to be used for quantization.\n",
"\n",
"#### Overview of the `create_rknn.py` script\n",
"\n",
"This script converts a YOLO ONNX model to RKNN format using a set of calibration images. It's designed to work with either:\n",
"\n",
"- A flat directory of images (e.g. `train/images`), **or**\n",
"- A dataset directory containing a `data.yaml` file that defines `train`, `val`, and/or `test` folders.\n",
"\n",
"##### Arguments\n",
"\n",
"| Argument | Type | Description |\n",
"|----------|------|-----------------------------------------------------------------------------------------------------------------|\n",
"| `--img_dir` (`-d`) | `str` (required) | Path to your image directory. This can either be a folder of images **or** a dataset folder with a `data.yaml`. |\n",
"| `--model_path` (`-m`) | `str` (required) | Path to your YOLO ONNX model, created in Step 1. |\n",
"| `--num_imgs` (`-ni`) | `int` (default: `300`) | Number of images to use for quantization calibration. |\n",
"| `--disable_quantize` (`-dq`) | `bool` (default: `False`) | Set to `True` to skip quantization entirely. Not recommended for performance, and should not be used for deployment on PhotonVision, which requires quantization. |\n",
"| `--rknn_output` (`-o`) | `str` (default: `out.rknn`) | File path where the final RKNN model should be saved. |\n",
"| `--img_dataset_txt` (`-ds`) | `str` (default: `imgs.txt`) | File path to store the list of images used during quantization. |\n",
"| `--verbose` (`-vb`) | `bool` (default: `False`) | Enable detailed logging from the RKNN API during conversion. |\n",
"\n",
"\n",
"##### *Notes*\n",
"\n",
"1. This script is designed for use with [PhotonVision](https://photonvision.org), and by default sets the target platform for RKNN conversion to `RK3588`, a chipset commonly found in many variants of the Orange Pi 5 series (e.g., Orange Pi 5, 5 Pro, 5 Plus, 5 Max, etc.). You may modify the `DEFAULT_PLATFORM` value in the `create_rknn.py` script to match your specific hardware or deployment requirements if necessary.\n",
"\n",
"2. If you followed the Roboflow dataset download instructions from the previous section, the dataset will have been extracted to your **current working directory**. In that case, you can simply set `--img_dir` to \"`.`\" to reference the current directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b09656dd",
"metadata": {},
"outputs": [],
"source": [
"%run create_rknn.py --img_dir ./datasets --model_path weights.onnx"
]
},
{
"cell_type": "markdown",
"id": "5b3a6806",
"metadata": {},
"source": [
"And thats it! Your RKNN model file is now ready for deployment on an Orange Pi."
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,617 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# RKNN Conversion Guide\n",
"\n",
"----------------------------\n",
"\n",
"### Before you start\n",
"\n",
"If you are not using Google Colab, it is recommended to create a separate [Python virtual environment](https://docs.python.org/3/library/venv.html) before you run this project. This ensures that packages installed for the conversion process do not conflict with other packages you may already have set up."
],
"id": "65e9f457d12dcc6b"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Setup\n",
"\n",
"This notebook requires the use of custom Python code. Please run the installation script below to import these external scripts if you do not have them already.\n",
"\n",
"**You may need to run the `Create ONNX/RKNN` cell multiple times when you restart your session. If you see a `create_onnx`\n",
"or `create_rknn` not found error, rerun the cell below and then retry.**\n",
"\n",
"**Do not modify the cells in this setup section unless you know what youre doing or have a specific reason to.**"
],
"id": "500d656b7cc0ebd7"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"#### Create ONNX/RKNN\n",
"\n",
"Please run the cell below to be able to use the `create_onnx` and `create_rknn` functions."
],
"id": "798298b1dbe33d2d"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"import os.path\n",
"import subprocess\n",
"import sys\n",
"import random\n",
"\n",
"# This will work for all models that don't use anchors (e.g. all YOLO models except YOLOv5/v7)\n",
"# This includes YOLOv5u\n",
"yolo_non_anchor_repo = \"https://github.com/airockchip/ultralytics_yolo11\"\n",
"\n",
"# For original YOLOv5 models\n",
"yolov5_repo = \"https://github.com/airockchip/yolov5\"\n",
"\n",
"valid_yolo_versions = [\"yolov5\", \"yolov8\", \"yolov11\"]\n",
"\n",
"ultralytics_folder_name_yolov5 = \"airockchip_yolo_pkg_yolov5\"\n",
"ultralytics_default_folder_name = \"airockchip_yolo_pkg\"\n",
"\n",
"bad_model_msg = \"\"\"\n",
"This is usually due to passing in the wrong model version.\n",
"Please make sure you have the right model version and try again.\n",
"\"\"\"\n",
"\n",
"\n",
"def print_bad_model_msg(cause):\n",
" print(f\"{cause}{bad_model_msg}\")\n",
"\n",
"def run_and_exit_with_error(cmd, error_msg, enable_error_output=True):\n",
" try:\n",
" if enable_error_output:\n",
" subprocess.run(\n",
" cmd,\n",
" stderr=subprocess.STDOUT,\n",
" stdout=subprocess.PIPE,\n",
" universal_newlines=True,\n",
" ).check_returncode()\n",
" else:\n",
" subprocess.run(cmd).check_returncode()\n",
" except subprocess.CalledProcessError as e:\n",
" print(error_msg)\n",
"\n",
" if enable_error_output:\n",
" print(e.stdout)\n",
"\n",
" sys.exit(1)\n",
"\n",
"\n",
"def check_git_installed():\n",
" run_and_exit_with_error(\n",
" [\"git\", \"--version\"],\n",
" \"\"\"Git is not installed or not found in your PATH.\n",
"Please install Git from https://git-scm.com/downloads and try again.\"\"\",\n",
" )\n",
"\n",
"\n",
"def check_or_clone_rockchip_repo(repo_url, repo_name=ultralytics_default_folder_name):\n",
" if os.path.exists(repo_name):\n",
" print(\n",
" f'Existing Rockchip repo \"{repo_name}\" detected, skipping installation...'\n",
" )\n",
" else:\n",
" print(f'Cloning Rockchip repo to \"{repo_name}\"')\n",
" run_and_exit_with_error(\n",
" [\"git\", \"clone\", repo_url, repo_name],\n",
" \"Failed to clone Rockchip repo, please see error output\",\n",
" )\n",
"\n",
"\n",
"def run_pip_install_or_else_exit(args):\n",
" print(\"Running pip install...\")\n",
" run_and_exit_with_error(\n",
" [\"pip\", \"install\"] + args,\n",
" \"Pip install rockchip repo failed, please see error output\",\n",
" )\n",
"\n",
"\n",
"def run_onnx_conversion_yolov5(model_path):\n",
" check_or_clone_rockchip_repo(yolov5_repo, ultralytics_folder_name_yolov5)\n",
" run_pip_install_or_else_exit(\n",
" [\n",
" \"-r\",\n",
" os.path.join(ultralytics_folder_name_yolov5, \"requirements.txt\"),\n",
" \"torch<2.6.0\",\n",
" \"onnx==1.18.0\",\n",
" \"onnxscript\",\n",
" ]\n",
" )\n",
"\n",
" model_abspath = os.path.abspath(model_path)\n",
"\n",
" try:\n",
" subprocess.run(\n",
" [\n",
" \"python\",\n",
" f\"{ultralytics_folder_name_yolov5}/export.py\",\n",
" \"--weights\",\n",
" model_abspath,\n",
" \"--rknpu\",\n",
" \"--include\",\n",
" \"onnx\",\n",
" ],\n",
" stderr=subprocess.STDOUT,\n",
" stdout=subprocess.PIPE,\n",
" universal_newlines=True,\n",
" ).check_returncode()\n",
" except subprocess.CalledProcessError as e:\n",
" print(\"Failed to run YOLOv5 export, please see error output\")\n",
"\n",
" if \"ModuleNotFoundError\" in e.stdout and \"ultralytics\" in e.stdout:\n",
" print_bad_model_msg(\n",
" \"It seems the YOLOv5 repo could not find an ultralytics installation.\"\n",
" )\n",
" elif \"AttributeError\" in e.stdout and \"_register_detect_seperate\" in e.stdout:\n",
" print_bad_model_msg(\"It seems that you received a model attribute error.\")\n",
" else:\n",
" print(\"Unknown Error when converting:\")\n",
" print(e.stdout)\n",
"\n",
" sys.exit(1)\n",
"\n",
"\n",
"def run_onnx_conversion_no_anchor(model_path):\n",
" check_or_clone_rockchip_repo(yolo_non_anchor_repo)\n",
" run_pip_install_or_else_exit(\n",
" [\"-e\", ultralytics_default_folder_name, \"onnx==1.18.0\", \"onnxscript\"]\n",
" )\n",
"\n",
" sys.path.insert(0, os.path.abspath(ultralytics_default_folder_name))\n",
" model_abs_path = os.path.abspath(model_path)\n",
"\n",
" from ultralytics import YOLO\n",
"\n",
" try:\n",
" model = YOLO(model_abs_path)\n",
" model.export(format=\"rknn\")\n",
" except TypeError as e:\n",
" if \"originally trained\" in str(e):\n",
" print_bad_model_msg(\n",
" \"Ultralytics has detected that this model is a YOLOv5 model.\"\n",
" )\n",
" else:\n",
" raise e\n",
"\n",
" sys.exit(1)\n",
"\n",
"\n",
"def create_onnx(model_path: str, version: str):\n",
" check_git_installed()\n",
"\n",
" if not version in valid_yolo_versions:\n",
" print(f\"YOLO version \\\"{version}\\\" is not a valid version! Valid versions are: {\", \".join(valid_yolo_versions)}\")\n",
"\n",
" try:\n",
" if version.lower() == \"yolov5\":\n",
" run_onnx_conversion_yolov5(model_path)\n",
" else:\n",
" run_onnx_conversion_no_anchor(model_path)\n",
"\n",
" print(\n",
" \"Model export finished. Please use the generated ONNX file to convert to RKNN.\"\n",
" )\n",
" except SystemExit:\n",
" raise RuntimeError(\"Model export failed. Please see output above.\")\n",
"\n",
"# RKNN Conversion code\n",
"\n",
"image_extensions = (\".jpg\", \".jpeg\", \".png\", \".bmp\", \".gif\", \".tiff\", \".webp\")\n",
"DEFAULT_PLATFORM = \"rk3588\"\n",
"\n",
"\n",
"def list_img_dir(img_dir):\n",
" return [\n",
" os.path.abspath(os.path.join(img_dir, f))\n",
" for f in os.listdir(img_dir)\n",
" if f.lower().endswith(image_extensions)\n",
" ]\n",
"\n",
"\n",
"def sample_imgs(num, img_list):\n",
" if len(img_list) < num:\n",
" return img_list\n",
" else:\n",
" return random.sample(img_list, num)\n",
"\n",
"\n",
"def get_image_list_from_dataset(num_imgs, yaml_dir):\n",
" print(f\"Dataset detected with {yaml_dir} file\")\n",
" img_raw_paths = []\n",
"\n",
" with open(yaml_dir, \"r\") as yaml_file:\n",
" for line in yaml_file:\n",
" line = line.strip()\n",
" if (\n",
" line.startswith(\"train:\")\n",
" or line.startswith(\"val:\")\n",
" or line.startswith(\"test:\")\n",
" ):\n",
" img_raw_paths.append(line.split(\":\", 1)[1].strip())\n",
"\n",
" no_yaml_dir = yaml_dir.replace(\n",
" \"data.yaml\", \"dummy_dir\"\n",
" ) # data.yaml sets dirs one level up\n",
" img_set_paths = []\n",
"\n",
" for img_raw_path in img_raw_paths:\n",
" p = (\n",
" img_raw_path\n",
" if os.path.isabs(img_raw_path)\n",
" else os.path.realpath(os.path.join(no_yaml_dir, img_raw_path))\n",
" )\n",
"\n",
" if os.path.exists(p):\n",
" img_set_paths.append(p)\n",
"\n",
" if len(img_set_paths) < 1:\n",
" return None\n",
"\n",
" all_imgs = [list_img_dir(path) for path in img_set_paths]\n",
"\n",
" for imgs in all_imgs:\n",
" print(len(imgs))\n",
"\n",
" total_imgs = sum(len(group) for group in all_imgs)\n",
"\n",
" sampled_imgs = [\n",
" sample_imgs(round((len(group) / total_imgs) * num_imgs), group)\n",
" for group in all_imgs\n",
" ]\n",
"\n",
" return [img for group in sampled_imgs for img in group]\n",
"\n",
"\n",
"def get_image_list_from_img_dir(num_imgs, img_dir):\n",
" return sample_imgs(num_imgs, list_img_dir(img_dir))\n",
"\n",
"\n",
"def get_image_list(num_imgs, image_dir):\n",
" yaml_path = os.path.join(image_dir, \"data.yaml\")\n",
"\n",
" if os.path.exists(yaml_path):\n",
" return get_image_list_from_dataset(num_imgs, yaml_path)\n",
" else:\n",
" return get_image_list_from_img_dir(num_imgs, image_dir)\n",
"\n",
"\n",
"def run_rknn_conversion(\n",
" img_list_txt, disable_quant, model_path, rknn_output, verbose_logging\n",
"):\n",
" from rknn.api import RKNN\n",
"\n",
" rknn = RKNN(\n",
" verbose=verbose_logging,\n",
" verbose_file=(\"rknn_convert.log\" if verbose_logging else None),\n",
" )\n",
"\n",
" rknn.config(\n",
" mean_values=[[0, 0, 0]],\n",
" std_values=[[255, 255, 255]],\n",
" target_platform=DEFAULT_PLATFORM,\n",
" )\n",
"\n",
" print(\"Attempted RKNN load\")\n",
" ret = rknn.load_onnx(model=model_path)\n",
" if ret != 0:\n",
" print(\"Loading model failed!\")\n",
" exit(ret)\n",
"\n",
" print(\"Attempting RKNN build\")\n",
" ret = rknn.build(do_quantization=(not disable_quant), dataset=img_list_txt)\n",
" if ret != 0:\n",
" print(\"Building model failed!\")\n",
" exit(ret)\n",
"\n",
" print(\"Build succeeded! Starting export...\")\n",
" ret = rknn.export_rknn(rknn_output)\n",
" if ret != 0:\n",
" print(\"Exporting model failed!\")\n",
" exit(ret)\n",
" print(\"Finished export!\")\n",
"\n",
" # Release\n",
" rknn.release()\n",
"\n",
" print(f'Your model is in \"{rknn_output}\" and ready to use!')\n",
"\n",
"\n",
"def create_rknn(\n",
" model_path: str,\n",
" rknn_output: str = \"out.rknn\",\n",
" num_imgs: int = 300,\n",
" img_dir: str = None,\n",
" img_dataset_txt: str = \"imgs.txt\",\n",
" disable_quantize: bool = False,\n",
" verbose: bool = False,\n",
"):\n",
" if not rknn_output.endswith(\".rknn\"):\n",
" print(\"RKNN output path must end in .rknn!\")\n",
" return\n",
"\n",
" if not disable_quantize:\n",
" if img_dir is None or len(img_dir) < 1:\n",
" print(f\"Must specify list of images to use with --img_dir\")\n",
" return\n",
"\n",
" img_dir_abs = os.path.abspath(img_dir)\n",
"\n",
" img_list = get_image_list(num_imgs, img_dir_abs)\n",
" img_list_len = 0 if img_list is None else len(img_list)\n",
"\n",
" if img_list_len == 0:\n",
" print(f\"No images found in {img_dir_abs}\")\n",
" return\n",
" elif img_list_len < num_imgs:\n",
" print(\n",
" f\"Not enough images in your dataset/directory, you have {img_list_len} images, but need {num_imgs}\"\n",
" )\n",
" return\n",
"\n",
" if not img_dataset_txt.endswith(\".txt\"):\n",
" print(f\"Image dataset text file path must end in .txt\")\n",
" return\n",
"\n",
" with open(img_dataset_txt, \"w\") as set_file:\n",
" set_file.writelines(f\"{img}\\n\" for img in img_list)\n",
"\n",
" try:\n",
" run_rknn_conversion(\n",
" img_dataset_txt,\n",
" disable_quantize,\n",
" model_path,\n",
" rknn_output,\n",
" verbose,\n",
" )\n",
" except SystemExit:\n",
" print(\"RKNN Conversion failed, see output above\")\n"
],
"id": "ea6869140a61126d"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"#### *Numpy Fix* - Important for Google Colab Users\n",
"\n",
"Google Colab comes with an incompatible version of Numpy installed. To fix this, please run the following cells below and **restart your session** when prompted."
],
"id": "b3a9e1a334bce144"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"%pip uninstall numpy -y\n",
"%pip install \"numpy>=1.23.0,<2.0.0\""
],
"id": "7156e69495f48f49"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Step 1: Convert to ONNX\n",
"\n",
"To convert to ONNX, simply run the `create_onnx` function, providing the path to your model weights and specifying the model version, as shown below."
],
"id": "332942d8582c9bae"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "create_onnx(model_path=\"weights.pt\", version=\"yolov8\") # Valid versions are yolov5, yolov8, and yolov11",
"id": "408440b32de224ed"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Step 2: Download RKNN API\n",
"\n",
"You can either use `pip` below to automatically detect and install the correct RKNN API Python library for you, or install it manually.\n",
"\n",
"#### Automatic installation\n",
"\n",
"Please run `pip` below. If it does not work, refer to the instructions for manual installation. You may need to restart your session after running the command below.\n"
],
"id": "ebd148faaa2b1933"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "%pip install rknn-toolkit2",
"id": "7c7ef3010c663fc2"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"#### Manual Installation (If Automatic Installation Fails)\n",
"Visit the [RKNN Toolkit 2](https://github.com/airockchip/rknn-toolkit2) Github repository, then click on rknn-toolkit2, followed by packages.\n",
"If you are running an x86_64 CPU (e.g., most Intel and AMD processors), select that option; otherwise, choose arm64 for ARM-based computers (e.g., M-series Macs or Snapdragon processors). If you're unsure which CPU you're using, check your system settings for processor architecture information.\n",
"\n",
"Once you've selected the correct CPU architecture, you'll see multiple packages. The file names will look something like:\n",
"`rknn_toolkit2-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`.\n",
"The numbers after cp correspond to your Python version. For example, if you're using Python 3.10, look for a package with cp310 in the name. For Python 3.8, look for cp38; for Python 3.7, cp37, and so on.\n",
"\n",
"Once you've found the correct package, click the \"Raw\" button to download the .whl file. Then, run the following command in your terminal, replacing rknn_toolkit2.whl with the actual path to the file you downloaded:"
],
"id": "f44b71c5a820ab2"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "%pip install rknn_toolkit2.whl",
"id": "c9b9d3da532eb916"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Step 3: Convert to RKNN\n",
"\n",
"Please review the notes about quantization before running the RKNN conversion."
],
"id": "fc74ebc99b596f76"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Quantization\n",
"\n",
"When performing quantization, it is critical to provide representative images of the objects or scenes you are trying to detect. These images are used to calibrate the models internal activations and greatly influence the final performance.\n",
"\n",
"It is recommended to use 300500 representative images that reflect the real-world input your model will encounter. As the old saying goes, its quality over quantity — having a diverse, relevant set matters more than simply having many images.\n",
"\n",
"Quantization will cause some loss in model accuracy. However, if your calibration images are chosen wisely, this accuracy drop should be minimal and acceptable. If the sampled images are too uniform or unrelated, your quantized model's performance may worsen significantly.\n",
"\n",
"The script will automatically sample representative images randomly from the provided dataset. While this usually works well, please verify that the dataset contains diverse and relevant examples of your target objects. As a reminder, the images used to quantize the model are stored in the text file specified by `--img_dataset_txt`.\n"
],
"id": "80cf63a7f16f9af"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Optional: Download a dataset from Roboflow for quantization\n",
"\n",
"If you do not already have a dataset or set of images containing the objects you want to detect, follow the steps below to download one from Roboflow Universe.\n",
"\n",
"#### **Step 1: Search for a Dataset**\n",
"\n",
"Go to [Roboflow Universe](https://universe.roboflow.com) and use the search bar to locate a dataset relevant to what you want to detect.\n",
"**Note:** The dataset must include the classes or object types you intend to detect.\n",
"\n",
"#### **Step 2: Access the Dataset Tab**\n",
"\n",
"After selecting a suitable project, navigate to the **Dataset** tab. Click the **\"Download Dataset\"** button. A prompt will appear with several options, including:\n",
"\n",
"- Train a model with this dataset\n",
"- Train from a portion of this dataset\n",
"- Download dataset\n",
"\n",
"Select **Download dataset**.\n",
"\n",
"#### **Step 3: Choose Format and View Download Code**\n",
"\n",
"- Under **Image and Annotation Format**, choose the version of YOLO you are using:\n",
" - For **YOLOv5**, choose `YOLOv5 PyTorch`\n",
" - For **YOLOv8**, choose `YOLOv8`\n",
" - For **YOLOv11**, choose `YOLOv11`\n",
"- If multiple annotation formats are listed for your model, always select the one ending in **\"PyTorch\"**.\n",
"\n",
"Then, under **Download Options**, click **\"Show Download Code\"** and continue.\n",
"\n",
"In the resulting screen, you will see three tabs:\n",
"- **Jupyter**\n",
"- **Terminal**\n",
"- **Raw URL**\n",
"\n",
"Select the **Terminal** tab and copy the provided command.\n",
"\n",
"#### **Step 4: Paste and Run**\n",
"\n",
"Paste the copied command into the notebook cell below and run it. This will download and extract the dataset into your environment, making it ready for use in the quantization process.\n",
"\n",
"Make sure to prefix the command with \"`!`\" so it executes properly in this Jupyter Notebook environment."
],
"id": "ae229bef78e3bc0c"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "!curl -L \"https://universe.roboflow.com/ds/FaF3HbDmF7?key=iMoJR25O9H\" > roboflow.zip; unzip roboflow.zip; rm roboflow.zip",
"id": "e16fb4f928fd956b"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### RKNN Conversion Script\n",
"To get started, run the `create_rknn` script below, replacing the arguments with your own values. Refer to the table below for detailed information on each arguments purpose and usage. The `model_path` argument should point to your exported ONNX model from Step 1, and `img_dir` must reference a valid directory containing either a dataset or a set of images to be used for quantization.\n",
"\n",
"#### Overview of the `create_rknn` function\n",
"\n",
"This script converts a YOLO ONNX model to RKNN format using a set of calibration images. It's designed to work with either:\n",
"\n",
"- A flat directory of images (e.g. `train/images`), **or**\n",
"- A dataset directory containing a `data.yaml` file that defines `train`, `val`, and/or `test` folders.\n",
"\n",
"##### Arguments\n",
"\n",
"| Argument | Type | Description |\n",
"|----------|------|-----------------------------------------------------------------------------------------------------------------|\n",
"| `img_dir` | `str` (required) | Path to your image directory. This can either be a folder of images **or** a dataset folder with a `data.yaml`. |\n",
"| `model_path` | `str` (required) | Path to your YOLO ONNX model, created in Step 1. |\n",
"| `num_imgs` | `int` (default: `300`) | Number of images to use for quantization calibration. |\n",
"| `disable_quantize` | `bool` (default: `False`) | Set to `True` to skip quantization entirely. Not recommended for performance, and should not be used for deployment on PhotonVision, which requires quantization. |\n",
"| `rknn_output` | `str` (default: `out.rknn`) | File path where the final RKNN model should be saved. |\n",
"| `img_dataset_txt` | `str` (default: `imgs.txt`) | File path to store the list of images used during quantization. |\n",
"| `verbose` | `bool` (default: `False`) | Enable detailed logging from the RKNN API during conversion. |\n",
"\n",
"\n",
"##### *Notes*\n",
"\n",
"1. This script is designed for use with [PhotonVision](https://photonvision.org), and by default sets the target platform for RKNN conversion to `RK3588`, a chipset commonly found in many variants of the Orange Pi 5 series (e.g., Orange Pi 5, 5 Pro, 5 Plus, 5 Max, etc.). You may modify the `DEFAULT_PLATFORM` value in the setup cell to match your specific hardware or deployment requirements if necessary.\n",
"\n",
"2. If you followed the Roboflow dataset download instructions from the previous section, the dataset will have been extracted to your **current working directory**. In that case, you can simply set `img_dir` to \"`.`\" to reference the current directory."
],
"id": "f8f48b3139509618"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "create_rknn(img_dir=\"./datasets\", model_path=\"weights.onnx\")",
"id": "2c48b133f5c93c7a"
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -67,7 +67,7 @@ tasks.register('testHeadless', Test) {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
exceptionFormat "full"
exceptionFormat = "full"
showStandardStreams = true
}
exclude '**/*BenchmarkTest*'

View File

@@ -45,7 +45,7 @@ model {
task copyAllOutputs(type: Copy) {
def outputsFolder = file("$project.buildDir/outputs")
destinationDir outputsFolder
destinationDir = outputsFolder
}
ext.addTaskToCopyAllOutputs = { task ->
@@ -127,8 +127,14 @@ ext.createComponentZipTasks = { components, names, base, type, project, func ->
project.build.dependsOn task
project.artifacts {
task
// If the zip artifact matches the platform we're building for (either host or whatever the ArchOverride is), and it's a shared library built in release mode, add it to the list of artifacts the project exposes for use
if (key.contains(wpilibTools.getPlatformMapper().getWpilibClassifier()) && !key.contains("debug") && !key.contains("static")) {
// For more information, see https://docs.gradle.org/current/userguide/variant_model.html and the outgoingVariants task
project.artifacts.add("wpilibNatives", task)
} else {
project.artifacts {
task
}
}
addTaskToCopyAllOutputs(task)
}

View File

@@ -85,8 +85,8 @@ publishing {
artifact javadocJar
artifactId = "${baseArtifactId}-java"
groupId artifactGroupId
version pubVersion
groupId = artifactGroupId
version = pubVersion
}
}
@@ -97,10 +97,10 @@ publishing {
if (project.hasProperty('copyOfflineArtifacts')) {
url(localMavenURL)
} else {
url(photonMavenURL)
url = photonMavenURL
credentials {
username 'ghactions'
password System.getenv("ARTIFACTORY_API_KEY")
username = 'ghactions'
password = System.getenv("ARTIFACTORY_API_KEY")
}
}
}
@@ -112,7 +112,7 @@ test {
systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true'
testLogging {
events "failed"
exceptionFormat "full"
exceptionFormat = "full"
showStandardStreams = true
}
forkEvery = 1

View File

@@ -8,7 +8,7 @@ gradle.allprojects {
def stdout = new ByteArrayOutputStream()
String tagIsh
try {
exec {
project.exec {
commandLine 'git', 'describe', '--tags', "--match=v*"
standardOutput = stdout
}
@@ -19,7 +19,7 @@ gradle.allprojects {
// Dev tags: v2021.1.6-3-gf922466d
// We're specifically looking to capture the middle -3-
boolean isDev = tagIsh.matches(".*-[0-9]*-g[0-9a-f]*")
boolean isDev = tagIsh.matches(".*-[0-9a-z]*-g[0-9a-f]*")
if (isDev && !tagIsh.startsWith("dev-")) tagIsh = "dev-" + tagIsh
println("Picked up version: " + tagIsh)
return tagIsh