From 0b09e72771896f8c724a4c02cb8912a1530efe34 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 7 Jun 2020 12:25:18 -0700 Subject: [PATCH] Config (#101) * Add some config stuff, run format * Create JacksonUtils.java * Fix deserialization, run wpiformat * Update ConfigTest.java * Change config root folder, auto-sort pipelines when added to CameraConfiguration * Split ConfigTest tests in to multiple tests * Fix unsatisfied link error in config test * Add equals and hashCode to all PipelineSettings classes * Fix NPE in config test * run spotless * Address changes Co-authored-by: Banks Troutman Co-authored-by: ori agranat --- .gitignore | 2 +- chameleon-server/.gitignore | 3 +- chameleon-server/build.gradle | 2 - .../_2/config/CameraConfig.java | 2 +- .../_2/config/ConfigManager.java | 2 +- .../_2/config/PipelineConfig.java | 2 +- .../configuration/CameraConfiguration.java | 49 ++++ .../configuration/ChameleonConfiguration.java | 43 ++++ .../common/configuration/ConfigFile.java | 11 - .../common/configuration/ConfigFolder.java | 11 - .../common/configuration/ConfigManager.java | 231 +++++++++++++++++- .../common/configuration/HardwareConfig.java | 6 + .../common/configuration/NetworkConfig.java | 12 + .../common/util/numbers/NumberCouple.java | 3 + .../pipeline/AdvancedPipelineSettings.java | 51 ++++ .../vision/pipeline/CVPipelineSettings.java | 48 ++++ .../ColoredShapePipelineSettings.java | 22 ++ .../pipeline/DriverModePipelineSettings.java | 7 + .../pipeline/ReflectivePipelineSettings.java | 45 ++++ .../vision/processes/PipelineManager.java | 11 +- .../common/vision/target/TargetModel.java | 33 ++- .../common/configuration/ConfigTest.java | 105 ++++++++ 22 files changed, 654 insertions(+), 47 deletions(-) create mode 100644 chameleon-server/src/main/java/com/chameleonvision/common/configuration/CameraConfiguration.java create mode 100644 chameleon-server/src/main/java/com/chameleonvision/common/configuration/ChameleonConfiguration.java delete mode 100644 chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFile.java delete mode 100644 chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFolder.java create mode 100644 chameleon-server/src/main/java/com/chameleonvision/common/configuration/HardwareConfig.java create mode 100644 chameleon-server/src/main/java/com/chameleonvision/common/configuration/NetworkConfig.java create mode 100644 chameleon-server/src/test/java/com/chameleonvision/common/configuration/ConfigTest.java diff --git a/.gitignore b/.gitignore index beb360ffd..5878f011f 100644 --- a/.gitignore +++ b/.gitignore @@ -122,4 +122,4 @@ New client/chameleon-client/* .DS_Store # *.iml chameleon-server/build -chameleon-server/chameleon-vision (1).iml +chameleon-server/chameleon-vision diff --git a/chameleon-server/.gitignore b/chameleon-server/.gitignore index 31636b185..87e45e9ff 100644 --- a/chameleon-server/.gitignore +++ b/chameleon-server/.gitignore @@ -2,4 +2,5 @@ bin/* .settings/* .project .classpath -*.prefs \ No newline at end of file +*.prefs +chameleonVision \ No newline at end of file diff --git a/chameleon-server/build.gradle b/chameleon-server/build.gradle index d8ee1a321..0010659e2 100644 --- a/chameleon-server/build.gradle +++ b/chameleon-server/build.gradle @@ -41,8 +41,6 @@ dependencies { implementation "org.apache.commons:commons-collections4:4.4" implementation "org.apache.commons:commons-exec:1.3" - implementation "com.moandjiezana.toml:toml4j:0.7.2" - // wpilib stuff implementation "edu.wpi.first.wpiutil:wpiutil-java:$wpilibVersion" implementation "edu.wpi.first.cameraserver:cameraserver-java:$wpilibVersion" diff --git a/chameleon-server/src/main/java/com/chameleonvision/_2/config/CameraConfig.java b/chameleon-server/src/main/java/com/chameleonvision/_2/config/CameraConfig.java index eef4da9a2..b5194426f 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/_2/config/CameraConfig.java +++ b/chameleon-server/src/main/java/com/chameleonvision/_2/config/CameraConfig.java @@ -2,7 +2,7 @@ package com.chameleonvision._2.config; import com.chameleonvision._2.vision.pipeline.CVPipelineSettings; import com.chameleonvision.common.util.file.FileUtils; -import com.chameleonvision.common.util.file.JacksonUtils; +import com.chameleonvision.common.util.jackson.JacksonUtils; import java.io.File; import java.io.IOException; import java.nio.file.Files; diff --git a/chameleon-server/src/main/java/com/chameleonvision/_2/config/ConfigManager.java b/chameleon-server/src/main/java/com/chameleonvision/_2/config/ConfigManager.java index 2574c21c4..874f4faf5 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/_2/config/ConfigManager.java +++ b/chameleon-server/src/main/java/com/chameleonvision/_2/config/ConfigManager.java @@ -5,7 +5,7 @@ import com.chameleonvision._2.vision.pipeline.CVPipelineSettings; import com.chameleonvision.common.util.Platform; import com.chameleonvision.common.util.ShellExec; import com.chameleonvision.common.util.file.FileUtils; -import com.chameleonvision.common.util.file.JacksonUtils; +import com.chameleonvision.common.util.jackson.JacksonUtils; import java.io.File; import java.io.IOException; import java.nio.file.Files; diff --git a/chameleon-server/src/main/java/com/chameleonvision/_2/config/PipelineConfig.java b/chameleon-server/src/main/java/com/chameleonvision/_2/config/PipelineConfig.java index ae48c7917..18b675d31 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/_2/config/PipelineConfig.java +++ b/chameleon-server/src/main/java/com/chameleonvision/_2/config/PipelineConfig.java @@ -5,7 +5,7 @@ import com.chameleonvision._2.config.serializers.StandardCVPipelineSettingsSeria import com.chameleonvision._2.vision.pipeline.CVPipelineSettings; import com.chameleonvision._2.vision.pipeline.impl.StandardCVPipelineSettings; import com.chameleonvision.common.util.file.FileUtils; -import com.chameleonvision.common.util.file.JacksonUtils; +import com.chameleonvision.common.util.jackson.JacksonUtils; import java.io.File; import java.io.IOException; import java.nio.file.Files; diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/CameraConfiguration.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/CameraConfiguration.java new file mode 100644 index 000000000..afd6d18ca --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/CameraConfiguration.java @@ -0,0 +1,49 @@ +package com.chameleonvision.common.configuration; + +import com.chameleonvision.common.calibration.CameraCalibrationCoefficients; +import com.chameleonvision.common.logging.LogGroup; +import com.chameleonvision.common.logging.Logger; +import com.chameleonvision.common.vision.pipeline.CVPipelineSettings; +import com.chameleonvision.common.vision.pipeline.DriverModePipelineSettings; +import com.chameleonvision.common.vision.processes.PipelineManager; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.ArrayList; +import java.util.List; + +public class CameraConfiguration { + private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera); + + public String name = ""; + public String nickname = ""; + public double FOV = 70; + public CameraCalibrationCoefficients calibration; + + @JsonIgnore // this ignores the pipes as we serialize them to their own subfolder + public final List pipelineSettings = new ArrayList<>(); + + @JsonIgnore + public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings(); + + public void addPipelineSettings(List settings) { + for (var setting : settings) { + addPipelineSetting(setting); + } + } + + public void addPipelineSetting(CVPipelineSettings setting) { + if (pipelineSettings.stream() + .anyMatch(s -> s.pipelineNickname.equalsIgnoreCase(setting.pipelineNickname))) { + logger.error("Could not name two pipelines the same thing! Renaming"); + setting.pipelineNickname += "_1"; // TODO verify this logic + } + + if (pipelineSettings.stream().anyMatch(s -> s.pipelineIndex == setting.pipelineIndex)) { + var newIndex = pipelineSettings.size(); + logger.error("Could not insert two pipelines at same index! Changing to " + newIndex); + setting.pipelineIndex = newIndex; // TODO verify this logic + } + + pipelineSettings.add(setting); + pipelineSettings.sort(PipelineManager.PipelineSettingsIndexComparator); + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ChameleonConfiguration.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ChameleonConfiguration.java new file mode 100644 index 000000000..988d360b7 --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ChameleonConfiguration.java @@ -0,0 +1,43 @@ +package com.chameleonvision.common.configuration; + +import java.util.HashMap; + +public class ChameleonConfiguration { + public HardwareConfig getHardwareConfig() { + return hardwareConfig; + } + + public NetworkConfig getNetworkConfig() { + return networkConfig; + } + + public HashMap getCameraConfigurations() { + return cameraConfigurations; + } + + public void addCameraConfig(CameraConfiguration config) { + addCameraConfig(config.name, config); + } + + public void addCameraConfig(String name, CameraConfiguration config) { + cameraConfigurations.put(name, config); + } + + private HardwareConfig hardwareConfig; + private NetworkConfig networkConfig; + + private HashMap cameraConfigurations; + + public ChameleonConfiguration(HardwareConfig hardwareConfig, NetworkConfig networkConfig) { + this(hardwareConfig, networkConfig, new HashMap<>()); + } + + public ChameleonConfiguration( + HardwareConfig hardwareConfig, + NetworkConfig networkConfig, + HashMap cameraConfigurations) { + this.hardwareConfig = hardwareConfig; + this.networkConfig = networkConfig; + this.cameraConfigurations = cameraConfigurations; + } +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFile.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFile.java deleted file mode 100644 index 3f25637ef..000000000 --- a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFile.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.chameleonvision.common.configuration; - -public class ConfigFile { - - /** - * Represents a config file at a fixed path - * - * @param path Path to config file - */ - public ConfigFile(String path) {} -} diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFolder.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFolder.java deleted file mode 100644 index 5f712d7eb..000000000 --- a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigFolder.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.chameleonvision.common.configuration; - -public class ConfigFolder { - - /** - * Represents a folder of config files - * - * @param path path to config file - */ - public ConfigFolder(String path) {} -} diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigManager.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigManager.java index 03777de33..3cc3ac581 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigManager.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/ConfigManager.java @@ -1,18 +1,229 @@ package com.chameleonvision.common.configuration; +import com.chameleonvision.common.logging.LogGroup; +import com.chameleonvision.common.logging.Logger; +import com.chameleonvision.common.util.file.JacksonUtils; +import com.chameleonvision.common.vision.pipeline.CVPipelineSettings; +import com.chameleonvision.common.vision.pipeline.DriverModePipelineSettings; +import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + public class ConfigManager { + private static final Logger logger = new Logger(ConfigManager.class, LogGroup.General); + private static ConfigManager INSTANCE; - private final ConfigFolder rootFolder; - - protected ConfigManager() { - rootFolder = new ConfigFolder(""); - } - - private static class SingletonHolder { - private static final ConfigManager INSTANCE = new ConfigManager(); - } + private ChameleonConfiguration config; + private final File rootFolder; + private final File hardwareConfigFile; + private final File networkConfigFile; + private final File camerasFolder; public static ConfigManager getInstance() { - return SingletonHolder.INSTANCE; + if (INSTANCE == null) { + INSTANCE = new ConfigManager(getRootFolder()); + } + return INSTANCE; + } + + public ChameleonConfiguration getConfig() { + return config; + } + + protected static Path getRootFolder() { + return Path.of("chameleon-vision"); // TODO change root folder? + } + + private ConfigManager(Path rootFolder) { + this.rootFolder = new File(rootFolder.toUri()); + this.hardwareConfigFile = + new File(Path.of(rootFolder.toString(), "hardwareConfig.json").toUri()); + this.networkConfigFile = + new File(Path.of(rootFolder.toString(), "networkSettings.json").toUri()); + this.camerasFolder = new File(Path.of(rootFolder.toString(), "cameras").toUri()); + load(); + } + + public void load() { + logger.info("Loading settings..."); + if (!rootFolder.exists()) { + if (rootFolder.mkdirs()) { + logger.debug("Root config folder did not exist. Created!"); + } else { + logger.error("Failed to create root config folder!"); + } + } + + HardwareConfig hardwareConfig; + NetworkConfig networkConfig; + + try { + hardwareConfig = JacksonUtils.deserialize(hardwareConfigFile.toPath(), HardwareConfig.class); + if (hardwareConfig == null) { + logger.error("Could not deserialize hardware config! Loading defaults"); + hardwareConfig = new HardwareConfig(); + } + } catch (IOException e) { + logger.error("Could not deserialize hardware config! Loading defaults"); + hardwareConfig = new HardwareConfig(); + } + + try { + networkConfig = JacksonUtils.deserialize(networkConfigFile.toPath(), NetworkConfig.class); + if (networkConfig == null) { + logger.error("Could not deserialize network config! Loading defaults"); + networkConfig = new NetworkConfig(); + } + } catch (IOException e) { + logger.error("Could not deserialize network config! Loading defaults"); + networkConfig = new NetworkConfig(); + } + + if (!camerasFolder.exists()) { + if (camerasFolder.mkdirs()) { + logger.debug("Cameras config folder did not exist. Created!"); + } else { + logger.error("Failed to create cameras config folder!"); + } + } + + HashMap cameraConfigurations = loadCameraConfigs(); + + this.config = new ChameleonConfiguration(hardwareConfig, networkConfig, cameraConfigurations); + } + + public void save() { + logger.info("Saving settings..."); + + try { + JacksonUtils.serializer(hardwareConfigFile.toPath(), config.getHardwareConfig()); + } catch (IOException e) { + logger.error("Could not save hardware config!"); + e.printStackTrace(); + } + try { + JacksonUtils.serializer(networkConfigFile.toPath(), config.getNetworkConfig()); + } catch (IOException e) { + logger.error("Could not save network config!"); + e.printStackTrace(); + } + + // save all of our cameras + var cameraConfigMap = config.getCameraConfigurations(); + for (var subdirName : cameraConfigMap.keySet()) { + var camConfig = cameraConfigMap.get(subdirName); + var subdir = Path.of(camerasFolder.toPath().toString(), subdirName); + + if (!subdir.toFile().exists()) { + subdir.toFile().mkdirs(); + } + + try { + JacksonUtils.serializer(Path.of(subdir.toString(), "config.json"), camConfig); + } catch (IOException e) { + logger.error("Could not save config.json for " + subdir); + } + + try { + JacksonUtils.serializer( + Path.of(subdir.toString(), "drivermode.json"), camConfig.driveModeSettings); + } catch (IOException e) { + logger.error("Could not save drivermode.json for " + subdir); + } + + for (var pipe : camConfig.pipelineSettings) { + var pipePath = Path.of(subdir.toString(), "pipelines", pipe.pipelineNickname + ".json"); + + if (!pipePath.getParent().toFile().exists()) { + pipePath.getParent().toFile().mkdirs(); + } + + try { + JacksonUtils.serializer(pipePath, pipe); + } catch (IOException e) { + logger.error("Could not save " + pipe.pipelineNickname + ".json!"); + } + } + } + } + + private HashMap loadCameraConfigs() { + HashMap loadedConfigurations = new HashMap<>(); + try { + var subdirectories = + Files.list(camerasFolder.toPath()) + .filter(f -> f.toFile().isDirectory()) + .collect(Collectors.toList()); + + for (var subdir : subdirectories) { + var cameraConfigPath = Path.of(subdir.toString(), "config.json"); + CameraConfiguration loadedConfig = + JacksonUtils.deserialize(cameraConfigPath.toAbsolutePath(), CameraConfiguration.class); + if (loadedConfig == null) { + logger.warn("Could not load camera " + subdir + "'s config.json! Loading " + "default"); + loadedConfig = new CameraConfiguration(); + } + + // At this point we have only loaded the base stuff + // We still need to deserialize pipelines, as well as + // driver mode settings + var driverModeFile = Path.of(subdir.toString(), "drivermode.json"); + DriverModePipelineSettings driverMode; + try { + driverMode = + JacksonUtils.deserialize( + driverModeFile.toAbsolutePath(), DriverModePipelineSettings.class); + } catch (JsonProcessingException e) { + logger.error("Could not deserialize drivermode.json! Loading defaults"); + logger.de_pest(Arrays.toString(e.getStackTrace())); + driverMode = new DriverModePipelineSettings(); + } + if (driverMode == null) { + logger.warn( + "Could not load camera " + subdir + "'s drivermode.json! Loading" + " default"); + driverMode = new DriverModePipelineSettings(); + } + + // Load pipelines by mapping the files within the pipelines subdir + // to their deserialized equivalents + List settings = + Files.list(Path.of(subdir.toString(), "pipelines")) + .filter(p -> p.toFile().isFile()) + .map( + p -> { + var relativizedFilePath = + getRootFolder().toAbsolutePath().relativize(p).toString(); + try { + var ret = JacksonUtils.deserialize(p, CVPipelineSettings.class); + return ret; + + } catch (JsonProcessingException e) { + logger.error("Exception while deserializing " + relativizedFilePath); + e.printStackTrace(); + } catch (IOException e) { + logger.warn( + "Could not load pipeline at " + relativizedFilePath + "! Skipping..."); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + loadedConfig.driveModeSettings = driverMode; + loadedConfig.addPipelineSettings(settings); + + loadedConfigurations.put(subdir.toFile().getName(), loadedConfig); + } + } catch (IOException e) { + e.printStackTrace(); + } + return loadedConfigurations; } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/HardwareConfig.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/HardwareConfig.java new file mode 100644 index 000000000..6b604b0cd --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/HardwareConfig.java @@ -0,0 +1,6 @@ +package com.chameleonvision.common.configuration; + +/** Defines a;lskdjfa;dsklf */ +public class HardwareConfig { + public int ledPin = 1; // just to stop jackson from yeeting +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/configuration/NetworkConfig.java b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/NetworkConfig.java new file mode 100644 index 000000000..bf8041336 --- /dev/null +++ b/chameleon-server/src/main/java/com/chameleonvision/common/configuration/NetworkConfig.java @@ -0,0 +1,12 @@ +package com.chameleonvision.common.configuration; + +import com.chameleonvision.common.networking.NetworkMode; + +public class NetworkConfig { + public int teamNumber = 1577; + public NetworkMode connectionType = NetworkMode.DHCP; + public String ip = ""; + public String gateway = ""; + public String netmask = ""; + public String hostname = "chameleon-vision"; +} diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/util/numbers/NumberCouple.java b/chameleon-server/src/main/java/com/chameleonvision/common/util/numbers/NumberCouple.java index bc5c0b14c..26f5ff913 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/util/numbers/NumberCouple.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/util/numbers/NumberCouple.java @@ -1,5 +1,7 @@ package com.chameleonvision.common.util.numbers; +import com.fasterxml.jackson.annotation.JsonIgnore; + public abstract class NumberCouple { protected T first; @@ -49,6 +51,7 @@ public abstract class NumberCouple { return true; } + @JsonIgnore public boolean isEmpty() { return first.intValue() == 0 && second.intValue() == 0; } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/AdvancedPipelineSettings.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/AdvancedPipelineSettings.java index 57837bdef..f2ccc1ade 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/AdvancedPipelineSettings.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/AdvancedPipelineSettings.java @@ -6,6 +6,7 @@ import com.chameleonvision.common.vision.opencv.ContourSortMode; import com.chameleonvision.common.vision.target.RobotOffsetPointMode; import com.chameleonvision.common.vision.target.TargetOffsetPointEdge; import com.chameleonvision.common.vision.target.TargetOrientation; +import java.util.Objects; public class AdvancedPipelineSettings extends CVPipelineSettings { @@ -49,4 +50,54 @@ public class AdvancedPipelineSettings extends CVPipelineSettings { // the two values that define the line of the Dual Point Offset calibration (think y=mx+b) public double offsetDualLineM = 1; public double offsetDualLineB = 0; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + AdvancedPipelineSettings that = (AdvancedPipelineSettings) o; + return outputShowThresholded == that.outputShowThresholded + && outputShowMultipleTargets == that.outputShowMultipleTargets + && erode == that.erode + && dilate == that.dilate + && contourSpecklePercentage == that.contourSpecklePercentage + && Double.compare(that.offsetDualLineM, offsetDualLineM) == 0 + && Double.compare(that.offsetDualLineB, offsetDualLineB) == 0 + && hsvHue.equals(that.hsvHue) + && hsvSaturation.equals(that.hsvSaturation) + && hsvValue.equals(that.hsvValue) + && contourArea.equals(that.contourArea) + && contourRatio.equals(that.contourRatio) + && contourExtent.equals(that.contourExtent) + && contourSortMode == that.contourSortMode + && contourTargetOffsetPointEdge == that.contourTargetOffsetPointEdge + && contourTargetOrientation == that.contourTargetOrientation + && offsetRobotOffsetMode == that.offsetRobotOffsetMode + && offsetCalibrationPoint.equals(that.offsetCalibrationPoint); + } + + @Override + public int hashCode() { + return Objects.hash( + super.hashCode(), + hsvHue, + hsvSaturation, + hsvValue, + outputShowThresholded, + outputShowMultipleTargets, + erode, + dilate, + contourArea, + contourRatio, + contourExtent, + contourSpecklePercentage, + contourSortMode, + contourTargetOffsetPointEdge, + contourTargetOrientation, + offsetRobotOffsetMode, + offsetCalibrationPoint, + offsetDualLineM, + offsetDualLineB); + } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/CVPipelineSettings.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/CVPipelineSettings.java index bf83fb737..7ae07a0ee 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/CVPipelineSettings.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/CVPipelineSettings.java @@ -3,7 +3,19 @@ package com.chameleonvision.common.vision.pipeline; import com.chameleonvision.common.vision.frame.FrameDivisor; import com.chameleonvision.common.vision.pipe.ImageFlipMode; import com.chameleonvision.common.vision.pipe.ImageRotationMode; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.Objects; +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.WRAPPER_ARRAY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = ColoredShapePipelineSettings.class), + @JsonSubTypes.Type(value = ReflectivePipelineSettings.class), + @JsonSubTypes.Type(value = DriverModePipelineSettings.class) +}) public class CVPipelineSettings { public int pipelineIndex = 0; public PipelineType pipelineType = PipelineType.DriverMode; @@ -17,4 +29,40 @@ public class CVPipelineSettings { public FrameDivisor inputFrameDivisor = FrameDivisor.NONE; public FrameDivisor outputFrameDivisor = FrameDivisor.NONE; public boolean ledMode = false; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CVPipelineSettings that = (CVPipelineSettings) o; + return pipelineIndex == that.pipelineIndex + && Double.compare(that.cameraExposure, cameraExposure) == 0 + && Double.compare(that.cameraBrightness, cameraBrightness) == 0 + && Double.compare(that.cameraGain, cameraGain) == 0 + && cameraVideoModeIndex == that.cameraVideoModeIndex + && ledMode == that.ledMode + && pipelineType == that.pipelineType + && inputImageFlipMode == that.inputImageFlipMode + && inputImageRotationMode == that.inputImageRotationMode + && pipelineNickname.equals(that.pipelineNickname) + && inputFrameDivisor == that.inputFrameDivisor + && outputFrameDivisor == that.outputFrameDivisor; + } + + @Override + public int hashCode() { + return Objects.hash( + pipelineIndex, + pipelineType, + inputImageFlipMode, + inputImageRotationMode, + pipelineNickname, + cameraExposure, + cameraBrightness, + cameraGain, + cameraVideoModeIndex, + inputFrameDivisor, + outputFrameDivisor, + ledMode); + } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ColoredShapePipelineSettings.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ColoredShapePipelineSettings.java index feed77b98..898185894 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ColoredShapePipelineSettings.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ColoredShapePipelineSettings.java @@ -1,7 +1,29 @@ package com.chameleonvision.common.vision.pipeline; import com.chameleonvision.common.vision.opencv.ContourShape; +import com.fasterxml.jackson.annotation.JsonTypeName; +import java.util.Objects; +@JsonTypeName("ColoredShapePipelineSettings") public class ColoredShapePipelineSettings extends AdvancedPipelineSettings { ContourShape desiredShape; + + public ColoredShapePipelineSettings() { + super(); + pipelineType = PipelineType.ColoredShape; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ColoredShapePipelineSettings that = (ColoredShapePipelineSettings) o; + return desiredShape == that.desiredShape; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), desiredShape); + } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/DriverModePipelineSettings.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/DriverModePipelineSettings.java index 458507485..c930f635f 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/DriverModePipelineSettings.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/DriverModePipelineSettings.java @@ -2,8 +2,15 @@ package com.chameleonvision.common.vision.pipeline; import com.chameleonvision.common.util.numbers.DoubleCouple; import com.chameleonvision.common.vision.target.RobotOffsetPointMode; +import com.fasterxml.jackson.annotation.JsonTypeName; +@JsonTypeName("DriverModePipelineSettings") public class DriverModePipelineSettings extends CVPipelineSettings { public RobotOffsetPointMode offsetPointMode = RobotOffsetPointMode.None; public DoubleCouple offsetPoint = new DoubleCouple(); + + public DriverModePipelineSettings() { + super(); + pipelineType = PipelineType.DriverMode; + } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ReflectivePipelineSettings.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ReflectivePipelineSettings.java index 8cd30ac8b..d2ee5ab53 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ReflectivePipelineSettings.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/pipeline/ReflectivePipelineSettings.java @@ -5,8 +5,11 @@ import com.chameleonvision.common.vision.opencv.ContourGroupingMode; import com.chameleonvision.common.vision.opencv.ContourIntersectionDirection; import com.chameleonvision.common.vision.pipe.impl.CornerDetectionPipe; import com.chameleonvision.common.vision.target.TargetModel; +import com.fasterxml.jackson.annotation.JsonTypeName; import edu.wpi.first.wpilibj.geometry.Rotation2d; +import java.util.Objects; +@JsonTypeName("ReflectivePipelineSettings") public class ReflectivePipelineSettings extends AdvancedPipelineSettings { // how many contours to attempt to group (Single, Dual) public ContourGroupingMode contourGroupingMode = ContourGroupingMode.Single; @@ -27,4 +30,46 @@ public class ReflectivePipelineSettings extends AdvancedPipelineSettings { public boolean cornerDetectionExactSideCount = false; public int cornerDetectionSideCount = 4; public double cornerDetectionAccuracyPercentage = 10; + + public ReflectivePipelineSettings() { + super(); + pipelineType = PipelineType.Reflective; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ReflectivePipelineSettings that = (ReflectivePipelineSettings) o; + return solvePNPEnabled == that.solvePNPEnabled + && cornerDetectionUseConvexHulls == that.cornerDetectionUseConvexHulls + && cornerDetectionExactSideCount == that.cornerDetectionExactSideCount + && cornerDetectionSideCount == that.cornerDetectionSideCount + && Double.compare(that.cornerDetectionAccuracyPercentage, cornerDetectionAccuracyPercentage) + == 0 + && contourGroupingMode == that.contourGroupingMode + && contourIntersection == that.contourIntersection + && Objects.equals(cameraCalibration, that.cameraCalibration) + && targetModel.equals(that.targetModel) + && cameraPitch.equals(that.cameraPitch) + && cornerDetectionStrategy == that.cornerDetectionStrategy; + } + + @Override + public int hashCode() { + return Objects.hash( + super.hashCode(), + contourGroupingMode, + contourIntersection, + solvePNPEnabled, + cameraCalibration, + targetModel, + cameraPitch, + cornerDetectionStrategy, + cornerDetectionUseConvexHulls, + cornerDetectionExactSideCount, + cornerDetectionSideCount, + cornerDetectionAccuracyPercentage); + } } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/processes/PipelineManager.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/processes/PipelineManager.java index ed3c311b3..0c05f8123 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/processes/PipelineManager.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/processes/PipelineManager.java @@ -115,10 +115,10 @@ public class PipelineManager { } } - private static final Comparator IndexComparator = + public static final Comparator PipelineSettingsIndexComparator = (o1, o2) -> { - int o1Index = o1.getSettings().pipelineIndex; - int o2Index = o2.getSettings().pipelineIndex; + int o1Index = o1.pipelineIndex; + int o2Index = o2.pipelineIndex; if (o1Index == o2Index) { return 0; @@ -128,13 +128,16 @@ public class PipelineManager { return 1; }; + public static final Comparator PipelineIndexComparator = + (o1, o2) -> PipelineSettingsIndexComparator.compare(o1.getSettings(), o2.getSettings()); + /** * Sorts the pipeline list by index, and reassigns their indexes to match the new order.
*
* I don't like this but I have no other ideas, and it works so ¯\_(ツ)_/¯ */ private void reassignIndexes() { - userPipelines.sort(IndexComparator); + userPipelines.sort(PipelineIndexComparator); for (int i = 0; i < userPipelines.size(); i++) { getPipelineSettings(i).pipelineIndex = i; } diff --git a/chameleon-server/src/main/java/com/chameleonvision/common/vision/target/TargetModel.java b/chameleon-server/src/main/java/com/chameleonvision/common/vision/target/TargetModel.java index 6f48d991e..f61997453 100644 --- a/chameleon-server/src/main/java/com/chameleonvision/common/vision/target/TargetModel.java +++ b/chameleon-server/src/main/java/com/chameleonvision/common/vision/target/TargetModel.java @@ -1,20 +1,28 @@ package com.chameleonvision.common.vision.target; import com.chameleonvision.common.vision.opencv.Releasable; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.opencv.core.MatOfPoint3f; import org.opencv.core.Point3; public class TargetModel implements Releasable { - private final MatOfPoint3f realWorldTargetCoordinates; + @JsonIgnore private final MatOfPoint3f realWorldTargetCoordinates; + @JsonIgnore private final MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f(); + @JsonIgnore private final MatOfPoint3f visualizationBoxTop = new MatOfPoint3f(); - private final MatOfPoint3f visualizationBoxBottom = new MatOfPoint3f(); - private final MatOfPoint3f visualizationBoxTop = new MatOfPoint3f(); + public final List realWorldCoordinatesArray; + public final double boxHeight; public TargetModel(MatOfPoint3f realWorldTargetCoordinates, double boxHeight) { this.realWorldTargetCoordinates = realWorldTargetCoordinates; + this.realWorldCoordinatesArray = realWorldTargetCoordinates.toList(); + this.boxHeight = boxHeight; var bottomList = realWorldTargetCoordinates.toList(); var topList = new ArrayList(); @@ -26,7 +34,10 @@ public class TargetModel implements Releasable { this.visualizationBoxTop.fromList(topList); } - public TargetModel(List points, double boxHeight) { + @JsonCreator + public TargetModel( + @JsonProperty(value = "realWorldCoordinatesArray") List points, + @JsonProperty(value = "boxHeight") double boxHeight) { this(listToMat(points), boxHeight); } @@ -76,6 +87,20 @@ public class TargetModel implements Releasable { return new TargetModel(corners, 4); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TargetModel)) return false; + TargetModel that = (TargetModel) o; + return Double.compare(that.boxHeight, boxHeight) == 0 + && Objects.equals(realWorldCoordinatesArray, that.realWorldCoordinatesArray); + } + + @Override + public int hashCode() { + return Objects.hash(realWorldCoordinatesArray, boxHeight); + } + @Override public void release() { realWorldTargetCoordinates.release(); diff --git a/chameleon-server/src/test/java/com/chameleonvision/common/configuration/ConfigTest.java b/chameleon-server/src/test/java/com/chameleonvision/common/configuration/ConfigTest.java new file mode 100644 index 000000000..2f08d5e7d --- /dev/null +++ b/chameleon-server/src/test/java/com/chameleonvision/common/configuration/ConfigTest.java @@ -0,0 +1,105 @@ +package com.chameleonvision.common.configuration; + +import com.chameleonvision.common.logging.Level; +import com.chameleonvision.common.logging.LogGroup; +import com.chameleonvision.common.logging.Logger; +import com.chameleonvision.common.util.TestUtils; +import com.chameleonvision.common.util.file.JacksonUtils; +import com.chameleonvision.common.vision.pipeline.ColoredShapePipelineSettings; +import com.chameleonvision.common.vision.pipeline.ReflectivePipelineSettings; +import com.chameleonvision.common.vision.target.TargetModel; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.*; + +public class ConfigTest { + + private static final ConfigManager configMgr; + private static final CameraConfiguration cameraConfig = new CameraConfiguration(); + private static final ReflectivePipelineSettings REFLECTIVE_PIPELINE_SETTINGS = + new ReflectivePipelineSettings(); + private static final ColoredShapePipelineSettings COLORED_SHAPE_PIPELINE_SETTINGS = + new ColoredShapePipelineSettings(); + + static { + TestUtils.loadLibraries(); + configMgr = ConfigManager.getInstance(); + } + + @BeforeAll + public static void init() { + TestUtils.loadLibraries(); + Logger.setLevel(LogGroup.General, Level.DE_PEST); + + cameraConfig.name = "TestCamera"; + + REFLECTIVE_PIPELINE_SETTINGS.pipelineNickname = "2019Tape"; + REFLECTIVE_PIPELINE_SETTINGS.targetModel = TargetModel.get2019Target(); + + COLORED_SHAPE_PIPELINE_SETTINGS.pipelineNickname = "2019Cargo"; + COLORED_SHAPE_PIPELINE_SETTINGS.pipelineIndex = 1; + + cameraConfig.addPipelineSetting(REFLECTIVE_PIPELINE_SETTINGS); + cameraConfig.addPipelineSetting(COLORED_SHAPE_PIPELINE_SETTINGS); + } + + @Test + @Order(1) + public void serializeConfig() throws IOException { + TestUtils.loadLibraries(); + JacksonUtils.serializer(Path.of("settings.json"), REFLECTIVE_PIPELINE_SETTINGS); + + Logger.setLevel(LogGroup.General, Level.DE_PEST); + configMgr.getConfig().addCameraConfig(cameraConfig); + configMgr.save(); + + var camConfDir = + new File( + Path.of(ConfigManager.getRootFolder().toString(), "cameras", "TestCamera") + .toAbsolutePath() + .toString()); + Assertions.assertTrue(camConfDir.exists(), "TestCamera config folder not found!"); + + Assertions.assertTrue( + Files.exists(Path.of(ConfigManager.getRootFolder().toString(), "hardwareConfig.json")), + "hardwareConfig.json file not found!"); + Assertions.assertTrue( + Files.exists(Path.of(ConfigManager.getRootFolder().toString(), "networkSettings.json")), + "networkSettings.json file not found!"); + } + + @Test + @Order(2) + public void deserializeConfig() { + configMgr.load(); + + var reflectivePipelineSettings = + configMgr.getConfig().getCameraConfigurations().get("TestCamera").pipelineSettings.get(0); + var coloredShapePipelineSettings = + configMgr.getConfig().getCameraConfigurations().get("TestCamera").pipelineSettings.get(1); + + Assertions.assertEquals(REFLECTIVE_PIPELINE_SETTINGS, reflectivePipelineSettings); + Assertions.assertEquals(COLORED_SHAPE_PIPELINE_SETTINGS, coloredShapePipelineSettings); + + Assertions.assertTrue( + reflectivePipelineSettings instanceof ReflectivePipelineSettings, + "Conig loaded pipeline settings for index 0 not of expected type ReflectivePipelineSettings!"); + Assertions.assertTrue( + coloredShapePipelineSettings instanceof ColoredShapePipelineSettings, + "Conig loaded pipeline settings for index 1 not of expected type ColoredShapePipelineSettings!"); + } + + @AfterAll + public static void cleanup() { + try { + Files.deleteIfExists(Paths.get("settings.json")); + } catch (IOException e) { + e.printStackTrace(); + } + + new File(ConfigManager.getRootFolder().toAbsolutePath().toString()).delete(); + } +}