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/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/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/build.gradle b/EOCV-Sim/build.gradle
index eed74a75..aea2c540 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')
@@ -66,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..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
@@ -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
@@ -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/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/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 3617079a..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
@@ -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
@@ -258,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.")
}
@@ -347,22 +364,18 @@ class PipelineManager(
}
}
- if(!isActive) {
- activePipelineContexts.remove(this.coroutineContext)
- }
-
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(currentPipelineIndex)
+ eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = 0
+ changePipeline(0)
- logger.error("Error while initializing requested pipeline, $currentPipelineName", ex)
+ logger.error("Error while initializing requested pipeline, $currentPipelineName. Falling back to default.", ex)
} else {
updateExceptionTracker(ex)
}
@@ -386,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.
@@ -463,10 +474,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
@@ -601,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()
@@ -668,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)
}
}
@@ -698,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 extends TunableFieldAcceptor>> 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 extends TunableField> getTunableFieldOf(Field field) {
+ public Class extends TunableField> 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 extends TunableField> 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 extends TunableField> 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 extends TunableField> constructor = tunableFieldClass.getConstructor(Object.class, Field.class, EOCVSim.class);
- this.fields.add(constructor.newInstance(target, field, eocvSim));
+ Constructor extends TunableField> 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/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/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 f9e18a9a..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
@@ -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
@@ -44,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 {
@@ -87,33 +93,33 @@ 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<*> {
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
}
+ }
+
+ if(!inWhitelist && !pluginContext.hasSuperAccess) {
+ throw IllegalAccessError("Plugins are not whitelisted to use $name")
+ }
- // fallback to the system classloader
- clazz = Class.forName(name)
+ clazz = try {
+ Class.forName(name)
+ } catch (e: ClassNotFoundException) {
+ loadClassStrict(name)
}
+
+ if(resolve) resolveClass(clazz)
}
return clazz!!
@@ -131,6 +137,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/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..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
@@ -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,8 @@ class PluginManager(val eocvSim: EOCVSim) {
for (pluginFile in pluginFiles) {
loaders[pluginFile] = PluginLoader(pluginFile, eocvSim)
}
+
+ isEnabled = true
}
/**
@@ -115,7 +119,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 +131,8 @@ class PluginManager(val eocvSim: EOCVSim) {
loader.kill()
}
}
+
+ isEnabled = false
}
/**
@@ -138,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/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..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
@@ -24,74 +24,29 @@
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()
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): 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 +80,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 +116,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/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/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 00000000..22f456d8
Binary files /dev/null and b/EOCV-Sim/src/main/resources/images/icon/ico_eocvsim.ico differ
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
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/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..d88dd8a1
--- /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);
+ }
+
+}
\ No newline at end of file
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/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 472c1f3d..d99a652a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,7 +37,7 @@ plugins {
allprojects {
group 'com.github.deltacv'
- version '3.6.0'
+ version '3.7.0'
apply plugin: 'java'