Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c5dc14a
add periods and underscores to allowed characters in object detection…
samfreund Jan 27, 2025
37cc78c
add periods and underscores to allowed characters in object detection…
samfreund Jan 27, 2025
c82ee14
idk man
samfreund Jan 27, 2025
617c394
move logic to NNMhandler
samfreund Jan 27, 2025
0b5e9ec
add test for names
samfreund Jan 27, 2025
8f3c268
add test for names
samfreund Jan 27, 2025
f587308
Merge branch 'regexxxxx' of github.com:Sam948-byte/photonvision into …
samfreund Jan 27, 2025
df14bca
linting?
samfreund Jan 27, 2025
b312650
istg linting will be the death of me
samfreund Jan 27, 2025
afcc1d7
Update ObjectDetectionTest.java
samfreund Jan 27, 2025
40e06af
Update ObjectDetectionTest.java
samfreund Jan 27, 2025
11f4b91
add comment explaining regex
samfreund Jan 27, 2025
87a81b5
Merge branch 'main' into regexxxxx
samfreund Jan 28, 2025
8d38ffc
adjust comments and parameterize tests
samfreund Jan 28, 2025
8ba9b73
formatting
samfreund Jan 28, 2025
34f34fd
broken, but changing computers
samfreund Jan 31, 2025
96b14f3
I think this should work
samfreund Jan 31, 2025
5ace06a
rewrite tests
samfreund Jan 31, 2025
7b4a073
fix tests
samfreund Jan 31, 2025
604b3f4
fix tests
samfreund Jan 31, 2025
ea89bf0
Merge branch 'regexxxxx' of github.com:Sam948-byte/photonvision into …
samfreund Jan 31, 2025
553d56d
linting
samfreund Jan 31, 2025
91250c8
linting 2 eletric boogaloo
samfreund Jan 31, 2025
94d1aa8
fix typo in the docs
samfreund Jan 31, 2025
6fe6be0
Merge branch 'main' into regexxxxx
samfreund Feb 1, 2025
93c7484
Merge branch 'main' into regexxxxx
samfreund Feb 4, 2025
3705847
Merge branch 'main' into regexxxxx
samfreund Feb 9, 2025
78dfefb
update regex for 11
samfreund Feb 9, 2025
9fe0dec
linting
samfreund Feb 9, 2025
2f56cc4
docs update
samfreund Feb 9, 2025
f9c0dc2
linting
samfreund Feb 9, 2025
5675369
add x support for yolo size
samfreund Feb 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/source/docs/objectDetection/about-object-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

PhotonVision supports object detection using neural network accelerator hardware built into Orange Pi 5/5+ coprocessors. Please note that the Orange Pi 5/5+ are the only coprocessors that are currently supported. The Neural Processing Unit, or NPU, is [used by PhotonVision](https://github.com/PhotonVision/rknn_jni/tree/main) to massively accelerate certain math operations like those needed for running ML-based object detection.

For the 2025 season, PhotonVision ships with a pretrained ALGAE model. A model to detect coral is not currently stable, and interested teams should ask in the Photonvision discord.
For the 2025 season, PhotonVision ships with a pretrained ALGAE model. A model to detect coral is not currently stable, and interested teams should ask in the Photonvision discord.

## Tracking Objects

Expand Down Expand Up @@ -36,19 +36,20 @@ Photonvision will letterbox your camera frame to 640x640. This means that if you
## Training Custom Models

:::{warning}
Power users only. This requires some setup, such as obtaining your own dataset and installing various tools. It's additionally advised to have a general knowledge of ML before attempting to train your own model. Additionally, this is not officialy supported by Photonvision, and any problems that may arise are not attributable to Photonvision.
Power users only. This requires some setup, such as obtaining your own dataset and installing various tools. It's additionally advised to have a general knowledge of ML before attempting to train your own model. Additionally, this is not officially supported by Photonvision, and any problems that may arise are not attributable to Photonvision.
:::

Before beginning, it is necessary to install the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2). Then, install the relevant [Ultralytics repository](https://github.com/airockchip?tab=repositories&q=yolo&type=&language=&sort=) from this list. After training your model, export it to ``rknn``. This will give you an ``onnx`` file, formatted for conversion. Copy this file to the relevant folder in [rknn_model_zoo](https://github.com/airockchip/rknn_model_zoo), and use the conversion script located there to convert it. If necessary, modify the script to provide the path to your training database for quantization.
Before beginning, it is necessary to install the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2). Then, install the relevant [Ultralytics repository](https://github.com/airockchip?tab=repositories&q=yolo&type=&language=&sort=) from this list. After training your model, export it to `rknn`. This will give you an `onnx` file, formatted for conversion. Copy this file to the relevant folder in [rknn_model_zoo](https://github.com/airockchip/rknn_model_zoo), and use the conversion script located there to convert it. If necessary, modify the script to provide the path to your training database for quantization.

## Uploading Custom Models

:::{warning}
PhotonVision currently ONLY supports 640x640 YOLOv5, YOLOv8, and YOLO11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLO11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
:::

In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
should be `name-verticalResolution-horizontalResolution-modelType`. Additionally, the labels
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For example, if the
RKNN file is named `note-640-640-yolov5s.rknn`, the labels file should be named
`note-640-640-yolov5s-labels.txt`.
should be `name-verticalResolution-horizontalResolution-modelType`. The
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
named `Algae_1.03.2025-640-640-yolov5s-labels.txt`.
11 changes: 6 additions & 5 deletions photon-client/src/components/settings/ObjectDetectionCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,12 @@ const supportedModels = computed(() => {
<v-card-title>Import New Object Detection Model</v-card-title>
<v-card-text>
Upload a new object detection model to this device that can be used in a pipeline. Naming convention
should be <code>name-verticalResolution-horizontalResolution-modelType</code>. Additionally, the labels
file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the end. For
example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file should be
named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5 & YOLOv8 models
trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
should be <code>name-verticalResolution-horizontalResolution-modelType</code>. The
<code>name</code> should only include alphanumeric characters, periods, and underscores. Additionally,
the labels file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the
end. For example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file
should be named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5 & YOLOv8
models trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
<v-row class="mt-6 ml-4 mr-8">
<v-file-input v-model="importRKNNFile" label="RKNN File" accept=".rknn" />
</v-row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
Expand Down Expand Up @@ -301,4 +303,66 @@ public void extractModels(File modelsDirectory) {
logger.error("Error extracting models", e);
}
}

private static Pattern modelPattern =
Pattern.compile("^([a-zA-Z0-9._]+)-(\\d+)-(\\d+)-(yolov(?:5|8|11)[nsmlx]*)\\.rknn$");

private static Pattern labelsPattern =
Pattern.compile("^([a-zA-Z0-9._]+)-(\\d+)-(\\d+)-(yolov(?:5|8|11)[nsmlx]*)-labels\\.txt$");

/**
* Check naming conventions for models and labels.
*
* <p>This is static as it is not dependent on the state of the class.
*
* @param modelName the name of the model
* @param labelsName the name of the labels file
* @throws IllegalArgumentException if the names are invalid
*/
public static void verifyRKNNNames(String modelName, String labelsName) {
// check null
if (modelName == null || labelsName == null) {
throw new IllegalArgumentException("Model name and labels name cannot be null");
}

// These patterns check that the naming convention of
// name-widthResolution-heightResolution-modelType is followed

Matcher modelMatcher = modelPattern.matcher(modelName);
Matcher labelsMatcher = labelsPattern.matcher(labelsName);

if (!modelMatcher.matches() || !labelsMatcher.matches()) {
throw new IllegalArgumentException(
"Model name and labels name must follow the naming convention of name-widthResolution-heightResolution-modelType.rknn and name-widthResolution-heightResolution-modelType-labels.txt");
}

if (!modelMatcher.group(1).equals(labelsMatcher.group(1))
|| !modelMatcher.group(2).equals(labelsMatcher.group(2))
|| !modelMatcher.group(3).equals(labelsMatcher.group(3))
|| !modelMatcher.group(4).equals(labelsMatcher.group(4))) {
throw new IllegalArgumentException("Model name and labels name must be matching.");
}
}

/**
* Parse RKNN name and return the name, width, height, and model type.
*
* <p>This is static as it is not dependent on the state of the class.
*
* @param modelName the name of the model
* @throws IllegalArgumentException if the model name does not follow the naming convention
* @return an array containing the name, width, height, and model type
*/
public static String[] parseRKNNName(String modelName) {
Matcher modelMatcher = modelPattern.matcher(modelName);

if (!modelMatcher.matches()) {
throw new IllegalArgumentException(
"Model name must follow the naming convention of name-widthResolution-heightResolution-modelType.rknn");
}

return new String[] {
modelMatcher.group(1), modelMatcher.group(2), modelMatcher.group(3), modelMatcher.group(4)
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.nio.file.Paths;
import java.util.List;
import org.opencv.core.Size;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.jni.RknnObjectDetector;
import org.photonvision.rknn.RknnJNI;

Expand Down Expand Up @@ -67,10 +68,8 @@ private static RknnJNI.ModelVersion getModelVersion(String modelName)
public RknnModel(File modelFile, String labels) throws IllegalArgumentException, IOException {
this.modelFile = modelFile;

String[] parts = modelFile.getName().split("-");
if (parts.length != 4) {
throw new IllegalArgumentException("Invalid model file name: " + modelFile);
}
// parseRKNNName throws an IllegalArgumentException if the model name is invalid
String[] parts = NeuralNetworkModelManager.parseRKNNName(modelFile.getName());

this.version = getModelVersion(parts[3]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package org.photonvision.vision.pipeline;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.LinkedList;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.photonvision.common.configuration.NeuralNetworkModelManager;

public class ObjectDetectionTest {
private static LinkedList<String[]> passNames =
new LinkedList<String[]>(
java.util.Arrays.asList(
new String[] {"note-640-640-yolov5s.rknn", "note-640-640-yolov5s-labels.txt"},
new String[] {"object-640-640-yolov8n.rknn", "object-640-640-yolov8n-labels.txt"},
new String[] {
"example_1.2-640-640-yolov5l.rknn", "example_1.2-640-640-yolov5l-labels.txt"
},
new String[] {"demo_3.5-640-640-yolov8m.rknn", "demo_3.5-640-640-yolov8m-labels.txt"},
new String[] {"sample-640-640-yolov5x.rknn", "sample-640-640-yolov5x-labels.txt"},
new String[] {
"test_case-640-640-yolov8s.rknn", "test_case-640-640-yolov8s-labels.txt"
},
new String[] {
"model_ABC-640-640-yolov5n.rknn", "model_ABC-640-640-yolov5n-labels.txt"
},
new String[] {"my_model-640-640-yolov8x.rknn", "my_model-640-640-yolov8x-labels.txt"},
new String[] {"name_1.0-640-640-yolov5n.rknn", "name_1.0-640-640-yolov5n-labels.txt"},
new String[] {
"valid_name-640-640-yolov8s.rknn", "valid_name-640-640-yolov8s-labels.txt"
},
new String[] {
"test.model-640-640-yolov5l.rknn", "test.model-640-640-yolov5l-labels.txt"
},
new String[] {
"case1_test-640-640-yolov8m.rknn", "case1_test-640-640-yolov8m-labels.txt"
},
new String[] {"A123-640-640-yolov5x.rknn", "A123-640-640-yolov5x-labels.txt"},
new String[] {
"z_y_test.model-640-640-yolov8n.rknn", "z_y_test.model-640-640-yolov8n-labels.txt"
}));
private static LinkedList<String[]> parsedPassNames =
new LinkedList<String[]>(
java.util.Arrays.asList(
new String[] {"note", "640", "640", "yolov5s"},
new String[] {"object", "640", "640", "yolov8n"},
new String[] {"example_1.2", "640", "640", "yolov5l"},
new String[] {"demo_3.5", "640", "640", "yolov8m"},
new String[] {"sample", "640", "640", "yolov5x"},
new String[] {"test_case", "640", "640", "yolov8s"},
new String[] {"model_ABC", "640", "640", "yolov5n"},
new String[] {"my_model", "640", "640", "yolov8x"},
new String[] {"name_1.0", "640", "640", "yolov5n"},
new String[] {"valid_name", "640", "640", "yolov8s"},
new String[] {"test.model", "640", "640", "yolov5l"},
new String[] {"case1_test", "640", "640", "yolov8m"},
new String[] {"A123", "640", "640", "yolov5x"},
new String[] {"z_y_test.model", "640", "640", "yolov8n"}));
private static LinkedList<String[]> failNames =
new LinkedList<String[]>(
java.util.Arrays.asList(
new String[] {"note-yolov5s.rknn", "note-640-640-yolov5s-labels.txt"},
new String[] {"640-640-yolov8n.rknn", "object-640-640-yolov8n-labels.txt"},
new String[] {"example_1.2.rknn", "example_1.2-640-640-yolov5l-labels.txt"},
new String[] {"demo_3.5-640-yolov8m.rknn", "demo_3.5-640-640-yolov8m-labels.txt"},
new String[] {"sample-640.rknn", "sample-640-640-yolov5x-labels.txt"},
new String[] {"test_case.txt", "test_case-640-640-yolov8s-labels.txt"},
new String[] {"model_ABC.onnx", "model_ABC-640-640-yolov5n-labels.txt"},
new String[] {"my_model", "my_model-640-640-yolov8x-labels.txt"},
new String[] {"name_1.0-yolov5n.rknn", "wrong-labels.txt"},
new String[] {"", "valid_name-640-640-yolov8s-labels.txt"},
new String[] {null, "test.model-640-640-yolov5l-labels.txt"},
new String[] {"case1_test-640-640-yolov8m.rknn", null},
new String[] {"A123-640-640.rknn", "different-labels.txt"},
new String[] {"z_y_test.model", ""}));

// Test the model name validation for names that ought to pass
@ParameterizedTest
@MethodSource("verifyPassNameProvider")
public void testRKNNVerificationPass(String[] names) {
NeuralNetworkModelManager.verifyRKNNNames(names[0], names[1]);
}

// // Test the model name validation for names that ought to fail
@ParameterizedTest
@MethodSource("verifyFailNameProvider")
public void testRNNVerificationFail(String[] names) {
assertThrows(
IllegalArgumentException.class,
() -> NeuralNetworkModelManager.verifyRKNNNames(names[0], names[1]));
}

// Test the model name parsing
@ParameterizedTest
@MethodSource("parseNameProvider")
public void testRKNNNameParsing(String[] expected, String name) {
String[] parsed = NeuralNetworkModelManager.parseRKNNName(name);
assertArrayEquals(expected, parsed);
}

static Stream<Arguments> verifyPassNameProvider() {
return passNames.stream().map(array -> Arguments.of((Object) array));
}

static Stream<Arguments> verifyFailNameProvider() {
return failNames.stream().map(array -> Arguments.of((Object) array));
}

static Stream<Arguments> parseNameProvider() {
// return a stream of parsed pass names, and the first element of each pass name
return passNames.stream()
.map(name -> Arguments.of(parsedPassNames.get(passNames.indexOf(name)), name[0]));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Optional;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import org.opencv.core.Mat;
Expand Down Expand Up @@ -573,25 +572,9 @@ public static void onImportObjectDetectionModelRequest(Context ctx) {
}

// verify naming convention
// this check will need to be modified if different model types are added

Pattern modelPattern =
Pattern.compile("^[a-zA-Z0-9]+-\\d+-\\d+-yolov(?:5|8|11)[a-z]*\\.rknn$");

Pattern labelsPattern =
Pattern.compile("^[a-zA-Z0-9]+-\\d+-\\d+-yolov(?:5|8|11)[a-z]*-labels\\.txt$");

if (!modelPattern.matcher(modelFile.filename()).matches()
|| !labelsPattern.matcher(labelsFile.filename()).matches()
|| !(modelFile
.filename()
.substring(0, modelFile.filename().indexOf("-"))
.equals(labelsFile.filename().substring(0, labelsFile.filename().indexOf("-"))))) {
ctx.status(400);
ctx.result("The uploaded files were not named correctly.");
logger.error("The uploaded object detection model files were not named correctly.");
return;
}
// throws IllegalArgumentException if the model name is invalid
NeuralNetworkModelManager.verifyRKNNNames(modelFile.filename(), labelsFile.filename());

// TODO move into neural network manager

Expand Down
Loading