Add 2025.3.1 backwards-compat to ML models (#2331)

This commit is contained in:
Matt Morley
2026-01-28 12:54:35 -08:00
committed by GitHub
parent 1b5f4fa802
commit a39844328d
29 changed files with 447 additions and 237 deletions

View File

@@ -39,7 +39,7 @@ import org.photonvision.vision.processes.VisionSource;
import org.zeroturnaround.zip.ZipUtil;
public class ConfigManager {
private static ConfigManager INSTANCE;
static ConfigManager INSTANCE;
public static final String HW_CFG_FNAME = "hardwareConfig.json";
public static final String HW_SET_FNAME = "hardwareSettings.json";

View File

@@ -218,7 +218,7 @@ class LegacyConfigProvider extends ConfigProvider {
hardwareSettings,
networkConfig,
atfl,
new NeuralNetworkPropertyManager(),
new NeuralNetworkModelsSettings(),
cameraConfigurations);
}

View File

@@ -34,7 +34,7 @@ import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -50,20 +50,20 @@ import org.photonvision.vision.objects.RubikModel;
* extracted to the filesystem, it will not be extracted again.
*
* <p>Each model must have a corresponding {@link ModelProperties} entry in {@link
* NeuralNetworkPropertyManager}.
* NeuralNetworkModelsSettings}.
*/
public class NeuralNetworkModelManager {
/** Singleton instance of the NeuralNetworkModelManager */
private static NeuralNetworkModelManager INSTANCE;
private final List<Family> supportedBackends = new ArrayList<>();
final List<Family> supportedBackends = new ArrayList<>();
/**
* This function stores the properties of the shipped object detection models. It is stored as a
* function so that it can be dynamic, to adjust for the models directory.
*/
private NeuralNetworkPropertyManager getShippedProperties(File modelsDirectory) {
NeuralNetworkPropertyManager nnProps = new NeuralNetworkPropertyManager();
private NeuralNetworkModelsSettings getShippedProperties(File modelsDirectory) {
NeuralNetworkModelsSettings nnProps = new NeuralNetworkModelsSettings();
LinkedList<String> cocoLabels =
new LinkedList<String>(
@@ -272,7 +272,7 @@ public class NeuralNetworkModelManager {
*
* <p>The first model in the list is the default model.
*/
private Map<Family, ArrayList<Model>> models;
Map<Family, ArrayList<Model>> models;
/**
* Retrieves the model with the specified name, assuming it is available under a supported
@@ -324,10 +324,23 @@ public class NeuralNetworkModelManager {
logger.error(
"Model properties are null. This could mean the config for model "
+ path
+ " was unable to be found in the database.");
return;
+ " was unable to be found in the database. Trying legacy...");
try {
properties = ModelProperties.createFromFilename(path.getFileName().toString());
// At this point this property is not serialized or known to our configuration. add to
// NeuralNetworkModelsSettings
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
.addModelProperties(properties);
} catch (IllegalArgumentException | IOException e) {
logger.error("Failed to translate legacy model filename to properties: " + path, e);
}
}
logger.debug(properties.toString());
if (!supportedBackends.contains(properties.family())) {
logger.warn(
"Model "
@@ -412,7 +425,7 @@ public class NeuralNetworkModelManager {
File modelsDirectory = ConfigManager.getInstance().getModelsDirectory();
// Filter shippedProprties by supportedBackends
NeuralNetworkPropertyManager supportedProperties = new NeuralNetworkPropertyManager();
NeuralNetworkModelsSettings supportedProperties = new NeuralNetworkModelsSettings();
for (ModelProperties model : getShippedProperties(modelsDirectory).getModels()) {
if (supportedBackends.contains(model.family())) {
supportedProperties.addModelProperties(model);

View File

@@ -0,0 +1,277 @@
/*
* 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.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
public class NeuralNetworkModelsSettings {
/*
* The properties of the model. This is used to determine which model to load.
* The only families currently supported are RKNN and Rubik (custom .tflite)
*/
public record ModelProperties(
@JsonProperty("modelPath") Path modelPath,
@JsonProperty("nickname") String nickname,
@JsonProperty("labels") List<String> labels,
@JsonProperty("resolutionWidth") int resolutionWidth,
@JsonProperty("resolutionHeight") int resolutionHeight,
@JsonProperty("family") Family family,
@JsonProperty("version") Version version) {
@JsonCreator
public ModelProperties {}
ModelProperties(ModelProperties other) {
this(
other.modelPath,
other.nickname,
other.labels, // note this does not clone the underlying list
other.resolutionWidth,
other.resolutionHeight,
other.family,
other.version);
}
// In v2025.3.1, this was single string for the model path. but the first argument
// is now nickname
public ModelProperties(@JsonProperty("nickname") String filename)
throws IllegalArgumentException, IOException {
this(createFromFilename(filename));
}
// ============= Migration code from v2025.3.1 ===========
private static Pattern modelPattern =
Pattern.compile("^([a-zA-Z0-9._]+)-(\\d+)-(\\d+)-(yolov(?:5|8|11)[nsmlx]*)\\.rknn$");
static ModelProperties createFromFilename(String modelFileName)
throws IllegalArgumentException, IOException {
// Used to point to default models directory
var model =
ConfigManager.getInstance().getModelsDirectory().toPath().resolve(modelFileName).toFile();
// Get the model extension and check if it is supported
String modelExtension = model.getName().substring(model.getName().lastIndexOf('.'));
if (!modelExtension.equals(".rknn")) {
throw new IllegalArgumentException("Model " + modelFileName + " is not a supported format");
}
var backend =
Arrays.stream(NeuralNetworkModelManager.Family.values())
.filter(b -> b.extension().equals(modelExtension))
.findFirst();
if (!backend.isPresent()) {
throw new IllegalArgumentException("Model " + modelFileName + " cannot find backend");
}
String labelFile = model.getAbsolutePath().replace(backend.get().extension(), "-labels.txt");
List<String> labels = Files.readAllLines(Paths.get(labelFile));
String[] parts = parseRKNNName(modelFileName);
var version = getModelVersion(parts[3]);
int width = Integer.parseInt(parts[1]);
int height = Integer.parseInt(parts[2]);
return new ModelProperties(
model.toPath(),
model.getName(),
labels,
// all files used to be 640x640
width,
height,
Family.RKNN,
version);
}
/**
* Determines the model version based on the model's filename.
*
* <p>"yolov5" -> "YOLO_V5"
*
* <p>"yolov8" -> "YOLO_V8"
*
* <p>"yolov11" -> "YOLO_V11"
*
* @param modelName The model's filename
* @return The model version
*/
private static Version getModelVersion(String modelName) throws IllegalArgumentException {
if (modelName.contains("yolov5")) {
return Version.YOLOV5;
} else if (modelName.contains("yolov8")) {
return Version.YOLOV8;
} else if (modelName.contains("yolov11")) {
return Version.YOLOV11;
} else {
throw new IllegalArgumentException("Unknown model version for model " + modelName);
}
}
/**
* 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)
};
}
}
// The path to the model is used as the key in the map because it is unique to
// the model, and should not change
@JsonProperty("modelPathToProperties")
private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>();
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects
*/
public NeuralNetworkModelsSettings() {}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects.
*
* @param modelPropertiesList When the class is constructed, it will hold the provided list
*/
public NeuralNetworkModelsSettings(HashMap<Path, ModelProperties> modelPropertiesList) {}
@Override
public String toString() {
String toReturn = "";
toReturn += "NeuralNetworkProperties [";
toReturn += modelPathToProperties.toString() + "]";
return toReturn;
}
/**
* Add a model to the list of models.
*
* @param modelProperties
*/
public void addModelProperties(ModelProperties modelProperties) {
modelPathToProperties.put(modelProperties.modelPath, modelProperties);
}
/**
* Add two Neural Network Properties together.
*
* <p>Any properties that are the same will be overwritten by the second
*
* @param nnProps
* @return itself, so it can be chained and used fluently
*/
public NeuralNetworkModelsSettings sum(NeuralNetworkModelsSettings nnProps) {
modelPathToProperties.putAll(nnProps.modelPathToProperties);
return this;
}
/**
* Remove a model from the list of models.
*
* @param modelPath
* @return True if the model was removed, false if it was not found
*/
public boolean removeModel(Path modelPath) {
return modelPathToProperties.remove(modelPath) != null;
}
/**
* Get the model properties for a given model path.
*
* @param modelPath
* @return {@link ModelProperties} object
*/
public ModelProperties getModel(Path modelPath) {
return modelPathToProperties.get(modelPath);
}
/**
* Get all models
*
* @return A list of all models
*/
@JsonIgnore
public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]);
}
/**
* Change the nickname of a {@link ModelProperties} object.
*
* @param modelPath
* @param newName
* @return True if the model was found and renamed, false if it was not found
*/
public boolean renameModel(Path modelPath, String newName) {
ModelProperties temp = modelPathToProperties.get(modelPath);
if (temp != null) {
modelPathToProperties.remove(modelPath);
modelPathToProperties.put(
modelPath,
new ModelProperties(
temp.modelPath,
newName,
temp.labels,
temp.resolutionWidth,
temp.resolutionHeight,
temp.family,
temp.version));
return true;
}
return false;
}
public boolean clear() {
modelPathToProperties.clear();
return true;
}
}

View File

@@ -1,164 +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.common.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.LinkedList;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
public class NeuralNetworkPropertyManager {
/*
* The properties of the model. This is used to determine which model to load.
* The only families currently supported are RKNN and Rubik (custom .tflite)
*/
public record ModelProperties(
@JsonProperty("modelPath") Path modelPath,
@JsonProperty("nickname") String nickname,
@JsonProperty("labels") LinkedList<String> labels,
@JsonProperty("resolutionWidth") int resolutionWidth,
@JsonProperty("resolutionHeight") int resolutionHeight,
@JsonProperty("family") Family family,
@JsonProperty("version") Version version) {
@JsonCreator
public ModelProperties {
// Record constructor is automatically annotated with @JsonCreator
}
}
// The path to the model is used as the key in the map because it is unique to
// the model, and should not change
@JsonProperty("modelPathToProperties")
private HashMap<Path, ModelProperties> modelPathToProperties =
new HashMap<Path, ModelProperties>();
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects
*/
public NeuralNetworkPropertyManager() {}
/**
* Constructor for the NeuralNetworkProperties class.
*
* <p>This object holds a LinkedList of {@link ModelProperties} objects.
*
* @param modelPropertiesList When the class is constructed, it will hold the provided list
*/
public NeuralNetworkPropertyManager(HashMap<Path, ModelProperties> modelPropertiesList) {}
@Override
public String toString() {
String toReturn = "";
toReturn += "NeuralNetworkProperties [";
toReturn += modelPathToProperties.toString() + "]";
return toReturn;
}
/**
* Add a model to the list of models.
*
* @param modelProperties
*/
public void addModelProperties(ModelProperties modelProperties) {
modelPathToProperties.put(modelProperties.modelPath, modelProperties);
}
/**
* Add two Neural Network Properties together.
*
* <p>Any properties that are the same will be overwritten by the second
*
* @param nnProps
* @return itself, so it can be chained and used fluently
*/
public NeuralNetworkPropertyManager sum(NeuralNetworkPropertyManager nnProps) {
modelPathToProperties.putAll(nnProps.modelPathToProperties);
return this;
}
/**
* Remove a model from the list of models.
*
* @param modelPath
* @return True if the model was removed, false if it was not found
*/
public boolean removeModel(Path modelPath) {
return modelPathToProperties.remove(modelPath) != null;
}
/**
* Get the model properties for a given model path.
*
* @param modelPath
* @return {@link ModelProperties} object
*/
public ModelProperties getModel(Path modelPath) {
return modelPathToProperties.get(modelPath);
}
/**
* Get all models
*
* @return A list of all models
*/
@JsonIgnore
public ModelProperties[] getModels() {
return modelPathToProperties.values().toArray(new ModelProperties[0]);
}
/**
* Change the nickname of a {@link ModelProperties} object.
*
* @param modelPath
* @param newName
* @return True if the model was found and renamed, false if it was not found
*/
public boolean renameModel(Path modelPath, String newName) {
ModelProperties temp = modelPathToProperties.get(modelPath);
if (temp != null) {
modelPathToProperties.remove(modelPath);
modelPathToProperties.put(
modelPath,
new ModelProperties(
temp.modelPath,
newName,
temp.labels,
temp.resolutionWidth,
temp.resolutionHeight,
temp.family,
temp.version));
return true;
}
return false;
}
public boolean clear() {
modelPathToProperties.clear();
return true;
}
}

View File

@@ -28,7 +28,7 @@ public class PhotonConfiguration {
private final HardwareSettings hardwareSettings;
private NetworkConfig networkConfig;
private AprilTagFieldLayout atfl;
private NeuralNetworkPropertyManager neuralNetworkProperties;
private NeuralNetworkModelsSettings neuralNetworkProperties;
private HashMap<String, CameraConfiguration> cameraConfigurations;
public PhotonConfiguration(
@@ -36,7 +36,7 @@ public class PhotonConfiguration {
HardwareSettings hardwareSettings,
NetworkConfig networkConfig,
AprilTagFieldLayout atfl,
NeuralNetworkPropertyManager neuralNetworkProperties) {
NeuralNetworkModelsSettings neuralNetworkProperties) {
this(
hardwareConfig,
hardwareSettings,
@@ -51,7 +51,7 @@ public class PhotonConfiguration {
HardwareSettings hardwareSettings,
NetworkConfig networkConfig,
AprilTagFieldLayout atfl,
NeuralNetworkPropertyManager neuralNetworkProperties,
NeuralNetworkModelsSettings neuralNetworkProperties,
HashMap<String, CameraConfiguration> cameraConfigurations) {
this.hardwareConfig = hardwareConfig;
this.hardwareSettings = hardwareSettings;
@@ -67,7 +67,7 @@ public class PhotonConfiguration {
new HardwareSettings(),
new NetworkConfig(),
new AprilTagFieldLayout(List.of(), 0, 0),
new NeuralNetworkPropertyManager());
new NeuralNetworkModelsSettings());
}
public HardwareConfig getHardwareConfig() {
@@ -86,7 +86,7 @@ public class PhotonConfiguration {
return atfl;
}
public NeuralNetworkPropertyManager neuralNetworkPropertyManager() {
public NeuralNetworkModelsSettings neuralNetworkPropertyManager() {
return neuralNetworkProperties;
}
@@ -98,7 +98,7 @@ public class PhotonConfiguration {
this.networkConfig = networkConfig;
}
public void setNeuralNetworkProperties(NeuralNetworkPropertyManager neuralNetworkProperties) {
public void setNeuralNetworkProperties(NeuralNetworkModelsSettings neuralNetworkProperties) {
this.neuralNetworkProperties = neuralNetworkProperties;
}
@@ -132,6 +132,16 @@ public class PhotonConfiguration {
@Override
public String toString() {
StringBuilder cameraConfigurationsString = new StringBuilder();
cameraConfigurations.forEach(
(key, value) -> {
cameraConfigurationsString
.append("\n ")
.append(key)
.append(" -> ")
.append(value.toString());
});
return "PhotonConfiguration [\n hardwareConfig="
+ hardwareConfig
+ "\n hardwareSettings="
@@ -142,8 +152,8 @@ public class PhotonConfiguration {
+ atfl
+ "\n neuralNetworkProperties="
+ neuralNetworkProperties
+ "\n cameraConfigurations="
+ cameraConfigurations
+ "\n]";
+ "\n cameraConfigurations={"
+ cameraConfigurationsString
+ "}\n]";
}
}

View File

@@ -313,8 +313,8 @@ public class SqlConfigProvider extends ConfigProvider {
loadConfigOrDefault(
conn,
GlobalKeys.NEURAL_NETWORK_PROPERTIES,
NeuralNetworkPropertyManager.class,
NeuralNetworkPropertyManager::new);
NeuralNetworkModelsSettings.class,
NeuralNetworkModelsSettings::new);
var atfl =
loadConfigOrDefault(
conn, GlobalKeys.ATFL_CONFIG_FILE, AprilTagFieldLayout.class, this::atflDefault);
@@ -612,52 +612,65 @@ public class SqlConfigProvider extends ConfigProvider {
// Iterate over every row/"camera" in the table
while (result.next()) {
List<String> dummyList = new ArrayList<>();
String uniqueName = "";
try {
List<String> dummyList = new ArrayList<>();
var uniqueName = result.getString(Columns.CAM_UNIQUE_NAME);
uniqueName = result.getString(Columns.CAM_UNIQUE_NAME);
// A horrifying hack to keep backward compat with otherpaths
// We -really- need to delete this -stupid- otherpaths column. I hate it.
var configStr = result.getString(Columns.CAM_CONFIG_JSON);
CameraConfiguration config = JacksonUtils.deserialize(configStr, CameraConfiguration.class);
// A horrifying hack to keep backward compat with otherpaths
// We -really- need to delete this -stupid- otherpaths column. I hate it.
var configStr = result.getString(Columns.CAM_CONFIG_JSON);
CameraConfiguration config =
JacksonUtils.deserialize(configStr, CameraConfiguration.class);
if (config.matchedCameraInfo == null) {
logger.info("Legacy CameraConfiguration detected - upgrading");
if (config.matchedCameraInfo == null) {
logger.info("Legacy CameraConfiguration detected - upgrading");
// manually create the matchedCameraInfo ourselves. Need to upgrade:
// baseName, path, otherPaths, cameraType, usbvid/pid -> matchedCameraInfo
config.matchedCameraInfo =
JacksonUtils.deserialize(configStr, LegacyCameraConfigStruct.class).matchedCameraInfo;
// manually create the matchedCameraInfo ourselves. Need to upgrade:
// baseName, path, otherPaths, cameraType, usbvid/pid -> matchedCameraInfo
config.matchedCameraInfo =
JacksonUtils.deserialize(configStr, LegacyCameraConfigStruct.class)
.matchedCameraInfo;
// Except that otherPaths used to be its own column. so hack that in here as well
var otherPaths =
// Except that otherPaths used to be its own column. so hack that in here as well
var otherPaths =
JacksonUtils.deserialize(
result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
if (config.matchedCameraInfo instanceof UsbCameraInfo usbInfo) {
usbInfo.otherPaths = otherPaths;
}
}
var driverMode =
JacksonUtils.deserialize(
result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
if (config.matchedCameraInfo instanceof UsbCameraInfo usbInfo) {
usbInfo.otherPaths = otherPaths;
result.getString(Columns.CAM_DRIVERMODE_JSON), DriverModePipelineSettings.class);
List<?> pipelineSettings =
JacksonUtils.deserialize(
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
for (var setting : pipelineSettings) {
if (setting instanceof String str) {
try {
loadedSettings.add(JacksonUtils.deserialize(str, CVPipelineSettings.class));
} catch (IOException e) {
logger.error(
"Could not deserialize pipeline setting for camera " + config.nickname, e);
}
}
}
config.pipelineSettings = loadedSettings;
config.driveModeSettings = driverMode;
loadedConfigurations.put(uniqueName, config);
} catch (IOException e) {
logger.error(
"Could not deserialize camera configuration " + uniqueName + " from database!", e);
}
var driverMode =
JacksonUtils.deserialize(
result.getString(Columns.CAM_DRIVERMODE_JSON), DriverModePipelineSettings.class);
List<?> pipelineSettings =
JacksonUtils.deserialize(
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
List<CVPipelineSettings> loadedSettings = new ArrayList<>();
for (var setting : pipelineSettings) {
if (setting instanceof String str) {
loadedSettings.add(JacksonUtils.deserialize(str, CVPipelineSettings.class));
}
}
config.pipelineSettings = loadedSettings;
config.driveModeSettings = driverMode;
loadedConfigurations.put(uniqueName, config);
}
} catch (SQLException | IOException e) {
logger.error("Err loading cameras: ", e);
} catch (SQLException e) {
logger.error("Err querying database to load cameras: ", e);
} finally {
try {
if (query != null) query.close();

View File

@@ -18,14 +18,14 @@
package org.photonvision.common.dataflow.websocket;
import java.util.List;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings;
public class UIGeneralSettings {
public UIGeneralSettings(
String version,
String gpuAcceleration,
boolean mrCalWorking,
NeuralNetworkPropertyManager.ModelProperties[] availableModels,
NeuralNetworkModelsSettings.ModelProperties[] availableModels,
List<String> supportedBackends,
String hardwareModel,
String hardwarePlatform,
@@ -45,7 +45,7 @@ public class UIGeneralSettings {
public String version;
public String gpuAcceleration;
public boolean mrCalWorking;
public NeuralNetworkPropertyManager.ModelProperties[] availableModels;
public NeuralNetworkModelsSettings.ModelProperties[] availableModels;
public List<String> supportedBackends;
public String hardwareModel;
public String hardwarePlatform;

View File

@@ -18,6 +18,7 @@
package org.photonvision.common.util.file;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.StreamReadFeature;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -80,6 +81,7 @@ public class JacksonUtils {
pathModule.addKeyDeserializer(Path.class, new PathKeyDeserializer());
return JsonMapper.builder()
.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
.configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)

View File

@@ -18,7 +18,7 @@
package org.photonvision.vision.objects;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
public interface Model {
public ObjectDetector load();

View File

@@ -20,7 +20,7 @@ package org.photonvision.vision.objects;
import java.util.List;
import org.opencv.core.Mat;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.vision.pipe.impl.NeuralNetworkPipeResult;
/**

View File

@@ -21,7 +21,7 @@ import java.io.File;
import org.opencv.core.Size;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
public class RknnModel implements Model {
public final File modelFile;

View File

@@ -21,7 +21,7 @@ import java.io.File;
import org.opencv.core.Size;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
public class RubikModel implements Model {
public final File modelFile;

View File

@@ -18,13 +18,13 @@
package org.photonvision.vision.pipeline;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings;
import org.photonvision.vision.objects.Model;
public class ObjectDetectionPipelineSettings extends AdvancedPipelineSettings {
public double confidence;
public double nms; // non maximal suppression
public NeuralNetworkPropertyManager.ModelProperties model;
public NeuralNetworkModelsSettings.ModelProperties model;
public ObjectDetectionPipelineSettings() {
super();

View File

@@ -25,7 +25,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import org.opencv.core.Point;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.common.dataflow.DataChangeSubscriber;
import org.photonvision.common.dataflow.events.DataChangeEvent;
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;

View File

@@ -25,13 +25,13 @@ import java.util.LinkedList;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Version;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.common.util.file.JacksonUtils;
public class NeuralNetworkPropertyManagerTest {
@Test
void testSerialization() {
var nnpm = new NeuralNetworkPropertyManager();
var nnpm = new NeuralNetworkModelsSettings();
// Path is always serialized as absolute; for the test to pass, this must also be made absolute
nnpm.addModelProperties(
new ModelProperties(
@@ -45,7 +45,7 @@ public class NeuralNetworkPropertyManagerTest {
String result = assertDoesNotThrow(() -> JacksonUtils.serializeToString(nnpm));
var deserializedNnpm =
assertDoesNotThrow(
() -> JacksonUtils.deserialize(result, NeuralNetworkPropertyManager.class));
() -> JacksonUtils.deserialize(result, NeuralNetworkModelsSettings.class));
assertEquals(nnpm.getModels()[0], deserializedNnpm.getModels()[0]);
}
}

View File

@@ -20,6 +20,7 @@ package org.photonvision.common.configuration;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.fasterxml.jackson.core.JsonProcessingException;
import edu.wpi.first.cscore.UsbCameraInfo;
import java.nio.file.Path;
import java.util.List;
@@ -28,11 +29,13 @@ import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.photonvision.common.LoadJNI;
import org.photonvision.common.configuration.NeuralNetworkModelManager.Family;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.camera.PVCameraInfo;
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
import org.photonvision.vision.pipeline.ObjectDetectionPipelineSettings;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
public class SQLConfigTest {
@@ -104,4 +107,52 @@ public class SQLConfigTest {
.hasQuirk(c));
}
}
void common2025p3p1Assertions(PhotonConfiguration config) {
// Make sure we got 8 cameras
assertEquals(8, config.getCameraConfigurations().size());
// Make sure exactly 2 have object detection pipelines
long count =
config.getCameraConfigurations().values().stream()
.filter(
c ->
c.pipelineSettings.stream()
.anyMatch(s -> s instanceof ObjectDetectionPipelineSettings))
.count();
assertEquals(2, count);
}
@Test
public void testLoadNewNNMM() throws JsonProcessingException {
var folder = TestUtils.getConfigDirectoriesPath(false).resolve("2025.3.1-old-nnmm");
var cfgManager = new ConfigManager(folder, new SqlConfigProvider(folder));
// Replace global configmanager
ConfigManager.INSTANCE = cfgManager;
assertDoesNotThrow(cfgManager::load);
System.out.println(cfgManager.getConfig());
common2025p3p1Assertions(cfgManager.getConfig());
// And we now see two models
NeuralNetworkModelManager.getInstance();
// force us to allow RKNN
NeuralNetworkModelManager.getInstance().supportedBackends.add(Family.RKNN);
NeuralNetworkModelManager.getInstance().discoverModels();
assertEquals(5, NeuralNetworkModelManager.getInstance().models.get(Family.RKNN).size());
ConfigManager.getInstance().saveToDisk();
// Now that we have the config saved, load it again
var reloadedProvider = new SqlConfigProvider(folder);
reloadedProvider.load();
common2025p3p1Assertions(reloadedProvider.getConfig());
// And make sure NNPM has all 5 models
assertEquals(5, reloadedProvider.getConfig().neuralNetworkPropertyManager().getModels().length);
ConfigManager.INSTANCE = null;
}
}

View File

@@ -39,7 +39,7 @@ import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.configuration.NeuralNetworkPropertyManager.ModelProperties;
import org.photonvision.common.configuration.NeuralNetworkModelsSettings.ModelProperties;
import org.photonvision.common.dataflow.DataChangeDestination;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.IncomingWebSocketEvent;