From 52e181df70c35898b7d2674055e6b91283c2bdd3 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Tue, 10 Sep 2024 19:52:42 -0600 Subject: [PATCH 1/5] Fix exception loop when an exception is thrown from pipeline init --- EOCV-Sim/build.gradle | 13 +++++ .../eocvsim/pipeline/PipelineManager.kt | 10 ++-- .../plugin/loader/PluginClassLoader.kt | 50 ++++++++++++------ .../resources/images/icon/ico_eocvsim.ico | Bin 0 -> 122354 bytes 4 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 EOCV-Sim/src/main/resources/images/icon/ico_eocvsim.ico diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index eed74a75..46f46c2f 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -6,6 +6,8 @@ plugins { id 'org.jetbrains.kotlin.jvm' id 'com.github.johnrengelman.shadow' id 'maven-publish' + + id 'edu.sc.seis.launch4j' version '3.0.6' } apply from: '../build.common.gradle' @@ -32,6 +34,17 @@ test { apply from: '../test-logging.gradle' +launch4j { + mainClassName = 'com.github.serivesmejia.eocvsim.Main' + icon = "${projectDir}/src/main/resources/images/icon/ico_eocvsim.ico" + + outfile = "${project.name}-${standardVersion}.exe" + + copyConfigurable = [] // Prevents copying dependencies + jarTask = shadowJar + +} + dependencies { api project(':Common') api project(':Vision') diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index 3617079a..c1947f8b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -360,9 +360,9 @@ class PipelineManager( ) eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = previousPipelineIndex - changePipeline(currentPipelineIndex) + changePipeline(previousPipelineIndex) - logger.error("Error while initializing requested pipeline, $currentPipelineName", ex) + logger.error("Error while initializing requested pipeline, $currentPipelineName. Falling back to previous one.", ex) } else { updateExceptionTracker(ex) } @@ -463,10 +463,14 @@ class PipelineManager( } fun addInstantiator(instantiatorFor: Class<*>, instantiator: PipelineInstantiator) { - pipelineInstantiators.put(instantiatorFor, instantiator) + pipelineInstantiators[instantiatorFor] = instantiator } fun getInstantiatorFor(clazz: Class<*>): PipelineInstantiator? { + if(pipelineInstantiators.containsKey(clazz)) { + return pipelineInstantiators[clazz] + } + for((instantiatorFor, instantiator) in pipelineInstantiators) { if(ReflectUtil.hasSuperclass(clazz, instantiatorFor)) { return instantiator diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt index f9e18a9a..efc04c46 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt @@ -33,6 +33,7 @@ import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException import java.io.InputStream +import java.net.URL import java.nio.file.FileSystems import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -94,26 +95,26 @@ class PluginClassLoader(private val pluginJar: File, val pluginContext: PluginCo var clazz = loadedClasses[name] if(clazz == null) { - try { - clazz = loadClassStrict(name) - if(resolve) resolveClass(clazz) - } catch(e: Exception) { - var inWhitelist = false - - for(whiteListedPackage in dynamicLoadingPackageWhitelist) { - if(name.contains(whiteListedPackage)) { - inWhitelist = true - break - } - } + var inWhitelist = false - if(!inWhitelist && !pluginContext.hasSuperAccess) { - throw IllegalAccessError("Plugins are not whitelisted to use $name") + for(whiteListedPackage in dynamicLoadingPackageWhitelist) { + if(name.contains(whiteListedPackage)) { + inWhitelist = true + break } + } - // fallback to the system classloader - clazz = Class.forName(name) + if(!inWhitelist && !pluginContext.hasSuperAccess) { + throw IllegalAccessError("Plugins are not whitelisted to use $name") } + + clazz = try { + Class.forName(name) + } catch (e: ClassNotFoundException) { + loadClassStrict(name) + } + + if(resolve) resolveClass(clazz) } return clazz!! @@ -131,6 +132,23 @@ class PluginClassLoader(private val pluginJar: File, val pluginContext: PluginCo return super.getResourceAsStream(name) } + override fun getResource(name: String): URL? { + // Try to find the resource inside the plugin JAR + val entry = zipFile.getEntry(name) + + if (entry != null) { + try { + // Construct a URL for the resource inside the plugin JAR + return URL("jar:file:${pluginJar.absolutePath}!/$name") + } catch (e: Exception) { + e.printStackTrace() + } + } + + // Fallback to the parent classloader if not found in the plugin JAR + return super.getResource(name) + } + override fun toString() = "PluginClassLoader@\"${pluginJar.name}\"" } \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_eocvsim.ico b/EOCV-Sim/src/main/resources/images/icon/ico_eocvsim.ico new file mode 100644 index 0000000000000000000000000000000000000000..22f456d82996e0c0b72fd55af90e1ea473e4b657 GIT binary patch literal 122354 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 XZI576#y^>kZ~RQZz446AY8m?l7%I`r literal 0 HcmV?d00001 From 0d2cb2e4f84239e001323356a4fb5de6dd800bba Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Thu, 12 Sep 2024 02:31:34 -0600 Subject: [PATCH 2/5] Implement virtualreflect onto the codebase and pipeline streaming working --- .../eocvsim/util/event/EventHandler.kt | 1 + .../tuner/TunableFieldPanelOptions.kt | 2 +- .../tuner/element/TunableComboBox.java | 2 +- .../component/tuner/element/TunableSlider.kt | 2 +- .../tuner/element/TunableTextField.java | 2 +- .../eocvsim/pipeline/PipelineManager.kt | 47 +++++++----- .../DefaultPipelineInstantiator.kt | 6 +- .../instantiator/PipelineInstantiator.kt | 5 +- .../processor/ProcessorInstantiator.kt | 6 +- .../eocvsim/pipeline/util/PipelineSnapshot.kt | 33 +++++---- .../eocvsim/tuner/TunableField.java | 16 ++-- .../eocvsim/tuner/TunerManager.java | 58 +++++++++++---- .../eocvsim/tuner/field/BooleanField.java | 23 +++--- .../eocvsim/tuner/field/EnumField.kt | 21 ++++-- .../eocvsim/tuner/field/NumericField.java | 27 +++---- .../eocvsim/tuner/field/StringField.java | 17 +++-- .../eocvsim/tuner/field/cv/PointField.java | 44 +++++++---- .../eocvsim/tuner/field/cv/RectField.kt | 46 +++++++----- .../eocvsim/tuner/field/cv/ScalarField.java | 50 ++++++++----- .../tuner/field/numeric/DoubleField.java | 28 ++++--- .../tuner/field/numeric/FloatField.java | 27 ++++--- .../tuner/field/numeric/IntegerField.java | 25 ++++--- .../tuner/field/numeric/LongField.java | 25 ++++--- .../StreamableOpenCvPipelineInstantiator.kt | 25 +++++++ .../plugin/loader/PluginClassLoader.kt | 9 ++- .../eocvsim/virtualreflect/VirtualField.kt | 44 +++++++++++ .../virtualreflect/VirtualReflectContext.kt | 37 ++++++++++ .../virtualreflect/VirtualReflection.kt | 32 ++++++++ .../virtualreflect/jvm/JvmVirtualField.kt | 73 +++++++++++++++++++ .../jvm/JvmVirtualReflectContext.kt | 68 +++++++++++++++++ .../jvm/JvmVirtualReflection.kt | 47 ++++++++++++ .../eocvsim/virtualreflect/jvm/Label.java | 12 +++ .../pipeline/StreamableOpenCvPipeline.java | 15 ++++ .../deltacv/eocvsim/stream/ImageStreamer.kt | 9 +++ build.gradle | 2 +- 35 files changed, 676 insertions(+), 210 deletions(-) create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipelineInstantiator.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualField.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflectContext.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflection.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualField.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflectContext.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflection.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/Label.java create mode 100644 Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java create mode 100644 Vision/src/main/java/io/github/deltacv/eocvsim/stream/ImageStreamer.kt diff --git a/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt index c07e1f2e..a004239d 100644 --- a/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt +++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt @@ -114,6 +114,7 @@ class EventHandler(val name: String) : Runnable { throw ex } else { logger.error("Error while running \"once\" ${listener.javaClass.name}", ex) + removeOnceListener(listener) } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt index b7d66e08..e0ce3d79 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt @@ -151,7 +151,7 @@ class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, if(i < colorScalar.`val`.size) { val colorVal = colorScalar.`val`[i] fieldPanel.setFieldValue(i, colorVal) - fieldPanel.tunableField.setGuiFieldValue(i, colorVal.toString()) + fieldPanel.tunableField.setFieldValueFromGui(i, colorVal.toString()) } else { break } //keep looping until we write the entire scalar value } colorPickButton.isSelected = false diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java index 7e0ef8ed..58ef4203 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java @@ -53,7 +53,7 @@ private void init() { addItemListener(evt -> eocvSim.onMainUpdate.doOnce(() -> { try { - tunableField.setGuiComboBoxValue(index, Objects.requireNonNull(getSelectedItem()).toString()); + tunableField.setComboBoxValueFromGui(index, Objects.requireNonNull(getSelectedItem()).toString()); } catch (IllegalAccessException ex) { ex.printStackTrace(); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt index 170a1f6d..7401d0da 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt @@ -45,7 +45,7 @@ class TunableSlider(val index: Int, private val changeFieldValue = EventListener { if(inControl) { - tunableField.setGuiFieldValue(index, scaledValue.toString()) + tunableField.setFieldValueFromGui(index, scaledValue.toString()) if (eocvSim.pipelineManager.paused) eocvSim.pipelineManager.setPaused(false) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java index 5088cee1..c6874cbd 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java @@ -123,7 +123,7 @@ public void replace(FilterBypass fb, int offset, int length, String text, Attrib Runnable changeFieldValue = () -> { if ((!hasValidText || !tunableField.isOnlyNumbers() || !getText().trim().equals(""))) { try { - tunableField.setGuiFieldValue(index, getText()); + tunableField.setFieldValueFromGui(index, getText()); } catch (Exception e) { setRedBorder(); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index c1947f8b..6d4f39c7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -40,6 +40,10 @@ import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import com.github.serivesmejia.eocvsim.util.loggerForThis import io.github.deltacv.common.image.MatPoster import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator +import io.github.deltacv.eocvsim.virtualreflect.VirtualField +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflection +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection import kotlinx.coroutines.* import org.firstinspires.ftc.robotcore.external.Telemetry import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl @@ -89,14 +93,21 @@ class PipelineManager( private set @Volatile var currentPipelineData: PipelineData? = null private set - var currentTunerTarget: Any? = null - private set var currentPipelineName = "" private set var currentPipelineIndex = -1 private set var previousPipelineIndex = 0 + var virtualReflect: VirtualReflection = JvmVirtualReflection + set(value) { + eocvSim.tunerManager.setVirtualReflection(value) + field = value + } + + var reflectTarget: Any? = null + private set + @Volatile var previousPipeline: OpenCvPipeline? = null private set @@ -130,7 +141,7 @@ class PipelineManager( var applyLatestSnapshotOnChange = false - val snapshotFieldFilter: (Field) -> Boolean = { + val snapshotFieldFilter: (VirtualField) -> Boolean = { // only snapshot fields managed by the variable tuner // when getTunableFieldOf returns null, it means that // it wasn't able to find a suitable TunableField for @@ -354,15 +365,15 @@ class PipelineManager( updateExceptionTracker() } catch (ex: Exception) { //handling exceptions from pipelines if(!hasInitCurrentPipeline) { - pipelineExceptionTracker.addMessage("Error while initializing requested pipeline, \"$currentPipelineName\". Falling back to previous one.") + pipelineExceptionTracker.addMessage("Error while initializing requested pipeline, \"$currentPipelineName\". Falling back to default.") pipelineExceptionTracker.addMessage( StrUtil.fromException(ex).trim() ) - eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = previousPipelineIndex - changePipeline(previousPipelineIndex) + eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = 0 + changePipeline(0) - logger.error("Error while initializing requested pipeline, $currentPipelineName. Falling back to previous one.", ex) + logger.error("Error while initializing requested pipeline, $currentPipelineName. Falling back to default.", ex) } else { updateExceptionTracker(ex) } @@ -605,16 +616,18 @@ class PipelineManager( previousPipelineIndex = currentPipelineIndex previousPipeline = currentPipeline - currentPipeline = nextPipeline - currentPipelineData = pipelines[index] - currentTelemetry = nextTelemetry - currentPipelineIndex = index - currentPipelineName = currentPipeline!!.javaClass.simpleName - currentTunerTarget = instantiator.variableTunerTargetObject(currentPipeline!!) + currentPipeline = nextPipeline + currentPipelineData = pipelines[index] + currentTelemetry = nextTelemetry + currentPipelineIndex = index + currentPipelineName = currentPipeline!!.javaClass.simpleName + + virtualReflect = instantiator.virtualReflectOf(currentPipeline!!) + reflectTarget = instantiator.variableTunerTarget(currentPipeline!!) currentTelemetry?.update() // clear telemetry - val snap = PipelineSnapshot(currentPipeline!!, snapshotFieldFilter) + val snap = PipelineSnapshot(virtualReflect.contextOf(reflectTarget!!)!!, snapshotFieldFilter) lastInitialSnapshot = if(applyLatestSnapshot) { applyLatestSnapshot() @@ -672,13 +685,13 @@ class PipelineManager( fun captureSnapshot() { if(currentPipeline != null) { - latestSnapshot = PipelineSnapshot(currentPipeline!!, snapshotFieldFilter) + latestSnapshot = PipelineSnapshot(virtualReflect.contextOf(reflectTarget!!)!!, snapshotFieldFilter) } } fun captureStaticSnapshot() { if(currentPipeline != null) { - staticSnapshot = PipelineSnapshot(currentPipeline!!, snapshotFieldFilter) + staticSnapshot = PipelineSnapshot(virtualReflect.contextOf(reflectTarget!!)!!, snapshotFieldFilter) } } @@ -702,7 +715,7 @@ class PipelineManager( fun getIndexOf(pipeline: OpenCvPipeline, source: PipelineSource = PipelineSource.CLASSPATH) = getIndexOf(pipeline::class.java, source) - fun getIndexOf(pipelineClass: Class, source: PipelineSource = PipelineSource.CLASSPATH): Int? { + fun getIndexOf(pipelineClass: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH): Int? { for((i, pipelineData) in pipelines.withIndex()) { if(pipelineData.clazz.name == pipelineClass.name && pipelineData.source == source) { return i diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt index f4b74ec5..3c3be994 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt @@ -23,6 +23,8 @@ package com.github.serivesmejia.eocvsim.pipeline.instantiator +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline @@ -38,6 +40,8 @@ object DefaultPipelineInstantiator : PipelineInstantiator { constructor.newInstance() as OpenCvPipeline } - override fun variableTunerTargetObject(pipeline: OpenCvPipeline) = pipeline + override fun virtualReflectOf(pipeline: OpenCvPipeline) = JvmVirtualReflection + + override fun variableTunerTarget(pipeline: OpenCvPipeline) = pipeline } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt index c3e30c49..65ddc5d5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt @@ -24,6 +24,8 @@ package com.github.serivesmejia.eocvsim.pipeline.instantiator import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflection import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline @@ -31,6 +33,7 @@ interface PipelineInstantiator { fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline - fun variableTunerTargetObject(pipeline: OpenCvPipeline): Any + fun virtualReflectOf(pipeline: OpenCvPipeline): VirtualReflection + fun variableTunerTarget(pipeline: OpenCvPipeline): Any? } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt index d42134c8..1f6d6f6a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt @@ -26,6 +26,7 @@ package com.github.serivesmejia.eocvsim.pipeline.instantiator.processor import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.pipeline.instantiator.PipelineInstantiator import com.github.serivesmejia.eocvsim.util.ReflectUtil +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection import org.firstinspires.ftc.robotcore.external.Telemetry import org.firstinspires.ftc.vision.VisionProcessor import org.openftc.easyopencv.OpenCvPipeline @@ -48,7 +49,8 @@ object ProcessorInstantiator : PipelineInstantiator { return ProcessorPipeline(processor) } - override fun variableTunerTargetObject(pipeline: OpenCvPipeline): VisionProcessor = - (pipeline as ProcessorPipeline).processor + override fun virtualReflectOf(pipeline: OpenCvPipeline) = JvmVirtualReflection + + override fun variableTunerTarget(pipeline: OpenCvPipeline) = (pipeline as ProcessorPipeline).processor } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt index efe3d85a..bc05c6cb 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt @@ -24,30 +24,33 @@ package com.github.serivesmejia.eocvsim.pipeline.util import com.github.serivesmejia.eocvsim.util.loggerForThis +import io.github.deltacv.eocvsim.virtualreflect.VirtualField +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection import org.openftc.easyopencv.OpenCvPipeline -import java.lang.reflect.Field -import java.lang.reflect.Modifier import java.util.* -class PipelineSnapshot(holdingPipeline: OpenCvPipeline, val filter: ((Field) -> Boolean)? = null) { +class PipelineSnapshot(val virtualReflectContext: VirtualReflectContext, filter: ((VirtualField) -> Boolean)? = null) { val logger by loggerForThis() - val holdingPipelineName = holdingPipeline::class.simpleName + val holdingPipelineName = virtualReflectContext.simpleName - val pipelineFieldValues: Map - val pipelineClass = holdingPipeline::class.java + val pipelineClass get() = (virtualReflectContext as JvmVirtualReflectContext).clazz + + val pipelineFieldValues: Map init { - val fieldValues = mutableMapOf() + val fieldValues = mutableMapOf() - for(field in pipelineClass.declaredFields) { - if(Modifier.isFinal(field.modifiers) || !Modifier.isPublic(field.modifiers)) + for(field in virtualReflectContext.fields) { + if(field.isFinal || field.isFinal) continue if(filter?.invoke(field) == false) continue - fieldValues[field] = field.get(holdingPipeline) + fieldValues[field] = field.get() } pipelineFieldValues = fieldValues.toMap() @@ -60,7 +63,7 @@ class PipelineSnapshot(holdingPipeline: OpenCvPipeline, val filter: ((Field) -> if(pipelineClass.name != otherPipeline::class.java.name) return val changedList = if(lastInitialPipelineSnapshot != null) - getChangedFieldsComparedTo(PipelineSnapshot(otherPipeline), lastInitialPipelineSnapshot) + getChangedFieldsComparedTo(PipelineSnapshot(JvmVirtualReflection.contextOf(otherPipeline)), lastInitialPipelineSnapshot) else Collections.emptyList() fieldValuesLoop@ @@ -76,7 +79,7 @@ class PipelineSnapshot(holdingPipeline: OpenCvPipeline, val filter: ((Field) -> } try { - field.set(otherPipeline, value) + field.set(value) } catch(e: Exception) { logger.trace( "Failed to set field ${field.name} from snapshot of ${pipelineClass.name}. " + @@ -96,7 +99,7 @@ class PipelineSnapshot(holdingPipeline: OpenCvPipeline, val filter: ((Field) -> } } - fun getField(name: String): Pair? { + fun getField(name: String): Pair? { for((field, value) in pipelineFieldValues) { if(field.name == name) { return Pair(field, value) @@ -109,11 +112,11 @@ class PipelineSnapshot(holdingPipeline: OpenCvPipeline, val filter: ((Field) -> private fun getChangedFieldsComparedTo( pipelineSnapshotA: PipelineSnapshot, pipelineSnapshotB: PipelineSnapshot - ): List = pipelineSnapshotA.run { + ): List = pipelineSnapshotA.run { if(holdingPipelineName != pipelineSnapshotB.holdingPipelineName && pipelineClass != pipelineSnapshotB.pipelineClass) return Collections.emptyList() - val changedList = mutableListOf() + val changedList = mutableListOf() for((field, value) in pipelineFieldValues) { val (otherField, otherValue) = pipelineSnapshotB.getField(field.name) ?: continue diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java index c1c707d2..ee0c0bf0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java @@ -27,13 +27,14 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanelConfig; import com.github.serivesmejia.eocvsim.util.event.EventHandler; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; public abstract class TunableField { - protected Field reflectionField; + protected VirtualField reflectionField; protected TunableFieldPanel fieldPanel; protected Object target; @@ -49,16 +50,16 @@ public abstract class TunableField { private TunableFieldPanel.Mode recommendedMode = null; - public TunableField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + public TunableField(Object target, VirtualField reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { this.reflectionField = reflectionField; this.target = target; this.allowMode = allowMode; this.eocvSim = eocvSim; - initialFieldValue = reflectionField.get(target); + initialFieldValue = reflectionField.get(); } - public TunableField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + public TunableField(Object target, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { this(target, reflectionField, eocvSim, AllowMode.TEXT); } @@ -70,14 +71,15 @@ public TunableField(Object target, Field reflectionField, EOCVSim eocvSim) throw public void setPipelineFieldValue(T newValue) throws IllegalAccessException { if (hasChanged()) { //execute if value is not the same to save resources - reflectionField.set(target, newValue); + reflectionField.set(newValue); onValueChange.run(); } } - public abstract void setGuiFieldValue(int index, String newValue) throws IllegalAccessException; + public abstract void setFieldValue(int index, Object newValue) throws IllegalAccessException; + public abstract void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException; - public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { } + public void setComboBoxValueFromGui(int index, String newValue) throws IllegalAccessException { } public final void setTunableFieldPanel(TunableFieldPanel fieldPanel) { this.fieldPanel = fieldPanel; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java index 089fbb2b..8995031a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java @@ -27,6 +27,11 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.exception.CancelTunableFieldAddingException; import com.github.serivesmejia.eocvsim.util.ReflectUtil; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext; +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflection; +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection; +import org.jetbrains.annotations.Nullable; import org.openftc.easyopencv.OpenCvPipeline; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,11 +40,12 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; @SuppressWarnings("rawtypes") public class TunerManager { + Logger logger = LoggerFactory.getLogger(getClass()); + private final EOCVSim eocvSim; private final List fields = new ArrayList<>(); @@ -49,9 +55,9 @@ public class TunerManager { private static HashMap>> tunableFieldsTypes = null; private static HashMap>, Class> tunableFieldAcceptors = null; - private boolean firstInit = true; + private VirtualReflection reflect = JvmVirtualReflection.INSTANCE; - Logger logger = LoggerFactory.getLogger(getClass()); + private boolean firstInit = true; public TunerManager(EOCVSim eocvSim) { this.eocvSim = eocvSim; @@ -84,14 +90,14 @@ public void init() { } if (eocvSim.pipelineManager.getCurrentPipeline() != null) { - addFieldsFrom(eocvSim.pipelineManager.getCurrentTunerTarget()); + addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline()); eocvSim.visualizer.updateTunerFields(createTunableFieldPanels()); for(TunableField field : fields.toArray(new TunableField[0])) { try { field.init(); } catch(CancelTunableFieldAddingException e) { - logger.trace("Field " + field.getFieldName() + " was removed due to \"" + e.getMessage() + "\""); + logger.info("Field " + field.getFieldName() + " was removed due to \"" + e.getMessage() + "\""); fields.remove(field); } } @@ -122,9 +128,9 @@ public void reset() { init(); } - public Class getTunableFieldOf(Field field) { + public Class getTunableFieldOf(VirtualField field) { //we only accept non-final fields - if (Modifier.isFinal(field.getModifiers())) return null; + if (field.isFinal()) return null; Class type = field.getType(); if (field.getType().isPrimitive()) { //wrap to java object equivalent if field type is primitive @@ -145,12 +151,15 @@ public Class getTunableFieldOf(Field field) { return tunableFieldClass; } - public void addFieldsFrom(Object target) { - if (target == null) return; + public void addFieldsFrom(OpenCvPipeline pipeline) { + if (pipeline == null) return; + + VirtualReflectContext reflectContext = reflect.contextOf(pipeline); + if(reflectContext == null) return; - Field[] fields = target.getClass().getFields(); + VirtualField[] fields = reflect.contextOf(pipeline).getFields(); - for (Field field : fields) { + for (VirtualField field : fields) { Class tunableFieldClass = getTunableFieldOf(field); // we can't handle this type @@ -160,12 +169,14 @@ public void addFieldsFrom(Object target) { //now, lets do some more reflection to instantiate this TunableField //and add it to the list... try { - Constructor constructor = tunableFieldClass.getConstructor(Object.class, Field.class, EOCVSim.class); - this.fields.add(constructor.newInstance(target, field, eocvSim)); + Constructor constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, VirtualField.class, EOCVSim.class); + this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); } catch(InvocationTargetException e) { if(e.getCause() instanceof CancelTunableFieldAddingException) { String message = e.getCause().getMessage(); logger.info("Field " + field.getName() + " wasn't added due to \"" + message + "\""); + } else { + logger.error("Reflection error while processing field: " + field.getName(), e.getCause()); } } catch (Exception ex) { //oops rip @@ -175,6 +186,25 @@ public void addFieldsFrom(Object target) { } } + @Nullable public TunableField getTunableFieldWithLabel(String label) { + TunableField labeledField = null; + + for(TunableField field : fields) { + String fieldLabel = field.reflectionField.getLabel(); + + if(fieldLabel != null && fieldLabel.equals(label)) { + labeledField = field; + break; + } + } + + return labeledField; + } + + public void setVirtualReflection(VirtualReflection reflect) { + this.reflect = reflect; + } + public void reevaluateConfigs() { for(TunableField field : fields) { field.fieldPanel.panelOptions.reevaluateConfig(); @@ -191,4 +221,4 @@ private List createTunableFieldPanels() { return panels; } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java index 8e6b3380..e1bd7809 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java @@ -26,10 +26,9 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; -import java.lang.reflect.Field; - @RegisterTunableField public class BooleanField extends TunableField { @@ -38,8 +37,8 @@ public class BooleanField extends TunableField { boolean lastVal; volatile boolean hasChanged = false; - public BooleanField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.TEXT); + public BooleanField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.TEXT); setGuiFieldAmount(0); setGuiComboBoxAmount(1); @@ -52,7 +51,6 @@ public void init() {} @Override public void update() { - hasChanged = value != lastVal; if (hasChanged) { //update values in GUI if they changed since last check @@ -60,7 +58,6 @@ public void update() { } lastVal = value; - } @Override @@ -69,14 +66,20 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - setGuiComboBoxValue(index, newValue); + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { + value = (boolean) newValue; + setPipelineFieldValue((boolean)newValue); + } + + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { + setComboBoxValueFromGui(index, newValue); } @Override - public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { + public void setComboBoxValueFromGui(int index, String newValue) throws IllegalAccessException { value = Boolean.parseBoolean(newValue); - setPipelineFieldValue(value); + setFieldValue(index, value); lastVal = value; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt index eb592693..c25a5a2c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt @@ -4,12 +4,13 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.tuner.TunableField import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField -import java.lang.reflect.Field +import io.github.deltacv.eocvsim.virtualreflect.VirtualField +import org.openftc.easyopencv.OpenCvPipeline @RegisterTunableField -class EnumField(target: Any, - reflectionField: Field, - eocvSim: EOCVSim) : TunableField>(target, reflectionField, eocvSim, AllowMode.TEXT) { +class EnumField(private val instance: OpenCvPipeline, + reflectionField: VirtualField, + eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) { val values = reflectionField.type.enumConstants @@ -39,11 +40,15 @@ class EnumField(target: Any, fieldPanel.setComboBoxSelection(0, currentValue) } - override fun setGuiComboBoxValue(index: Int, newValue: String) = setGuiFieldValue(index, newValue) + override fun setFieldValue(index: Int, newValue: Any) { + reflectionField.set(newValue) + } + + override fun setComboBoxValueFromGui(index: Int, newValue: String) = setFieldValueFromGui(index, newValue) - override fun setGuiFieldValue(index: Int, newValue: String) { + override fun setFieldValueFromGui(index: Int, newValue: String) { currentValue = java.lang.Enum.valueOf(initialValue::class.java, newValue) - reflectionField.set(target, currentValue) + setFieldValue(index, currentValue) } override fun getValue() = currentValue @@ -54,7 +59,7 @@ class EnumField(target: Any, return values } - override fun hasChanged() = reflectionField.get(target) != beforeValue + override fun hasChanged() = reflectionField.get() != beforeValue class EnumFieldAcceptor : TunableFieldAcceptor { override fun accept(clazz: Class<*>) = clazz.isEnum diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java index 2b5af34e..26f7405b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java @@ -26,18 +26,20 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.TunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; -public class NumericField extends TunableField { +abstract public class NumericField extends TunableField { protected T value; + protected T beforeValue; protected volatile boolean hasChanged = false; - public NumericField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { - super(target, reflectionField, eocvSim, allowMode); + public NumericField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, allowMode); } @Override @@ -46,14 +48,10 @@ public void init() { } @Override + @SuppressWarnings("unchecked") public void update() { if (value == null) return; - - try { - value = (T) reflectionField.get(target); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } + value = (T) reflectionField.get(); hasChanged = hasChanged(); @@ -67,10 +65,6 @@ public void updateGuiFieldValues() { fieldPanel.setFieldValue(0, value); } - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - } - @Override public T getValue() { return value; @@ -83,7 +77,10 @@ public Object getGuiFieldValue(int index) { @Override public boolean hasChanged() { - return false; + boolean hasChanged = value != beforeValue; + beforeValue = value; + return hasChanged; } -} + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java index d80895b1..ee78c291 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -40,8 +41,8 @@ public class StringField extends TunableField { volatile boolean hasChanged = false; - public StringField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.TEXT); + public StringField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.TEXT); if(initialFieldValue != null) { value = (String) initialFieldValue; @@ -72,14 +73,16 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { + setPipelineFieldValue((String)newValue); + } + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { value = newValue; - - setPipelineFieldValue(value); + setFieldValue(index, value); lastVal = value; - } @Override @@ -98,4 +101,4 @@ public boolean hasChanged() { return hasChanged; } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java index 7b7a6734..41c399a2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.opencv.core.Point; import org.openftc.easyopencv.OpenCvPipeline; @@ -36,12 +37,13 @@ public class PointField extends TunableField { Point point; - double[] lastXY = {0, 0}; + double lastX = 0; + double lastY = 0; volatile boolean hasChanged = false; - public PointField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public PointField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue != null) { Point p = (Point) initialFieldValue; @@ -51,23 +53,23 @@ public PointField(Object target, Field reflectionField, EOCVSim eocvSim) throws } setGuiFieldAmount(2); - } @Override - public void init() { } + public void init() { + reflectionField.set(point); + } @Override public void update() { - - hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; + hasChanged = point.x != lastX || point.y != lastY; if (hasChanged) { //update values in GUI if they changed since last check updateGuiFieldValues(); } - lastXY = new double[]{point.x, point.y}; - + lastX = point.x; + lastY = point.y; } @Override @@ -77,23 +79,33 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { try { - double value = Double.parseDouble(newValue); + double value = 0; + if(newValue instanceof String) { + value = Double.parseDouble((String)newValue); + } else { + value = (double)newValue; + } + if (index == 0) { point.x = value; } else { point.y = value; } - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } catch (Exception ex) { + throw new IllegalArgumentException("Parameter should be a valid number", ex); } setPipelineFieldValue(point); - lastXY = new double[]{point.x, point.y}; + lastX = point.x; + lastY = point.y; + } + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { + setFieldValue(index, point); } @Override @@ -108,7 +120,7 @@ public Object getGuiFieldValue(int index) { @Override public boolean hasChanged() { - hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; + hasChanged = point.x != lastX || point.y != lastY; return hasChanged; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt index b83cf1de..7bd0b937 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt @@ -26,13 +26,14 @@ package com.github.serivesmejia.eocvsim.tuner.field.cv import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.tuner.TunableField import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField +import io.github.deltacv.eocvsim.virtualreflect.VirtualField import org.opencv.core.Rect import org.openftc.easyopencv.OpenCvPipeline import java.lang.reflect.Field @RegisterTunableField -class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : - TunableField(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { +class RectField(instance: OpenCvPipeline, reflectionField: VirtualField, eocvSim: EOCVSim) : + TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { private var rect = arrayOf(0.0, 0.0, 0.0, 0.0) private var lastRect = arrayOf(0.0, 0.0, 0.0, 0.0) @@ -40,7 +41,7 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : @Volatile private var hasChanged = false private var initialRect = if(initialFieldValue != null) - initialFieldValue as Rect + (initialFieldValue as Rect).clone() else Rect(0, 0, 0, 0) init { @@ -48,15 +49,17 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : rect[1] = initialRect.y.toDouble() rect[2] = initialRect.width.toDouble() rect[3] = initialRect.height.toDouble() - + guiFieldAmount = 4 } - override fun init() {} + override fun init() { + reflectionField.set(initialRect) + } override fun update() { if(hasChanged()){ - initialRect = reflectionField.get(target) as Rect + initialRect = reflectionField.get() as Rect rect[0] = initialRect.x.toDouble() rect[1] = initialRect.y.toDouble() @@ -67,21 +70,16 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : } } - override fun updateGuiFieldValues() { - for((i, value) in rect.withIndex()) { - fieldPanel.setFieldValue(i, value) - } - } - - override fun setGuiFieldValue(index: Int, newValue: String) { + override fun setFieldValue(index: Int, newValue: Any) { try { - val value = newValue.toDouble() - rect[index] = value - } catch (ex: NumberFormatException) { - throw IllegalArgumentException("Parameter should be a valid numeric String") + rect[index] = if(newValue is String) + newValue.toDouble() + else (newValue as Number).toDouble() + } catch (e: Exception) { + throw IllegalArgumentException("Parameter should be a valid numeric value", e) } - initialRect.set(rect.toDoubleArray()); + initialRect.set(rect.toDoubleArray()) setPipelineFieldValue(initialRect) lastRect[0] = initialRect.x.toDouble() @@ -90,13 +88,23 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : lastRect[3] = initialRect.height.toDouble() } + override fun updateGuiFieldValues() { + for((i, value) in rect.withIndex()) { + fieldPanel.setFieldValue(i, value) + } + } + + override fun setFieldValueFromGui(index: Int, newValue: String) { + setFieldValue(index, newValue) + } + override fun getValue(): Rect = Rect(rect.toDoubleArray()) override fun getGuiFieldValue(index: Int): Any = rect[index] override fun hasChanged(): Boolean { hasChanged = rect[0] != lastRect[0] || rect[1] != lastRect[1] - || rect[2] != lastRect[2] || rect[3] != lastRect[3] + || rect[2] != lastRect[2] || rect[3] != lastRect[3] return hasChanged } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java index fa79db64..1c380131 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java @@ -27,10 +27,10 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.opencv.core.Scalar; import org.openftc.easyopencv.OpenCvPipeline; -import java.lang.reflect.Field; import java.util.Arrays; @RegisterTunableField @@ -39,34 +39,33 @@ public class ScalarField extends TunableField { int scalarSize; Scalar scalar; - double[] lastVal = {}; + double[] lastVal = {0, 0, 0, 0}; volatile boolean hasChanged = false; - public ScalarField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public ScalarField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue == null) { scalar = new Scalar(0, 0, 0); } else { - scalar = (Scalar) initialFieldValue; + scalar = ((Scalar) initialFieldValue).clone(); } + scalarSize = scalar.val.length; - setGuiFieldAmount(scalarSize); + setGuiFieldAmount(4); setRecommendedPanelMode(TunableFieldPanel.Mode.SLIDERS); } @Override - public void init() { } + public void init() { + reflectionField.set(scalar); + } @Override public void update() { - try { - scalar = (Scalar) reflectionField.get(target); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } + scalar = (Scalar) reflectionField.get(); hasChanged = !Arrays.equals(scalar.val, lastVal); @@ -74,7 +73,10 @@ public void update() { updateGuiFieldValues(); } - lastVal = scalar.val.clone(); + lastVal[0] = scalar.val[0]; + lastVal[1] = scalar.val[1]; + lastVal[2] = scalar.val[2]; + lastVal[3] = scalar.val[3]; } @Override @@ -85,16 +87,28 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { try { - scalar.val[index] = Double.parseDouble(newValue); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); + if(newValue instanceof String) { + scalar.val[index] = Double.parseDouble((String) newValue); + } else { + scalar.val[index] = (double)newValue; + } + } catch (Exception ex) { + throw new IllegalArgumentException("Parameter should be a valid number", ex); } setPipelineFieldValue(scalar); - lastVal = scalar.val.clone(); + lastVal[0] = scalar.val[0]; + lastVal[1] = scalar.val[1]; + lastVal[2] = scalar.val[2]; + lastVal[3] = scalar.val[3]; + } + + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { + setFieldValue(index, newValue); } @Override diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java index 572bedbd..d7af5eda 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,34 +34,31 @@ @RegisterTunableField public class DoubleField extends NumericField { - private double beforeValue; - - public DoubleField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public DoubleField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (double) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = Double.valueOf(newValue); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; - } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).doubleValue(); + } else { + this.value = (double)value; + } + setPipelineFieldValue(this.value); } - -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java index 321e01dd..eb6928b7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,33 +34,31 @@ @RegisterTunableField public class FloatField extends NumericField { - protected float beforeValue; - - public FloatField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public FloatField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (float) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = Float.parseFloat(newValue); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; - } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).floatValue(); + } else { + this.value = (float)value; + } + setPipelineFieldValue(this.value); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java index 08ff9bf2..a4cd5b96 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,31 +34,31 @@ @RegisterTunableField public class IntegerField extends NumericField { - protected int beforeValue; - - public IntegerField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + public IntegerField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (int) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = (int) Math.round(Double.parseDouble(newValue)); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).intValue(); + } else { + this.value = (int)value; + } + setPipelineFieldValue(this.value); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java index 6645a505..839a7b42 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,31 +34,31 @@ @RegisterTunableField public class LongField extends NumericField { - private long beforeValue; - - public LongField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + public LongField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (long) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = Math.round(Double.parseDouble(newValue)); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).longValue(); + } else { + this.value = (long)value; + } + setPipelineFieldValue(this.value); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipelineInstantiator.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipelineInstantiator.kt new file mode 100644 index 00000000..e5cffaa5 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipelineInstantiator.kt @@ -0,0 +1,25 @@ +package io.github.deltacv.eocvsim.pipeline + +import com.github.serivesmejia.eocvsim.pipeline.instantiator.DefaultPipelineInstantiator +import com.github.serivesmejia.eocvsim.pipeline.instantiator.PipelineInstantiator +import io.github.deltacv.eocvsim.stream.ImageStreamer +import io.github.deltacv.eocvsim.virtualreflect.jvm.JvmVirtualReflection +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline + +class StreamableOpenCvPipelineInstantiator( + val imageStreamer: ImageStreamer +) : PipelineInstantiator { + + override fun instantiate(clazz: Class<*>, telemetry: Telemetry) = + DefaultPipelineInstantiator.instantiate(clazz, telemetry).apply { + if(this is StreamableOpenCvPipeline) { + this.streamer = imageStreamer + } + } + + override fun virtualReflectOf(pipeline: OpenCvPipeline) = JvmVirtualReflection + + override fun variableTunerTarget(pipeline: OpenCvPipeline) = pipeline + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt index efc04c46..daf81e8a 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginClassLoader.kt @@ -45,7 +45,12 @@ import java.util.zip.ZipFile */ class PluginClassLoader(private val pluginJar: File, val pluginContext: PluginContext) : ClassLoader() { - private val zipFile = ZipFile(pluginJar) + private val zipFile = try { + ZipFile(pluginJar) + } catch(e: Exception) { + throw IOException("Failed to open plugin JAR file", e) + } + private val loadedClasses = mutableMapOf>() init { @@ -88,7 +93,7 @@ class PluginClassLoader(private val pluginJar: File, val pluginContext: PluginCo } } - return loadClass(zipFile.getEntry(name.replace('.', '/') + ".class")) + return loadClass(zipFile.getEntry(name.replace('.', '/') + ".class") ?: throw ClassNotFoundException(name)) } override fun loadClass(name: String, resolve: Boolean): Class<*> { diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualField.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualField.kt new file mode 100644 index 00000000..71efc62c --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualField.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.virtualreflect + +interface VirtualField { + + val name: String + val type: Class<*> + + val isFinal: Boolean + + val visibility: Visibility + + val label: String? + + fun get(): Any? + fun set(value: Any?) + +} + +enum class Visibility { + PUBLIC, PROTECTED, PRIVATE, PACKAGE_PRIVATE +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflectContext.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflectContext.kt new file mode 100644 index 00000000..7583c027 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflectContext.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.virtualreflect + +interface VirtualReflectContext { + + val name: String + val simpleName: String + + val fields: Array + + fun getField(name: String): VirtualField? + + fun getLabeledField(label: String): VirtualField? + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflection.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflection.kt new file mode 100644 index 00000000..27a68b9e --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/VirtualReflection.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.virtualreflect + +interface VirtualReflection { + + fun contextOf(c: Class<*>): VirtualReflectContext? + + fun contextOf(value: Any): VirtualReflectContext? + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualField.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualField.kt new file mode 100644 index 00000000..aea7811c --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualField.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.virtualreflect.jvm + +import io.github.deltacv.eocvsim.virtualreflect.VirtualField +import io.github.deltacv.eocvsim.virtualreflect.Visibility +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +class JvmVirtualField( + val instance: Any?, + val field: Field +) : VirtualField { + + override val name: String = field.name + override val type: Class<*> = field.type + + override val isFinal get() = Modifier.isFinal(field.modifiers) + + override val visibility get() = when { + Modifier.isPublic(field.modifiers) -> Visibility.PUBLIC + Modifier.isProtected(field.modifiers) -> Visibility.PROTECTED + Modifier.isPrivate(field.modifiers) -> Visibility.PRIVATE + else -> Visibility.PACKAGE_PRIVATE + } + + private var hasLabel: Boolean? = null + private var cachedLabel: String? = null + + override val label: String? + get() = if(hasLabel == null) { + val labelAnnotations = this.field.getDeclaredAnnotationsByType(Label::class.java) + if(labelAnnotations.isEmpty()) { + hasLabel = false + null + } else { + hasLabel = true + cachedLabel = labelAnnotations[0].name + + cachedLabel + } + } else if(hasLabel == true) { + cachedLabel + } else null + + override fun get(): Any? = field.get(instance) + + override fun set(value: Any?) { + field.set(instance, value) + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflectContext.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflectContext.kt new file mode 100644 index 00000000..fb2c6f9b --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflectContext.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.virtualreflect.jvm + +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.VirtualField +import java.lang.reflect.Field + +class JvmVirtualReflectContext( + val instance: Any? = null, + val clazz: Class<*> +) : VirtualReflectContext { + + override val name: String = clazz.name + override val simpleName: String = clazz.simpleName + + private val cachedVirtualFields = mutableMapOf() + + override val fields: Array = clazz.fields.map { virtualFieldFor(it) }.toTypedArray() + + override fun getField(name: String): VirtualField? { + val field = clazz.getField(name) ?: return null + return virtualFieldFor(field) + } + + override fun getLabeledField(label: String): VirtualField? { + var labeledField: VirtualField? = null + + for(field in fields) { + if(field.label == label) { + labeledField = field + break + } + } + + return labeledField + } + + private fun virtualFieldFor(field: Field): JvmVirtualField { + if(!cachedVirtualFields.containsKey(field)) { + cachedVirtualFields[field] = JvmVirtualField(instance, field) + } + + return cachedVirtualFields[field]!! + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflection.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflection.kt new file mode 100644 index 00000000..1053279e --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/JvmVirtualReflection.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package io.github.deltacv.eocvsim.virtualreflect.jvm + +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflectContext +import io.github.deltacv.eocvsim.virtualreflect.VirtualReflection +import java.lang.ref.WeakReference +import java.util.* + +object JvmVirtualReflection : VirtualReflection { + + private val cache = WeakHashMap>() + + override fun contextOf(c: Class<*>) = cacheContextOf(null, c) + + override fun contextOf(value: Any) = cacheContextOf(value, value::class.java) + + private fun cacheContextOf(value: Any?, clazz: Class<*>): VirtualReflectContext { + if(!cache.containsKey(value) || cache[value]?.get() == null) { + cache[value] = WeakReference(JvmVirtualReflectContext(value, clazz)) + } + + return cache[value]!!.get()!! + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/Label.java b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/Label.java new file mode 100644 index 00000000..0b1af0f2 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/virtualreflect/jvm/Label.java @@ -0,0 +1,12 @@ +package io.github.deltacv.eocvsim.virtualreflect.jvm; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Label { + String name(); +} diff --git a/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java b/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java new file mode 100644 index 00000000..b98b24bb --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java @@ -0,0 +1,15 @@ +package io.github.deltacv.eocvsim.pipeline; + +import io.github.deltacv.eocvsim.stream.ImageStreamer; +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvPipeline; + +public abstract class StreamableOpenCvPipeline extends OpenCvPipeline { + + protected ImageStreamer streamer = null; + + public void streamFrame(int id, Mat image, Integer cvtCode) { + streamer.sendFrame(id, image, cvtCode); + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/eocvsim/stream/ImageStreamer.kt b/Vision/src/main/java/io/github/deltacv/eocvsim/stream/ImageStreamer.kt new file mode 100644 index 00000000..57269c68 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/eocvsim/stream/ImageStreamer.kt @@ -0,0 +1,9 @@ +package io.github.deltacv.eocvsim.stream + +import org.opencv.core.Mat + +interface ImageStreamer { + + fun sendFrame(id: Int, image: Mat, cvtCode: Int? = null) + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 472c1f3d..6192f2fa 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ plugins { allprojects { group 'com.github.deltacv' - version '3.6.0' + version '4.0.0' apply plugin: 'java' From cc66232581ef57efdfc2eacc345359519f1ae10f Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Wed, 18 Sep 2024 16:29:13 -0600 Subject: [PATCH 3/5] Remove jimfs and use standard zip file system for sandbox --- EOCV-Sim/build.gradle | 2 - .../github/serivesmejia/eocvsim/EOCVSim.kt | 4 +- .../com/github/serivesmejia/eocvsim/Main.kt | 2 + .../component/tuner/TunableFieldPanel.java | 1 + .../eocvsim/pipeline/PipelineManager.kt | 12 +-- .../eocvsim/util/JavaProcess.java | 8 +- .../eocvsim/plugin/loader/PluginLoader.kt | 2 +- .../eocvsim/plugin/loader/PluginManager.kt | 13 +++ .../eocvsim/sandbox/nio/JimfsWatcher.kt | 97 ------------------- .../eocvsim/sandbox/nio/SandboxFileSystem.kt | 67 +++---------- .../pipeline/StreamableOpenCvPipeline.java | 2 +- 11 files changed, 45 insertions(+), 165 deletions(-) delete mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/JimfsWatcher.kt diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 46f46c2f..aea2c540 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -79,8 +79,6 @@ dependencies { testImplementation "io.kotest:kotest-assertions-core:$kotest_version" implementation 'com.moandjiezana.toml:toml4j:0.7.2' - implementation 'com.google.jimfs:jimfs:1.3.0' - implementation 'org.ow2.asm:asm:9.7' } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index f526b15f..b688abb9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -23,6 +23,7 @@ package com.github.serivesmejia.eocvsim +import com.github.serivesmejia.eocvsim.EOCVSim.Parameters import com.github.serivesmejia.eocvsim.config.Config import com.github.serivesmejia.eocvsim.config.ConfigManager import com.github.serivesmejia.eocvsim.gui.DialogFactory @@ -32,7 +33,6 @@ import com.github.serivesmejia.eocvsim.input.InputSourceManager import com.github.serivesmejia.eocvsim.output.VideoRecordingSession import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.pipeline.PipelineSource -import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import com.github.serivesmejia.eocvsim.tuner.TunerManager import com.github.serivesmejia.eocvsim.util.ClasspathScan import com.github.serivesmejia.eocvsim.util.FileFilters @@ -41,13 +41,13 @@ import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException import com.github.serivesmejia.eocvsim.util.exception.handling.CrashReport import com.github.serivesmejia.eocvsim.util.exception.handling.EOCVSimUncaughtExceptionHandler -import com.github.serivesmejia.eocvsim.util.extension.plus import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder import com.github.serivesmejia.eocvsim.util.loggerFor import com.github.serivesmejia.eocvsim.workspace.WorkspaceManager import com.qualcomm.robotcore.eventloop.opmode.OpMode import com.qualcomm.robotcore.eventloop.opmode.OpModePipelineHandler +import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import io.github.deltacv.common.util.ParsedVersion import io.github.deltacv.eocvsim.plugin.loader.PluginManager import io.github.deltacv.vision.external.PipelineRenderHook diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index 47704082..bc494082 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -17,6 +17,8 @@ var currentMainThread: Thread = jvmMainThread * @see CommandLine */ fun main(args: Array) { + System.setProperty("sun.java2d.d3d", "false") + val result = CommandLine( EOCVSimCommandInterface() ).setCaseInsensitiveEnumValuesAllowed(true).execute(*args) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java index 37b70b63..523e2e87 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java @@ -33,6 +33,7 @@ import javax.swing.border.SoftBevelBorder; import java.awt.*; +@SuppressWarnings("Unchecked") public class TunableFieldPanel extends JPanel { public final TunableField tunableField; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index 6d4f39c7..e3b3757f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -269,6 +269,12 @@ class PipelineManager( val telemetry = currentTelemetry onUpdate.run() + for(activeContext in activePipelineContexts.toTypedArray()) { + if(!activeContext.isActive) { + activePipelineContexts.remove(activeContext) + } + } + if(activePipelineContexts.size > MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS) { throw MaxActiveContextsException("Current amount of active pipeline coroutine contexts (${activePipelineContexts.size}) is more than the maximum allowed. This generally means that there are multiple pipelines stuck in processFrame() running in the background, check for any lengthy operations in your pipelines.") } @@ -358,10 +364,6 @@ class PipelineManager( } } - if(!isActive) { - activePipelineContexts.remove(this.coroutineContext) - } - updateExceptionTracker() } catch (ex: Exception) { //handling exceptions from pipelines if(!hasInitCurrentPipeline) { @@ -397,8 +399,6 @@ class PipelineManager( withTimeout(timeout) { pipelineJob.join() } - - activePipelineContexts.remove(currentPipelineContext) } catch (ex: TimeoutCancellationException) { //oops, pipeline ran out of time! we'll fall back //to default pipeline to avoid further issues. diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java index 24b5d176..0f6f5ff4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/JavaProcess.java @@ -43,12 +43,11 @@ private JavaProcess() {} * @throws InterruptedException if the process is interrupted * @throws IOException if an I/O error occurs */ - public static int exec(Class klass, String... args) throws InterruptedException, IOException { + public static int execClasspath(Class klass, String classpath, String... args) throws InterruptedException, IOException { String javaHome = System.getProperty("java.home"); String javaBin = javaHome + File.separator + "bin" + File.separator + "java"; - String classpath = System.getProperty("java.class.path"); String className = klass.getName(); List command = new LinkedList<>(); @@ -67,4 +66,9 @@ public static int exec(Class klass, String... args) throws InterruptedException, return process.exitValue(); } + + public static int exec(Class klass, String... args) throws InterruptedException, IOException { + return execClasspath(klass, System.getProperty("java.class.path"), args); + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt index 35b7343a..004a2420 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginLoader.kt @@ -42,7 +42,7 @@ import java.security.MessageDigest * @param pluginFile the jar file of the plugin * @param eocvSim the EOCV-Sim instance */ -class PluginLoader(private val pluginFile: File, val eocvSim: EOCVSim) { +class PluginLoader(val pluginFile: File, val eocvSim: EOCVSim) { val logger by loggerForThis() diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt index 025cbdca..faf69e7f 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt @@ -56,6 +56,8 @@ class PluginManager(val eocvSim: EOCVSim) { private val loaders = mutableMapOf() + private var isEnabled = false + /** * Initializes the plugin manager * Loads all plugin files in the plugins folder @@ -78,6 +80,12 @@ class PluginManager(val eocvSim: EOCVSim) { for (pluginFile in pluginFiles) { loaders[pluginFile] = PluginLoader(pluginFile, eocvSim) } + + Runtime.getRuntime().addShutdownHook(Thread { + disablePlugins() + }) + + isEnabled = true } /** @@ -115,7 +123,10 @@ class PluginManager(val eocvSim: EOCVSim) { * Disables all plugins * @see PluginLoader.disable */ + @Synchronized fun disablePlugins() { + if(!isEnabled) return + for (loader in loaders.values) { try { loader.disable() @@ -124,6 +135,8 @@ class PluginManager(val eocvSim: EOCVSim) { loader.kill() } } + + isEnabled = false } /** diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/JimfsWatcher.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/JimfsWatcher.kt deleted file mode 100644 index 3c03a43f..00000000 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/JimfsWatcher.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2024 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.eocvsim.sandbox.nio - -import com.github.serivesmejia.eocvsim.util.loggerForThis -import java.io.* -import java.nio.file.* -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.attribute.FileTime -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -internal class JimfsWatcher(private val jimfs: FileSystem, private val zipFilePath: Path) { - - private val knownFiles = ConcurrentHashMap() - private val executor = Executors.newSingleThreadScheduledExecutor() - - val logger by loggerForThis() - - init { - startPolling() - } - - private fun startPolling() { - logger.info("Starting for $zipFilePath ...") - - executor.scheduleAtFixedRate({ - try { - checkForChanges() - } catch (e: IOException) { - logger.warn("IO Exception in executor", e) - } - }, 0, 3, TimeUnit.SECONDS) // Poll every 3 seconds - } - - fun stop() { - executor.shutdown() - } - - @Throws(IOException::class) - private fun checkForChanges() { - val root = jimfs.getPath("/") // or specify the root path if different - Files.walkFileTree(root, object : SimpleFileVisitor() { - @Throws(IOException::class) - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - if (Files.isRegularFile(file)) { - val lastModifiedTime = Files.getLastModifiedTime(file) - val previousTime = knownFiles[file] - - if (previousTime == null || !lastModifiedTime.equals(previousTime)) { - knownFiles[file] = lastModifiedTime - copyFileToZip(file, root.relativize(file).toString()) - } - } - return FileVisitResult.CONTINUE - } - }) - } - - @Throws(IOException::class) - private fun copyFileToZip(virtualFile: Path, entryName: String) { - ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath.toFile(), true))).use { zos -> - // Create a new entry in the ZIP file - zos.putNextEntry(ZipEntry(entryName)) - - // Copy the file content - Files.copy(virtualFile, zos) - - // Close the current entry - zos.closeEntry() - } - } -} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt index 96320f83..72bef1fd 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt @@ -24,29 +24,13 @@ package io.github.deltacv.eocvsim.sandbox.nio import com.github.serivesmejia.eocvsim.util.loggerForThis -import com.google.common.jimfs.Configuration -import com.google.common.jimfs.Feature -import com.google.common.jimfs.Jimfs -import com.google.common.jimfs.PathType import io.github.deltacv.eocvsim.plugin.loader.PluginLoader -import java.io.FileNotFoundException import java.nio.file.* import java.nio.file.attribute.FileAttribute -import java.util.zip.ZipFile class SandboxFileSystem(loader: PluginLoader) : FileSystem() { - val parent = Jimfs.newFileSystem( - Configuration.builder(PathType.unix()) - .setRoots("/") - .setWorkingDirectory("/") - .setAttributeViews("basic") - .setSupportedFeatures(Feature.SECURE_DIRECTORY_STREAM, Feature.FILE_CHANNEL) - .build() - ) - private val jimfsWatcher = JimfsWatcher(parent, loader.fileSystemZipPath) - - private val zipFile = ZipFile(loader.fileSystemZipPath.toFile()) + val parent = FileSystems.newFileSystem(loader.fileSystemZipPath, null) val logger by loggerForThis() @@ -54,44 +38,9 @@ class SandboxFileSystem(loader: PluginLoader) : FileSystem() { logger.info("Loading filesystem ${loader.hash()}") } - private fun checkForPath(path: Path): Boolean { - return if (Files.exists(path)) { - true - } else { - // Attempt to load the file if it doesn't exist - try { - loadFileToJimfs(path) - } catch(_: FileNotFoundException) {} - - true - } - } - - private fun loadFileToJimfs(path: Path) { - // Convert the path to a string relative to the root - val zipEntryPath = convertPathToZipEntryPath(path) - val zipEntry = zipFile.getEntry(zipEntryPath) ?: throw FileNotFoundException("File not found in ZIP: $zipEntryPath") - - // Ensure the parent directories exist in Jimfs before writing the file - val parentDir = path.parent - if (parentDir != null && !Files.exists(parentDir)) { - Files.createDirectories(parentDir) - } - - zipFile.getInputStream(zipEntry).use { inputStream -> - Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING) - } - } - - private fun convertPathToZipEntryPath(path: Path): String { - // Ensure the path is normalized and create a relative path for ZIP entries - val normalizedPath = path.normalize() - // Convert the path to a string with a leading directory (e.g., "work/") - return normalizedPath.toString().replace("\\", "/") - } + private fun checkForPath(path: Path) = Files.exists(path) override fun close() { - jimfsWatcher.stop() parent.close() } @@ -125,7 +74,7 @@ class SandboxFileSystem(loader: PluginLoader) : FileSystem() { } fun exists(path: Path, vararg options: LinkOption): Boolean { - checkForPath(path) + checkIfPathIsSandboxed(path) return Files.exists(path, *options) } @@ -161,4 +110,14 @@ class SandboxFileSystem(loader: PluginLoader) : FileSystem() { return Files.write(path, bytes, *options) } + fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) { + checkIfPathIsSandboxed(dir) + Files.createDirectory(dir, *attrs) + } + + fun createDirectories(dir: Path, vararg attrs: FileAttribute<*>) { + checkIfPathIsSandboxed(dir) + Files.createDirectories(dir, *attrs) + } + } \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java b/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java index b98b24bb..d88dd8a1 100644 --- a/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java +++ b/Vision/src/main/java/io/github/deltacv/eocvsim/pipeline/StreamableOpenCvPipeline.java @@ -12,4 +12,4 @@ public void streamFrame(int id, Mat image, Integer cvtCode) { streamer.sendFrame(id, image, cvtCode); } -} +} \ No newline at end of file From be3c08ad1df08b03537dde5fff37bbe75ee2822b Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 21 Sep 2024 21:11:03 -0600 Subject: [PATCH 4/5] Address changes of FTC SDK 10.1 --- .../java/androidx/annotation/ColorInt.java | 37 ++ .../qualcomm/robotcore/util/SortOrder.java | 28 + .../github/serivesmejia/eocvsim/EOCVSim.kt | 14 +- .../eocvsim/plugin/loader/PluginManager.kt | 6 +- .../eocvsim/sandbox/nio/SandboxFileSystem.kt | 6 + .../main/java/android/graphics/Canvas.java | 3 + .../opencv/ColorBlobLocatorProcessor.java | 484 ++++++++++++++++++ .../opencv/ColorBlobLocatorProcessorImpl.java | 430 ++++++++++++++++ .../ftc/vision/opencv/ColorRange.java | 86 ++++ .../ftc/vision/opencv/ColorSpace.java | 44 ++ .../ftc/vision/opencv/ImageRegion.java | 160 ++++++ .../opencv/PredominantColorProcessor.java | 170 ++++++ .../opencv/PredominantColorProcessorImpl.java | 262 ++++++++++ build.gradle | 2 +- 14 files changed, 1723 insertions(+), 9 deletions(-) create mode 100644 Common/src/main/java/androidx/annotation/ColorInt.java create mode 100644 Common/src/main/java/com/qualcomm/robotcore/util/SortOrder.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessor.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessorImpl.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorRange.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorSpace.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ImageRegion.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessor.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessorImpl.java diff --git a/Common/src/main/java/androidx/annotation/ColorInt.java b/Common/src/main/java/androidx/annotation/ColorInt.java new file mode 100644 index 00000000..ca5942a2 --- /dev/null +++ b/Common/src/main/java/androidx/annotation/ColorInt.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.annotation; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * Denotes that the annotated element represents a packed color + * int, {@code AARRGGBB}. If applied to an int array, every element + * in the array represents a color integer. + *

+ * Example: + *

{@code
+ *  public abstract void setTextColor(@ColorInt int color);
+ * }
+ */ +@Retention(CLASS) +@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD}) +public @interface ColorInt { +} \ No newline at end of file diff --git a/Common/src/main/java/com/qualcomm/robotcore/util/SortOrder.java b/Common/src/main/java/com/qualcomm/robotcore/util/SortOrder.java new file mode 100644 index 00000000..bc4fbcf8 --- /dev/null +++ b/Common/src/main/java/com/qualcomm/robotcore/util/SortOrder.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 FIRST + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.qualcomm.robotcore.util; + +public enum SortOrder +{ + ASCENDING, + DESCENDING +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index b688abb9..ffb1e9e9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -242,7 +242,7 @@ class EOCVSim(val params: Parameters = Parameters()) { * @see destroy */ enum class DestroyReason { - USER_REQUESTED, RESTART, CRASH + USER_REQUESTED, THREAD_EXIT, RESTART, CRASH } /** @@ -437,9 +437,12 @@ class EOCVSim(val params: Parameters = Parameters()) { logger.warn("Main thread interrupted ($hexCode)") + if(!destroying) { + destroy(DestroyReason.THREAD_EXIT) + } + if (isRestarting) { Thread.interrupted() //clear interrupted flag - EOCVSim(params).init() } } @@ -464,9 +467,14 @@ class EOCVSim(val params: Parameters = Parameters()) { configManager.saveToFile() visualizer.close() - eocvSimThread.interrupt() destroying = true + if(reason == DestroyReason.THREAD_EXIT) { + exitProcess(0) + } else { + eocvSimThread.interrupt() + } + if (reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) jvmMainThread.interrupt() } diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt index faf69e7f..c1402cbf 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/plugin/loader/PluginManager.kt @@ -81,10 +81,6 @@ class PluginManager(val eocvSim: EOCVSim) { loaders[pluginFile] = PluginLoader(pluginFile, eocvSim) } - Runtime.getRuntime().addShutdownHook(Thread { - disablePlugins() - }) - isEnabled = true } @@ -151,7 +147,7 @@ class PluginManager(val eocvSim: EOCVSim) { var warning = "$GENERIC_SUPERACCESS_WARN" if(reason.trim().isNotBlank()) { - warning += "

$reason" + warning += "

$reason" } warning += GENERIC_LAWYER_YEET diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt index 72bef1fd..1844c2be 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/sandbox/nio/SandboxFileSystem.kt @@ -36,6 +36,12 @@ class SandboxFileSystem(loader: PluginLoader) : FileSystem() { init { logger.info("Loading filesystem ${loader.hash()}") + Runtime.getRuntime().addShutdownHook(Thread { + if(isOpen) { + logger.info("Unloading filesystem ${loader.hash()} on shutdown") + close() + } + }) } private fun checkForPath(path: Path) = Files.exists(path) diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java index 2e3d8545..1bea8576 100644 --- a/Vision/src/main/java/android/graphics/Canvas.java +++ b/Vision/src/main/java/android/graphics/Canvas.java @@ -60,6 +60,9 @@ public Canvas drawLine(float x, float y, float x1, float y1, Paint paint) { return this; } + public void drawPoint(float x, float y, Paint paint) { + theCanvas.drawPoint(x, y, paint.thePaint); + } public void drawRoundRect(float l, float t, float r, float b, float xRad, float yRad, Paint rectPaint) { theCanvas.drawRRect(RRect.makeLTRB(l, t, r, b, xRad, yRad), rectPaint.thePaint); diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessor.java new file mode 100644 index 00000000..6b96c289 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessor.java @@ -0,0 +1,484 @@ +/* + * Copyright (c) 2024 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.opencv; + +import android.graphics.Color; + +import androidx.annotation.ColorInt; + +import com.qualcomm.robotcore.util.SortOrder; + +import org.firstinspires.ftc.vision.VisionProcessor; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Point; +import org.opencv.core.RotatedRect; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The {@link ColorBlobLocatorProcessor} finds "blobs" of a user-specified color + * in the image. You can restrict the search area to a specified Region + * of Interest (ROI). + */ +public abstract class ColorBlobLocatorProcessor implements VisionProcessor +{ + /** + * Class supporting construction of a {@link ColorBlobLocatorProcessor} + */ + public static class Builder + { + private ColorRange colorRange; + private ContourMode contourMode; + private ImageRegion imageRegion; + private int erodeSize = -1; + private int dilateSize = -1; + private boolean drawContours = false; + private int blurSize = -1; + private int boundingBoxColor = Color.rgb(255, 120, 31); + private int roiColor = Color.rgb(255, 255, 255); + private int contourColor = Color.rgb(3, 227, 252); + + /** + * Sets whether to draw the contour outline for the detected + * blobs on the camera preview. This can be helpful for debugging + * thresholding. + * @param drawContours whether to draw contours on the camera preview + * @return Builder object, to allow for method chaining + */ + public Builder setDrawContours(boolean drawContours) + { + this.drawContours = drawContours; + return this; + } + + /** + * Set the color used to draw the "best fit" bounding boxes for blobs + * @param color Android color int + * @return Builder object, to allow for method chaining + */ + public Builder setBoxFitColor(@ColorInt int color) + { + this.boundingBoxColor = color; + return this; + } + + /** + * Set the color used to draw the ROI on the camera preview + * @param color Android color int + * @return Builder object, to allow for method chaining + */ + public Builder setRoiColor(@ColorInt int color) + { + this.roiColor = color; + return this; + } + + /** + * Set the color used to draw blob contours on the camera preview + * @param color Android color int + * @return Builder object, to allow for method chaining + */ + public Builder setContourColor(@ColorInt int color) + { + this.contourColor = color; + return this; + } + + /** + * Set the color range used to find blobs + * @param colorRange the color range used to find blobs + * @return Builder object, to allow for method chaining + */ + public Builder setTargetColorRange(ColorRange colorRange) + { + this.colorRange = colorRange; + return this; + } + + /** + * Set the contour mode which will be used when generating + * the results provided by {@link #getBlobs()} + * @param contourMode contour mode which will be used when generating + * the results provided by {@link #getBlobs()} + * @return Builder object, to allow for method chaining + */ + public Builder setContourMode(ContourMode contourMode) + { + this.contourMode = contourMode; + return this; + } + + /** + * Set the Region of Interest on which to perform blob detection + * @param roi region of interest + * @return Builder object, to allow for method chaining + */ + public Builder setRoi(ImageRegion roi) + { + this.imageRegion = roi; + return this; + } + + /** + * Set the size of the blur kernel. Blurring can improve + * color thresholding results by smoothing color variation. + * @param blurSize size of the blur kernel + * 0 to disable + * @return Builder object, to allow for method chaining + */ + public Builder setBlurSize(int blurSize) + { + this.blurSize = blurSize; + return this; + } + + /** + * Set the size of the Erosion operation performed after applying + * the color threshold. Erosion eats away at the mask, reducing + * noise by eliminating super small areas, but also reduces the + * contour areas of everything a little bit. + * @param erodeSize size of the Erosion operation + * 0 to disable + * @return Builder object, to allow for method chaining + */ + public Builder setErodeSize(int erodeSize) + { + this.erodeSize = erodeSize; + return this; + } + + /** + * Set the size of the Dilation operation performed after applying + * the Erosion operation. Dilation expands mask areas, making up + * for shrinkage caused during erosion, and can also clean up results + * by closing small interior gaps in the mask. + * @param dilateSize the size of the Dilation operation performed + * 0 to disable + * @return Builder object, to allow for method chaining + */ + public Builder setDilateSize(int dilateSize) + { + this.dilateSize = dilateSize; + return this; + } + + /** + * Construct a {@link ColorBlobLocatorProcessor} object using previously + * set parameters + * @return a {@link ColorBlobLocatorProcessor} object which can be attached + * to your {@link org.firstinspires.ftc.vision.VisionPortal} + */ + public ColorBlobLocatorProcessor build() + { + if (colorRange == null) + { + throw new IllegalArgumentException("You must set a color range!"); + } + + if (contourMode == null) + { + throw new IllegalArgumentException("You must set a contour mode!"); + } + + return new ColorBlobLocatorProcessorImpl(colorRange, imageRegion, contourMode, erodeSize, dilateSize, drawContours, blurSize, boundingBoxColor, roiColor, contourColor); + } + } + + /** + * Determines what you get in {@link #getBlobs()} + */ + public enum ContourMode + { + /** + * Only return blobs from external contours + */ + EXTERNAL_ONLY, + + /** + * Return blobs which may be from nested contours + */ + ALL_FLATTENED_HIERARCHY + } + + /** + * The criteria used for filtering and sorting. + */ + public enum BlobCriteria + { + BY_CONTOUR_AREA, + BY_DENSITY, + BY_ASPECT_RATIO, + } + + /** + * Class describing how to filter blobs. + */ + public static class BlobFilter { + public final BlobCriteria criteria; + public final double minValue; + public final double maxValue; + + public BlobFilter(BlobCriteria criteria, double minValue, double maxValue) + { + this.criteria = criteria; + this.minValue = minValue; + this.maxValue = maxValue; + } + } + + /** + * Class describing how to sort blobs. + */ + public static class BlobSort + { + public final BlobCriteria criteria; + public final SortOrder sortOrder; + + public BlobSort(BlobCriteria criteria, SortOrder sortOrder) + { + this.criteria = criteria; + this.sortOrder = sortOrder; + } + } + + /** + * Class describing a Blob of color found inside the image + */ + public static abstract class Blob + { + /** + * Get the OpenCV contour for this blob + * @return OpenCV contour + */ + public abstract MatOfPoint getContour(); + + /** + * Get the contour points for this blob + * @return contour points for this blob + */ + public abstract Point[] getContourPoints(); + + /** + * Get the area enclosed by this blob's contour + * @return area enclosed by this blob's contour + */ + public abstract int getContourArea(); + + /** + * Get the density of this blob, i.e. ratio of + * contour area to convex hull area + * @return density of this blob + */ + public abstract double getDensity(); + + /** + * Get the aspect ratio of this blob, i.e. the ratio + * of longer side of the bounding box to the shorter side + * @return aspect ratio of this blob + */ + public abstract double getAspectRatio(); + + /** + * Get a "best fit" bounding box for this blob + * @return "best fit" bounding box for this blob + */ + public abstract RotatedRect getBoxFit(); + } + + /** + * Add a filter. + */ + public abstract void addFilter(BlobFilter filter); + + /** + * Remove a filter. + */ + public abstract void removeFilter(BlobFilter filter); + + /** + * Remove all filters. + */ + public abstract void removeAllFilters(); + + /** + * Sets the sort. + */ + public abstract void setSort(BlobSort sort); + + /** + * Get the results of the most recent blob analysis + * @return results of the most recent blob analysis + */ + public abstract List getBlobs(); + + /** + * Utility class for post-processing results from {@link #getBlobs()} + */ + public static class Util + { + /** + * Remove from a List of Blobs those which fail to meet an area criteria + * @param minArea minimum area + * @param maxArea maximum area + * @param blobs List of Blobs to operate on + */ + public static void filterByArea(double minArea, double maxArea, List blobs) + { + ArrayList toRemove = new ArrayList<>(); + + for(Blob b : blobs) + { + if (b.getContourArea() > maxArea || b.getContourArea() < minArea) + { + toRemove.add(b); + } + } + + blobs.removeAll(toRemove); + } + + /** + * Sort a list of Blobs based on area + * @param sortOrder sort order + * @param blobs List of Blobs to operate on + */ + public static void sortByArea(SortOrder sortOrder, List blobs) + { + blobs.sort(new Comparator() + { + public int compare(Blob c1, Blob c2) + { + int tmp = (int)Math.signum(c2.getContourArea() - c1.getContourArea()); + + if (sortOrder == SortOrder.ASCENDING) + { + tmp = -tmp; + } + + return tmp; + } + }); + } + + /** + * Remove from a List of Blobs those which fail to meet a density criteria + * @param minDensity minimum density + * @param maxDensity maximum desnity + * @param blobs List of Blobs to operate on + */ + public static void filterByDensity(double minDensity, double maxDensity, List blobs) + { + ArrayList toRemove = new ArrayList<>(); + + for(Blob b : blobs) + { + if (b.getDensity() > maxDensity || b.getDensity() < minDensity) + { + toRemove.add(b); + } + } + + blobs.removeAll(toRemove); + } + + /** + * Sort a list of Blobs based on density + * @param sortOrder sort order + * @param blobs List of Blobs to operate on + */ + public static void sortByDensity(SortOrder sortOrder, List blobs) + { + blobs.sort(new Comparator() + { + public int compare(Blob c1, Blob c2) + { + int tmp = (int)Math.signum(c2.getDensity() - c1.getDensity()); + + if (sortOrder == SortOrder.ASCENDING) + { + tmp = -tmp; + } + + return tmp; + } + }); + } + + /** + * Remove from a List of Blobs those which fail to meet an aspect ratio criteria + * @param minAspectRatio minimum aspect ratio + * @param maxAspectRatio maximum aspect ratio + * @param blobs List of Blobs to operate on + */ + public static void filterByAspectRatio(double minAspectRatio, double maxAspectRatio, List blobs) + { + ArrayList toRemove = new ArrayList<>(); + + for(Blob b : blobs) + { + if (b.getAspectRatio() > maxAspectRatio || b.getAspectRatio() < minAspectRatio) + { + toRemove.add(b); + } + } + + blobs.removeAll(toRemove); + } + + /** + * Sort a list of Blobs based on aspect ratio + * @param sortOrder sort order + * @param blobs List of Blobs to operate on + */ + public static void sortByAspectRatio(SortOrder sortOrder, List blobs) + { + blobs.sort(new Comparator() + { + public int compare(Blob c1, Blob c2) + { + int tmp = (int)Math.signum(c2.getAspectRatio() - c1.getAspectRatio()); + + if (sortOrder == SortOrder.ASCENDING) + { + tmp = -tmp; + } + + return tmp; + } + }); + } + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessorImpl.java new file mode 100644 index 00000000..dbf3247e --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorBlobLocatorProcessorImpl.java @@ -0,0 +1,430 @@ +package org.firstinspires.ftc.vision.opencv; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; + +import androidx.annotation.ColorInt; + +import com.qualcomm.robotcore.util.SortOrder; + +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.firstinspires.ftc.vision.VisionProcessor; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; + +import java.util.ArrayList; +import java.util.List; + +class ColorBlobLocatorProcessorImpl extends ColorBlobLocatorProcessor implements VisionProcessor +{ + private ColorRange colorRange; + private ImageRegion roiImg; + private Rect roi; + private int frameWidth; + private int frameHeight; + private Mat roiMat; + private Mat roiMat_userColorSpace; + private final int contourCode; + + private Mat mask = new Mat(); + + private final Paint boundingRectPaint; + private final Paint roiPaint; + private final Paint contourPaint; + private final boolean drawContours; + private final @ColorInt int boundingBoxColor; + private final @ColorInt int roiColor; + private final @ColorInt int contourColor; + + private final Mat erodeElement; + private final Mat dilateElement; + private final Size blurElement; + + private final Object lockFilters = new Object(); + private final List filters = new ArrayList<>(); + private volatile BlobSort sort; + + private volatile ArrayList userBlobs = new ArrayList<>(); + + ColorBlobLocatorProcessorImpl(ColorRange colorRange, ImageRegion roiImg, ContourMode contourMode, + int erodeSize, int dilateSize, boolean drawContours, int blurSize, + @ColorInt int boundingBoxColor, @ColorInt int roiColor, @ColorInt int contourColor) + { + this.colorRange = colorRange; + this.roiImg = roiImg; + this.drawContours = drawContours; + this.boundingBoxColor = boundingBoxColor; + this.roiColor = roiColor; + this.contourColor = contourColor; + + if (blurSize > 0) + { + // enforce Odd blurSize + blurElement = new Size(blurSize | 0x01, blurSize | 0x01); + } + else + { + blurElement = null; + } + + if (contourMode == ContourMode.EXTERNAL_ONLY) + { + contourCode = Imgproc.RETR_EXTERNAL; + } + else + { + contourCode = Imgproc.RETR_LIST; + } + + if (erodeSize > 0) + { + erodeElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(erodeSize, erodeSize)); + } + else + { + erodeElement = null; + } + + if (dilateSize > 0) + { + dilateElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(dilateSize, dilateSize)); + } + else + { + dilateElement = null; + } + + boundingRectPaint = new Paint(); + boundingRectPaint.setAntiAlias(true); + boundingRectPaint.setStrokeCap(Paint.Cap.BUTT); + boundingRectPaint.setColor(boundingBoxColor); + + roiPaint = new Paint(); + roiPaint.setAntiAlias(true); + roiPaint.setStrokeCap(Paint.Cap.BUTT); + roiPaint.setColor(roiColor); + + contourPaint = new Paint(); + contourPaint.setStyle(Paint.Style.STROKE); + contourPaint.setColor(contourColor); + } + + @Override + public void init(int width, int height, CameraCalibration calibration) + { + frameWidth = width; + frameHeight = height; + + roi = roiImg.asOpenCvRect(width, height); + } + + @Override + public Object processFrame(Mat frame, long captureTimeNanos) + { + if (roiMat == null) + { + roiMat = frame.submat(roi); + roiMat_userColorSpace = roiMat.clone(); + } + + if (colorRange.colorSpace == ColorSpace.YCrCb) + { + Imgproc.cvtColor(roiMat, roiMat_userColorSpace, Imgproc.COLOR_RGB2YCrCb); + } + else if (colorRange.colorSpace == ColorSpace.HSV) + { + Imgproc.cvtColor(roiMat, roiMat_userColorSpace, Imgproc.COLOR_RGB2HSV); + } + else if (colorRange.colorSpace == ColorSpace.RGB) + { + Imgproc.cvtColor(roiMat, roiMat_userColorSpace, Imgproc.COLOR_RGBA2RGB); + } + + if (blurElement != null) + { + Imgproc.GaussianBlur(roiMat_userColorSpace, roiMat_userColorSpace, blurElement, 0); + } + + Core.inRange(roiMat_userColorSpace, colorRange.min, colorRange.max, mask); + + if (erodeElement != null) + { + Imgproc.erode(mask, mask, erodeElement); + } + + if (dilateElement != null) + { + Imgproc.dilate(mask, mask, dilateElement); + } + + ArrayList contours = new ArrayList<>(); + Mat hierarchy = new Mat(); + Imgproc.findContours(mask, contours, hierarchy, contourCode, Imgproc.CHAIN_APPROX_SIMPLE); + hierarchy.release(); + + ArrayList blobs = new ArrayList<>(); + for (MatOfPoint contour : contours) + { + Core.add(contour, new Scalar(roi.x, roi.y), contour); + blobs.add(new BlobImpl(contour)); + } + + // Apply filters. + synchronized (lockFilters) + { + for (BlobFilter filter : filters) + { + switch (filter.criteria) + { + case BY_CONTOUR_AREA: + Util.filterByArea(filter.minValue, filter.maxValue, blobs); + break; + case BY_DENSITY: + Util.filterByDensity(filter.minValue, filter.maxValue, blobs); + break; + case BY_ASPECT_RATIO: + Util.filterByAspectRatio(filter.minValue, filter.maxValue, blobs); + break; + } + } + } + + // Apply sorting. + BlobSort sort = this.sort; // Put the field into a local variable for thread safety. + if (sort != null) + { + switch (sort.criteria) + { + case BY_CONTOUR_AREA: + Util.sortByArea(sort.sortOrder, blobs); + break; + case BY_DENSITY: + Util.sortByDensity(sort.sortOrder, blobs); + break; + case BY_ASPECT_RATIO: + Util.sortByAspectRatio(sort.sortOrder, blobs); + break; + } + } + else + { + // Apply a default sort by area + Util.sortByArea(SortOrder.DESCENDING, blobs); + } + + // Deep copy this to prevent concurrent modification exception + userBlobs = new ArrayList<>(blobs); + + return blobs; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + ArrayList blobs = (ArrayList) userContext; + + contourPaint.setStrokeWidth(scaleCanvasDensity * 4); + boundingRectPaint.setStrokeWidth(scaleCanvasDensity * 10); + roiPaint.setStrokeWidth(scaleCanvasDensity * 10); + + android.graphics.Rect gfxRect = makeGraphicsRect(roi, scaleBmpPxToCanvasPx); + + for (Blob blob : blobs) + { + if (drawContours) + { + Path path = new Path(); + + Point[] contourPts = blob.getContourPoints(); + + path.moveTo((float) (contourPts[0].x) * scaleBmpPxToCanvasPx, (float)(contourPts[0].y) * scaleBmpPxToCanvasPx); + for (int i = 1; i < contourPts.length; i++) + { + path.lineTo((float) (contourPts[i].x) * scaleBmpPxToCanvasPx, (float) (contourPts[i].y) * scaleBmpPxToCanvasPx); + } + path.close(); + + canvas.drawPath(path, contourPaint); + } + + /* + * Draws a rotated rect by drawing each of the 4 lines individually + */ + Point[] rotRectPts = new Point[4]; + blob.getBoxFit().points(rotRectPts); + + for(int i = 0; i < 4; ++i) + { + canvas.drawLine( + (float) (rotRectPts[i].x)*scaleBmpPxToCanvasPx, (float) (rotRectPts[i].y)*scaleBmpPxToCanvasPx, + (float) (rotRectPts[(i+1)%4].x)*scaleBmpPxToCanvasPx, (float) (rotRectPts[(i+1)%4].y)*scaleBmpPxToCanvasPx, + boundingRectPaint + ); + } + } + + canvas.drawLine(gfxRect.left, gfxRect.top, gfxRect.right, gfxRect.top, roiPaint); + canvas.drawLine(gfxRect.right, gfxRect.top, gfxRect.right, gfxRect.bottom, roiPaint); + canvas.drawLine(gfxRect.right, gfxRect.bottom, gfxRect.left, gfxRect.bottom, roiPaint); + canvas.drawLine(gfxRect.left, gfxRect.bottom, gfxRect.left, gfxRect.top, roiPaint); + } + + private android.graphics.Rect makeGraphicsRect(Rect rect, float scaleBmpPxToCanvasPx) + { + int left = Math.round(rect.x * scaleBmpPxToCanvasPx); + int top = Math.round(rect.y * scaleBmpPxToCanvasPx); + int right = left + Math.round(rect.width * scaleBmpPxToCanvasPx); + int bottom = top + Math.round(rect.height * scaleBmpPxToCanvasPx); + + return new android.graphics.Rect(left, top, right, bottom); + } + + @Override + public void addFilter(BlobFilter filter) + { + synchronized (lockFilters) + { + filters.add(filter); + } + } + + @Override + public void removeFilter(BlobFilter filter) + { + synchronized (lockFilters) + { + filters.remove(filter); + } + } + + @Override + public void removeAllFilters() + { + synchronized (lockFilters) + { + filters.clear(); + } + } + + @Override + public void setSort(BlobSort sort) + { + this.sort = sort; + } + + @Override + public List getBlobs() + { + return userBlobs; + } + + class BlobImpl extends Blob + { + private MatOfPoint contour; + private Point[] contourPts; + private int area = -1; + private double density = -1; + private double aspectRatio = -1; + private RotatedRect rect; + + BlobImpl(MatOfPoint contour) + { + this.contour = contour; + } + + @Override + public MatOfPoint getContour() + { + return contour; + } + + @Override + public Point[] getContourPoints() + { + if (contourPts == null) + { + contourPts = contour.toArray(); + } + + return contourPts; + } + + @Override + public int getContourArea() + { + if (area < 0) + { + area = Math.max(1, (int) Imgproc.contourArea(contour)); // Fix zero area issue + } + + return area; + } + + @Override + public double getDensity() + { + Point[] contourPts = getContourPoints(); + + if (density < 0) + { + // Compute the convex hull of the contour + MatOfInt hullMatOfInt = new MatOfInt(); + Imgproc.convexHull(contour, hullMatOfInt); + + // The convex hull calculation tells us the INDEX of the points which + // which were passed in eariler which form the convex hull. That's all + // well and good, but now we need filter out that original list to find + // the actual POINTS which form the convex hull + Point[] hullPoints = new Point[hullMatOfInt.rows()]; + List hullContourIdxList = hullMatOfInt.toList(); + + for (int i = 0; i < hullContourIdxList.size(); i++) + { + hullPoints[i] = contourPts[hullContourIdxList.get(i)]; + } + + double hullArea = Math.max(1.0,Imgproc.contourArea(new MatOfPoint(hullPoints))); // Fix zero area issue + + density = getContourArea() / hullArea; + } + return density; + } + + @Override + public double getAspectRatio() + { + if (aspectRatio < 0) + { + RotatedRect r = getBoxFit(); + + double longSize = Math.max(1, Math.max(r.size.width, r.size.height)); + double shortSize = Math.max(1, Math.min(r.size.width, r.size.height)); + + aspectRatio = longSize / shortSize; + } + + return aspectRatio; + } + + @Override + public RotatedRect getBoxFit() + { + if (rect == null) + { + rect = Imgproc.minAreaRect(new MatOfPoint2f(getContourPoints())); + } + return rect; + } + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorRange.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorRange.java new file mode 100644 index 00000000..1ad9148b --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorRange.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.opencv; + +import org.opencv.core.Scalar; + +/** + * An {@link ColorRange represents a 3-channel minimum/maximum + * range for a given color space} + */ +public class ColorRange +{ + protected final ColorSpace colorSpace; + protected final Scalar min; + protected final Scalar max; + + // ----------------------------------------------------------------------------- + // DEFAULT OPTIONS + // ----------------------------------------------------------------------------- + + public static final ColorRange BLUE = new ColorRange( + ColorSpace.YCrCb, + new Scalar( 16, 0, 155), + new Scalar(255, 127, 255) + ); + + public static final ColorRange RED = new ColorRange( + ColorSpace.YCrCb, + new Scalar( 32, 176, 0), + new Scalar(255, 255, 132) + ); + + public static final ColorRange YELLOW = new ColorRange( + ColorSpace.YCrCb, + new Scalar( 32, 128, 0), + new Scalar(255, 170, 120) + ); + + public static final ColorRange GREEN = new ColorRange( + ColorSpace.YCrCb, + new Scalar( 32, 0, 0), + new Scalar(255, 120, 133) + ); + + // ----------------------------------------------------------------------------- + // ROLL YOUR OWN + // ----------------------------------------------------------------------------- + + public ColorRange(ColorSpace colorSpace, Scalar min, Scalar max) + { + this.colorSpace = colorSpace; + this.min = min; + this.max = max; + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorSpace.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorSpace.java new file mode 100644 index 00000000..e21301ca --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ColorSpace.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.opencv; + +/** + * A {@link ColorSpace} is a means to map a set of numerical values to colors + */ +public enum ColorSpace +{ + YCrCb, + HSV, + RGB, +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ImageRegion.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ImageRegion.java new file mode 100644 index 00000000..83a65a13 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/ImageRegion.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.opencv; + +import com.qualcomm.robotcore.util.Range; +import org.opencv.core.Rect; + +/** + * An {@link ImageRegion} defines an area of an image buffer in terms of either a typical + * image processing coordinate system wherein the origin is in the top left corner and + * the domain of X and Y is dictated by the resolution of said image buffer; OR a "unity center" + * coordinate system wherein the origin is at the middle of the image and the domain of + * X and Y is {-1, 1} such that the region can be defined independent of the actual resolution + * of the image buffer. + */ +public class ImageRegion +{ + final boolean imageCoords; + final double left, top, right, bottom; + + /** + * Internal constructor + * @param imageCoords whether these coordinates are typical image processing coordinates + * @param left left coordinate + * @param top top coordinate + * @param right right coordiante + * @param bottom bottom coordinate + */ + private ImageRegion(boolean imageCoords, double left, double top, double right, double bottom ) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + this.imageCoords = imageCoords; + } + + /** + * Construct an {@link ImageRegion} using typical image processing coordinates + * + * -------------------------------------------- + * | (0,0)-------X | + * | | | + * | | | + * | Y | + * | | + * | (width,height) | + * -------------------------------------------- + * + * @param left left X coordinate {0, width} + * @param top top Y coordinate {0, height} + * @param right right X coordinate {0, width} + * @param bottom bottom Y coordinate {0, height} + * @return an {@link ImageRegion} object describing the region + */ + public static ImageRegion asImageCoordinates(int left, int top, int right, int bottom ) + { + return new ImageRegion(true, left, top, right, bottom); + } + + /** + * Construct an {@link ImageRegion} using "Unity Center" coordinates + * + * -------------------------------------------- + * | (-1,1) Y (1,1) | + * | | | + * | | | + * | (0,0) ----- X | + * | | + * | (-1,-1) (1, -1) | + * -------------------------------------------- + * + * @param left left X coordinate {-1, 1} + * @param top top Y coordinate {-1, 1} + * @param right right X coordinate {-1, 1} + * @param bottom bottom Y coordinate {-1, 1} + * @return an {@link ImageRegion} object describing the region + */ + public static ImageRegion asUnityCenterCoordinates(double left, double top, double right, double bottom) + { + return new ImageRegion(false, left, top, right, bottom); + } + + /** + * Construct an {@link ImageRegion} representing the entire frame + * @return an {@link ImageRegion} representing the entire frame + */ + public static ImageRegion entireFrame() + { + return ImageRegion.asUnityCenterCoordinates(-1, 1, 1, -1); + } + + /** + * Create an OpenCV Rect object which is representative of this {@link ImageRegion} + * for a specific image buffer size + * + * @param imageWidth width of the image buffer + * @param imageHeight height of the image buffer + * @return OpenCV Rect + */ + protected Rect asOpenCvRect(int imageWidth, int imageHeight) + { + Rect rect = new Rect(); + + if (imageCoords) + { + rect.x = (int) left; + rect.y = (int) top; + rect.width = (int) (right - left); + rect.height = (int) (bottom - top); + } + else // unity center + { + rect.x = (int) Range.scale(left, -1, 1, 0, imageWidth); + rect.y = (int) ( imageHeight - Range.scale(top, -1, 1, 0, imageHeight)); + rect.width = (int) Range.scale(right - left, 0, 2, 0, imageWidth); + rect.height = (int) Range.scale(top - bottom, 0, 2, 0, imageHeight); + } + + // Adjust the window position to ensure it stays on the screen. push it back into the screen area. + // We could just crop it instead, but then it may completely miss the screen. + rect.x = Math.max(rect.x, 0); + rect.x = Math.min(rect.x, imageWidth - rect.width); + rect.y = Math.max(rect.y, 0); + rect.y = Math.min(rect.y, imageHeight - rect.height); + + return rect; + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessor.java new file mode 100644 index 00000000..6596d42a --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessor.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2024 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.opencv; + +import org.firstinspires.ftc.vision.VisionProcessor; +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link PredominantColorProcessor} acts like a "Color Sensor", + * allowing you to define a Region of Interest (ROI) of the camera + * stream inside of which the dominant color is found. Additionally, + * said color is matched to one of the {@link Swatch}s specified by + * the user as a "best guess" at the general shade of the color + */ +public abstract class PredominantColorProcessor implements VisionProcessor +{ + /** + * Class supporting construction of a {@link PredominantColorProcessor} + */ + public static class Builder + { + ImageRegion roi; + Swatch[] swatches; + + /** + * Set the Region of Interest on which to perform color analysis + * @param roi region of interest + * @return Builder object, to allow for method chaining + */ + public Builder setRoi(ImageRegion roi) + { + this.roi = roi; + return this; + } + + /** + * Set the Swatches from which a "best guess" at the shade of the + * predominant color will be made + * @param swatches Swatches to choose from + * @return Builder object, to allow for method chaining + */ + public Builder setSwatches(Swatch... swatches) + { + this.swatches = swatches; + return this; + } + + /** + * Construct a {@link PredominantColorProcessor} object using previously + * set parameters + * @return a {@link PredominantColorProcessor} object which can be attached + * to your {@link org.firstinspires.ftc.vision.VisionPortal} + */ + public PredominantColorProcessor build() + { + if (roi == null) + { + throw new IllegalArgumentException("You must call setRoi()!"); + } + + if (swatches == null) + { + throw new IllegalArgumentException("You must call setSwatches()!"); + } + + return new PredominantColorProcessorImpl(roi, swatches); + } + } + + /** + * Get the result of the most recent color analysis + * @return result of the most recent color analysis + */ + public abstract Result getAnalysis(); + + /** + * Class describing the result of color analysis on the ROI + */ + public static class Result + { + /** + * "Best guess" at the general shade of the dominant color in the ROI + */ + public final Swatch closestSwatch; + + /** + * Exact numerical value of the dominant color in the ROI + */ + public final int rgb; + + public Result(Swatch closestSwatch, int rgb) + { + this.closestSwatch = closestSwatch; + this.rgb = rgb; + } + } + + /** + * Swatches from which you may choose from when invoking + * {@link Builder#setSwatches(Swatch...)} + */ + public enum Swatch + { + RED(0), + ORANGE(30), + YELLOW(46), + GREEN(120), + CYAN(180), + BLUE(240), + PURPLE(270), + MAGENTA(300), + BLACK(-1), + WHITE(-2); + + final int hue; + + // hue range 0-360 + Swatch(int hue) + { + this.hue = hue; + } + + private static Map map = new HashMap<>(); + + static + { + for (Swatch swatch : Swatch.values()) + { + map.put(swatch.hue, swatch); + } + } + + public static Swatch valueOf(int swatch) + { + return (Swatch) map.get(swatch); + } + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessorImpl.java new file mode 100644 index 00000000..01bf5568 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/opencv/PredominantColorProcessorImpl.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2024 FIRST + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior + * written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.vision.opencv; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; + +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Rect; +import org.opencv.core.TermCriteria; +import org.opencv.imgproc.Imgproc; + +import java.util.ArrayList; +import java.util.Arrays; + +class PredominantColorProcessorImpl extends PredominantColorProcessor +{ + private Mat roiMat; + private Mat roiMat_YCrCb; + private int frameWidth; + private int frameHeight; + private Rect roi; + private ImageRegion roiImg; + private int roiNumPixels; + private Mat roiFlattened; + private byte[] roi_YCrCb_data; + private float[] roiFlattened_data; + + private static int K = 5; // Get the top n color hues + + private final Paint boundingRectPaint; + private final Paint boundingRectCrosshairPaint; + + private volatile Result result = new Result(null, 0); + + private final ArrayList swatches; + + PredominantColorProcessorImpl(ImageRegion roi, Swatch[] swatches) + { + this.roiImg = roi; + + boundingRectPaint = new Paint(); + boundingRectPaint.setAntiAlias(true); + boundingRectPaint.setStrokeCap(Paint.Cap.ROUND); + boundingRectPaint.setColor(Color.WHITE); + boundingRectPaint.setStyle(Paint.Style.STROKE); + + boundingRectCrosshairPaint = new Paint(); + boundingRectCrosshairPaint.setAntiAlias(true); + boundingRectCrosshairPaint.setStrokeCap(Paint.Cap.BUTT); + boundingRectCrosshairPaint.setColor(Color.WHITE); + + this.swatches = new ArrayList<>(Arrays.asList(swatches)); + } + + @Override + public void init(int width, int height, CameraCalibration calibration) + { + this.frameWidth = width; + this.frameHeight = height; + + roi = roiImg.asOpenCvRect(width, height); + this.roiNumPixels = roi.width * roi.height; + this.roiFlattened = new Mat(roiNumPixels, 2, CvType.CV_32F); + roiFlattened_data = new float[roiNumPixels*2]; + roi_YCrCb_data = new byte[roiNumPixels*3]; + } + + @Override + public Object processFrame(Mat frame, long captureTimeNanos) + { + if (roiMat == null) + { + roiMat = frame.submat(roi); + roiMat_YCrCb = roiMat.clone(); + } + + Imgproc.cvtColor(roiMat, roiMat_YCrCb, Imgproc.COLOR_RGB2YCrCb); + + int avgLuminance = (int) (Core.sumElems(roiMat_YCrCb).val[0] / roiNumPixels); + + // flatten data for K-means + roiMat_YCrCb.get(0,0, roi_YCrCb_data); + for (int i = 0; i < roiNumPixels; i++) + { + int cr = roi_YCrCb_data[i*3 + 1]; + int cb = roi_YCrCb_data[i*3 + 2]; + + roiFlattened_data[i*2 ] = cr; + roiFlattened_data[i*2 + 1] = cb; + } + roiFlattened.put(0,0, roiFlattened_data); + + // Perform K-Means clustering + Mat labels = new Mat(); + Mat centers = new Mat(K, roiFlattened.cols(), roiFlattened.type()); + TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 10, 2.0); + + Core.kmeans(roiFlattened, K, labels, criteria, 1, Core.KMEANS_PP_CENTERS, centers); + + int[] clusterCounts = new int[K]; + int maxCount = 0; + int maxCountIndex = 0; + + int[] clusterIndicies = new int[roiNumPixels]; + labels.get(0,0, clusterIndicies); + + // Get the biggest count along the way + for (int i = 0; i < roiNumPixels; i++) + { + int clusterIndex = clusterIndicies[i]; + int newCount = clusterCounts[clusterIndex]++; + + if (newCount > maxCount) + { + maxCount = newCount; + maxCountIndex = clusterIndex; + } + } + + double Y = avgLuminance; // Luminance + double Cr = centers.get(maxCountIndex, 0)[0]; // Red-difference Chroma + double Cb = centers.get(maxCountIndex, 1)[0]; // Blue-difference Chroma + + byte[] rgb = yCrCb2Rgb(new byte[] {(byte) Y, (byte) Cr, (byte) Cb}); + float[] hsv = new float[3]; + int color = Color.rgb(rgb[0] & 0xFF, rgb[1] & 0xFF, rgb[2] & 0xFF); + + // Note this used 0-360, 0-1, 0-1 + Color.colorToHSV(color, hsv); + + float H = hsv[0]; + float S = hsv[1]; + float V = hsv[2]; + + // Log.d("Best HSV", String.format("H:%3.0f, S:%4.2f, V:%4.2f", H, S, V)); + + Swatch closestSwatch = null; + + // Check for Black or White before matching Hue. + if ((S < 0.15 && V > 0.55) && swatches.contains(Swatch.WHITE)) + { + closestSwatch = Swatch.WHITE; + } + else if ((V < 0.1) || (S < 0.2 || V < 0.2) && swatches.contains(Swatch.BLACK)) + { + closestSwatch = Swatch.BLACK; + } + else + { + // now scan the colorHue table to find the table entry closest to the prime hue. + // watch for hue wrap around at 360. + int shortestHueDist = 360; + + for (Swatch swatch : swatches) + { + if (swatch.hue < 0) + { + // Black or white + continue; + } + + int hueError = Math.abs((int) H - swatch.hue); + if (hueError > 180) + { + // wrap it around + hueError = 360 - hueError; + } + if (hueError < shortestHueDist) + { + shortestHueDist = hueError; + closestSwatch = swatch; + } + } + } + + result = new Result(closestSwatch, color); + + return result; + } + + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + android.graphics.Rect gfxRect = makeGraphicsRect(roi, scaleBmpPxToCanvasPx); + + boundingRectCrosshairPaint.setStrokeWidth(5 * scaleCanvasDensity); + canvas.drawLine(gfxRect.centerX(), gfxRect.top, gfxRect.centerX(), gfxRect.bottom, boundingRectCrosshairPaint); + canvas.drawLine(gfxRect.left, gfxRect.centerY(), gfxRect.right, gfxRect.centerY(), boundingRectCrosshairPaint); + + boundingRectPaint.setStrokeWidth(10 * scaleCanvasDensity); + boundingRectPaint.setColor(((Result)userContext).rgb); + canvas.drawRect(gfxRect, boundingRectPaint); + + canvas.drawPoint(gfxRect.left, gfxRect.top, boundingRectCrosshairPaint); + canvas.drawPoint(gfxRect.left, gfxRect.bottom, boundingRectCrosshairPaint); + canvas.drawPoint(gfxRect.right, gfxRect.top, boundingRectCrosshairPaint); + canvas.drawPoint(gfxRect.right, gfxRect.bottom, boundingRectCrosshairPaint); + } + + private android.graphics.Rect makeGraphicsRect(Rect rect, float scaleBmpPxToCanvasPx) + { + int left = Math.round(rect.x * scaleBmpPxToCanvasPx); + int top = Math.round(rect.y * scaleBmpPxToCanvasPx); + int right = left + Math.round(rect.width * scaleBmpPxToCanvasPx); + int bottom = top + Math.round(rect.height * scaleBmpPxToCanvasPx); + + return new android.graphics.Rect(left, top, right, bottom); + } + + @Override + public Result getAnalysis() + { + return result; + } + + byte[] yCrCb2Rgb(byte[] yCrCb) + { + Mat cvtColor = new Mat(1,1,CvType.CV_8UC3); + cvtColor.put(0,0, yCrCb); + Imgproc.cvtColor(cvtColor, cvtColor, Imgproc.COLOR_YCrCb2RGB); + byte[] rgb = new byte[3]; + cvtColor.get(0,0, rgb); + return rgb; + } +} diff --git a/build.gradle b/build.gradle index 6192f2fa..d99a652a 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ plugins { allprojects { group 'com.github.deltacv' - version '4.0.0' + version '3.7.0' apply plugin: 'java' From 98cf5fdd3ebef0a496dff7aa5fc6ea73598a9ba0 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 21 Sep 2024 21:17:13 -0600 Subject: [PATCH 5/5] Changelog for 3.6.0 --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 3334f77a..7d277903 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,19 @@ For bug reporting or feature requesting, use the [issues tab](https://github.com ### Formerly, EOCV-Sim was hosted on a [personal account repo](https://github.com/serivesmejia/EOCV-Sim/). Released prior to 3.0.0 can be found there for historic purposes. + +### [v3.7.0 - FTC SDK 10.1 & Refined Plugin System](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.5.4) +- This is the 23nd release for EOCV-Sim + - Changelog + - Addresses the changes made in the FTC SDK 10.1 for the 2024-2025 season: + - Adds new OpenCV-based VisionProcessors (which may be attached to a VisionPortal in either Java or Blocks) to help teams implement color processing via computer vision in the INTO THE DEEP game + - Internal changes: + - Fixes virtual filesystem by scrapping jimfs and using a zip filesystem. + - Implements virtualreflect api for variable tuner to abstract away the reflection api and allow for future diverse implementations. + - Implements StreamableOpenCvPipeline to allow for diverse implementations of streaming different Mat stages of a pipeline to a target. + - Bugfixes: + - Fixes exception loop when an exception is thrown from pipeline init + ### [v3.6.0 - Plugin System & Into the Deep AprilTags](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.5.4) - This is the 22nd release for EOCV-Sim - Changelog