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> tunableFieldAcceptors = null; - private boolean firstInit = true; + private VirtualReflection reflect = JvmVirtualReflection.INSTANCE; - Logger logger = LoggerFactory.getLogger(getClass()); + private boolean firstInit = true; public TunerManager(EOCVSim eocvSim) { this.eocvSim = eocvSim; @@ -84,14 +90,14 @@ public void init() { } if (eocvSim.pipelineManager.getCurrentPipeline() != null) { - addFieldsFrom(eocvSim.pipelineManager.getCurrentTunerTarget()); + addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline()); eocvSim.visualizer.updateTunerFields(createTunableFieldPanels()); for(TunableField field : fields.toArray(new TunableField[0])) { try { field.init(); } catch(CancelTunableFieldAddingException e) { - logger.trace("Field " + field.getFieldName() + " was removed due to \"" + e.getMessage() + "\""); + logger.info("Field " + field.getFieldName() + " was removed due to \"" + e.getMessage() + "\""); fields.remove(field); } } @@ -122,9 +128,9 @@ public void reset() { init(); } - public Class getTunableFieldOf(Field field) { + public Class getTunableFieldOf(VirtualField field) { //we only accept non-final fields - if (Modifier.isFinal(field.getModifiers())) return null; + if (field.isFinal()) return null; Class type = field.getType(); if (field.getType().isPrimitive()) { //wrap to java object equivalent if field type is primitive @@ -145,12 +151,15 @@ public Class getTunableFieldOf(Field field) { return tunableFieldClass; } - public void addFieldsFrom(Object target) { - if (target == null) return; + public void addFieldsFrom(OpenCvPipeline pipeline) { + if (pipeline == null) return; + + VirtualReflectContext reflectContext = reflect.contextOf(pipeline); + if(reflectContext == null) return; - Field[] fields = target.getClass().getFields(); + VirtualField[] fields = reflect.contextOf(pipeline).getFields(); - for (Field field : fields) { + for (VirtualField field : fields) { Class tunableFieldClass = getTunableFieldOf(field); // we can't handle this type @@ -160,12 +169,14 @@ public void addFieldsFrom(Object target) { //now, lets do some more reflection to instantiate this TunableField //and add it to the list... try { - Constructor constructor = tunableFieldClass.getConstructor(Object.class, Field.class, EOCVSim.class); - this.fields.add(constructor.newInstance(target, field, eocvSim)); + Constructor constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, VirtualField.class, EOCVSim.class); + this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); } catch(InvocationTargetException e) { if(e.getCause() instanceof CancelTunableFieldAddingException) { String message = e.getCause().getMessage(); logger.info("Field " + field.getName() + " wasn't added due to \"" + message + "\""); + } else { + logger.error("Reflection error while processing field: " + field.getName(), e.getCause()); } } catch (Exception ex) { //oops rip @@ -175,6 +186,25 @@ public void addFieldsFrom(Object target) { } } + @Nullable public TunableField getTunableFieldWithLabel(String label) { + TunableField labeledField = null; + + for(TunableField field : fields) { + String fieldLabel = field.reflectionField.getLabel(); + + if(fieldLabel != null && fieldLabel.equals(label)) { + labeledField = field; + break; + } + } + + return labeledField; + } + + public void setVirtualReflection(VirtualReflection reflect) { + this.reflect = reflect; + } + public void reevaluateConfigs() { for(TunableField field : fields) { field.fieldPanel.panelOptions.reevaluateConfig(); @@ -191,4 +221,4 @@ private List createTunableFieldPanels() { return panels; } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java index 8e6b3380..e1bd7809 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java @@ -26,10 +26,9 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; -import java.lang.reflect.Field; - @RegisterTunableField public class BooleanField extends TunableField { @@ -38,8 +37,8 @@ public class BooleanField extends TunableField { boolean lastVal; volatile boolean hasChanged = false; - public BooleanField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.TEXT); + public BooleanField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.TEXT); setGuiFieldAmount(0); setGuiComboBoxAmount(1); @@ -52,7 +51,6 @@ public void init() {} @Override public void update() { - hasChanged = value != lastVal; if (hasChanged) { //update values in GUI if they changed since last check @@ -60,7 +58,6 @@ public void update() { } lastVal = value; - } @Override @@ -69,14 +66,20 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - setGuiComboBoxValue(index, newValue); + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { + value = (boolean) newValue; + setPipelineFieldValue((boolean)newValue); + } + + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { + setComboBoxValueFromGui(index, newValue); } @Override - public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { + public void setComboBoxValueFromGui(int index, String newValue) throws IllegalAccessException { value = Boolean.parseBoolean(newValue); - setPipelineFieldValue(value); + setFieldValue(index, value); lastVal = value; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt index eb592693..c25a5a2c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt @@ -4,12 +4,13 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.tuner.TunableField import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField -import java.lang.reflect.Field +import io.github.deltacv.eocvsim.virtualreflect.VirtualField +import org.openftc.easyopencv.OpenCvPipeline @RegisterTunableField -class EnumField(target: Any, - reflectionField: Field, - eocvSim: EOCVSim) : TunableField>(target, reflectionField, eocvSim, AllowMode.TEXT) { +class EnumField(private val instance: OpenCvPipeline, + reflectionField: VirtualField, + eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) { val values = reflectionField.type.enumConstants @@ -39,11 +40,15 @@ class EnumField(target: Any, fieldPanel.setComboBoxSelection(0, currentValue) } - override fun setGuiComboBoxValue(index: Int, newValue: String) = setGuiFieldValue(index, newValue) + override fun setFieldValue(index: Int, newValue: Any) { + reflectionField.set(newValue) + } + + override fun setComboBoxValueFromGui(index: Int, newValue: String) = setFieldValueFromGui(index, newValue) - override fun setGuiFieldValue(index: Int, newValue: String) { + override fun setFieldValueFromGui(index: Int, newValue: String) { currentValue = java.lang.Enum.valueOf(initialValue::class.java, newValue) - reflectionField.set(target, currentValue) + setFieldValue(index, currentValue) } override fun getValue() = currentValue @@ -54,7 +59,7 @@ class EnumField(target: Any, return values } - override fun hasChanged() = reflectionField.get(target) != beforeValue + override fun hasChanged() = reflectionField.get() != beforeValue class EnumFieldAcceptor : TunableFieldAcceptor { override fun accept(clazz: Class<*>) = clazz.isEnum diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java index 2b5af34e..26f7405b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java @@ -26,18 +26,20 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.TunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; -public class NumericField extends TunableField { +abstract public class NumericField extends TunableField { protected T value; + protected T beforeValue; protected volatile boolean hasChanged = false; - public NumericField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { - super(target, reflectionField, eocvSim, allowMode); + public NumericField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, allowMode); } @Override @@ -46,14 +48,10 @@ public void init() { } @Override + @SuppressWarnings("unchecked") public void update() { if (value == null) return; - - try { - value = (T) reflectionField.get(target); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } + value = (T) reflectionField.get(); hasChanged = hasChanged(); @@ -67,10 +65,6 @@ public void updateGuiFieldValues() { fieldPanel.setFieldValue(0, value); } - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - } - @Override public T getValue() { return value; @@ -83,7 +77,10 @@ public Object getGuiFieldValue(int index) { @Override public boolean hasChanged() { - return false; + boolean hasChanged = value != beforeValue; + beforeValue = value; + return hasChanged; } -} + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java index d80895b1..ee78c291 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -40,8 +41,8 @@ public class StringField extends TunableField { volatile boolean hasChanged = false; - public StringField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.TEXT); + public StringField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.TEXT); if(initialFieldValue != null) { value = (String) initialFieldValue; @@ -72,14 +73,16 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { + setPipelineFieldValue((String)newValue); + } + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { value = newValue; - - setPipelineFieldValue(value); + setFieldValue(index, value); lastVal = value; - } @Override @@ -98,4 +101,4 @@ public boolean hasChanged() { return hasChanged; } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java index 7b7a6734..41c399a2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.opencv.core.Point; import org.openftc.easyopencv.OpenCvPipeline; @@ -36,12 +37,13 @@ public class PointField extends TunableField { Point point; - double[] lastXY = {0, 0}; + double lastX = 0; + double lastY = 0; volatile boolean hasChanged = false; - public PointField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public PointField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue != null) { Point p = (Point) initialFieldValue; @@ -51,23 +53,23 @@ public PointField(Object target, Field reflectionField, EOCVSim eocvSim) throws } setGuiFieldAmount(2); - } @Override - public void init() { } + public void init() { + reflectionField.set(point); + } @Override public void update() { - - hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; + hasChanged = point.x != lastX || point.y != lastY; if (hasChanged) { //update values in GUI if they changed since last check updateGuiFieldValues(); } - lastXY = new double[]{point.x, point.y}; - + lastX = point.x; + lastY = point.y; } @Override @@ -77,23 +79,33 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { try { - double value = Double.parseDouble(newValue); + double value = 0; + if(newValue instanceof String) { + value = Double.parseDouble((String)newValue); + } else { + value = (double)newValue; + } + if (index == 0) { point.x = value; } else { point.y = value; } - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } catch (Exception ex) { + throw new IllegalArgumentException("Parameter should be a valid number", ex); } setPipelineFieldValue(point); - lastXY = new double[]{point.x, point.y}; + lastX = point.x; + lastY = point.y; + } + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { + setFieldValue(index, point); } @Override @@ -108,7 +120,7 @@ public Object getGuiFieldValue(int index) { @Override public boolean hasChanged() { - hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; + hasChanged = point.x != lastX || point.y != lastY; return hasChanged; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt index b83cf1de..7bd0b937 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt @@ -26,13 +26,14 @@ package com.github.serivesmejia.eocvsim.tuner.field.cv import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.tuner.TunableField import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField +import io.github.deltacv.eocvsim.virtualreflect.VirtualField import org.opencv.core.Rect import org.openftc.easyopencv.OpenCvPipeline import java.lang.reflect.Field @RegisterTunableField -class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : - TunableField(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { +class RectField(instance: OpenCvPipeline, reflectionField: VirtualField, eocvSim: EOCVSim) : + TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { private var rect = arrayOf(0.0, 0.0, 0.0, 0.0) private var lastRect = arrayOf(0.0, 0.0, 0.0, 0.0) @@ -40,7 +41,7 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : @Volatile private var hasChanged = false private var initialRect = if(initialFieldValue != null) - initialFieldValue as Rect + (initialFieldValue as Rect).clone() else Rect(0, 0, 0, 0) init { @@ -48,15 +49,17 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : rect[1] = initialRect.y.toDouble() rect[2] = initialRect.width.toDouble() rect[3] = initialRect.height.toDouble() - + guiFieldAmount = 4 } - override fun init() {} + override fun init() { + reflectionField.set(initialRect) + } override fun update() { if(hasChanged()){ - initialRect = reflectionField.get(target) as Rect + initialRect = reflectionField.get() as Rect rect[0] = initialRect.x.toDouble() rect[1] = initialRect.y.toDouble() @@ -67,21 +70,16 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : } } - override fun updateGuiFieldValues() { - for((i, value) in rect.withIndex()) { - fieldPanel.setFieldValue(i, value) - } - } - - override fun setGuiFieldValue(index: Int, newValue: String) { + override fun setFieldValue(index: Int, newValue: Any) { try { - val value = newValue.toDouble() - rect[index] = value - } catch (ex: NumberFormatException) { - throw IllegalArgumentException("Parameter should be a valid numeric String") + rect[index] = if(newValue is String) + newValue.toDouble() + else (newValue as Number).toDouble() + } catch (e: Exception) { + throw IllegalArgumentException("Parameter should be a valid numeric value", e) } - initialRect.set(rect.toDoubleArray()); + initialRect.set(rect.toDoubleArray()) setPipelineFieldValue(initialRect) lastRect[0] = initialRect.x.toDouble() @@ -90,13 +88,23 @@ class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : lastRect[3] = initialRect.height.toDouble() } + override fun updateGuiFieldValues() { + for((i, value) in rect.withIndex()) { + fieldPanel.setFieldValue(i, value) + } + } + + override fun setFieldValueFromGui(index: Int, newValue: String) { + setFieldValue(index, newValue) + } + override fun getValue(): Rect = Rect(rect.toDoubleArray()) override fun getGuiFieldValue(index: Int): Any = rect[index] override fun hasChanged(): Boolean { hasChanged = rect[0] != lastRect[0] || rect[1] != lastRect[1] - || rect[2] != lastRect[2] || rect[3] != lastRect[3] + || rect[2] != lastRect[2] || rect[3] != lastRect[3] return hasChanged } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java index fa79db64..1c380131 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java @@ -27,10 +27,10 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.tuner.TunableField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.opencv.core.Scalar; import org.openftc.easyopencv.OpenCvPipeline; -import java.lang.reflect.Field; import java.util.Arrays; @RegisterTunableField @@ -39,34 +39,33 @@ public class ScalarField extends TunableField { int scalarSize; Scalar scalar; - double[] lastVal = {}; + double[] lastVal = {0, 0, 0, 0}; volatile boolean hasChanged = false; - public ScalarField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public ScalarField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue == null) { scalar = new Scalar(0, 0, 0); } else { - scalar = (Scalar) initialFieldValue; + scalar = ((Scalar) initialFieldValue).clone(); } + scalarSize = scalar.val.length; - setGuiFieldAmount(scalarSize); + setGuiFieldAmount(4); setRecommendedPanelMode(TunableFieldPanel.Mode.SLIDERS); } @Override - public void init() { } + public void init() { + reflectionField.set(scalar); + } @Override public void update() { - try { - scalar = (Scalar) reflectionField.get(target); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } + scalar = (Scalar) reflectionField.get(); hasChanged = !Arrays.equals(scalar.val, lastVal); @@ -74,7 +73,10 @@ public void update() { updateGuiFieldValues(); } - lastVal = scalar.val.clone(); + lastVal[0] = scalar.val[0]; + lastVal[1] = scalar.val[1]; + lastVal[2] = scalar.val[2]; + lastVal[3] = scalar.val[3]; } @Override @@ -85,16 +87,28 @@ public void updateGuiFieldValues() { } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValue(int index, Object newValue) throws IllegalAccessException { try { - scalar.val[index] = Double.parseDouble(newValue); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); + if(newValue instanceof String) { + scalar.val[index] = Double.parseDouble((String) newValue); + } else { + scalar.val[index] = (double)newValue; + } + } catch (Exception ex) { + throw new IllegalArgumentException("Parameter should be a valid number", ex); } setPipelineFieldValue(scalar); - lastVal = scalar.val.clone(); + lastVal[0] = scalar.val[0]; + lastVal[1] = scalar.val[1]; + lastVal[2] = scalar.val[2]; + lastVal[3] = scalar.val[3]; + } + + @Override + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { + setFieldValue(index, newValue); } @Override diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java index 572bedbd..d7af5eda 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,34 +34,31 @@ @RegisterTunableField public class DoubleField extends NumericField { - private double beforeValue; - - public DoubleField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public DoubleField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (double) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = Double.valueOf(newValue); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; - } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).doubleValue(); + } else { + this.value = (double)value; + } + setPipelineFieldValue(this.value); } - -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java index 321e01dd..eb6928b7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,33 +34,31 @@ @RegisterTunableField public class FloatField extends NumericField { - protected float beforeValue; - - public FloatField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public FloatField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (float) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = Float.parseFloat(newValue); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; - } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).floatValue(); + } else { + this.value = (float)value; + } + setPipelineFieldValue(this.value); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java index 08ff9bf2..a4cd5b96 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,31 +34,31 @@ @RegisterTunableField public class IntegerField extends NumericField { - protected int beforeValue; - - public IntegerField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + public IntegerField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (int) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = (int) Math.round(Double.parseDouble(newValue)); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).intValue(); + } else { + this.value = (int)value; + } + setPipelineFieldValue(this.value); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java index 6645a505..839a7b42 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java @@ -26,6 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.tuner.field.NumericField; import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import io.github.deltacv.eocvsim.virtualreflect.VirtualField; import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; @@ -33,31 +34,31 @@ @RegisterTunableField public class LongField extends NumericField { - private long beforeValue; - - public LongField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + public LongField(OpenCvPipeline instance, VirtualField reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (long) initialFieldValue; } @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + public void setFieldValueFromGui(int index, String newValue) throws IllegalAccessException { try { value = Math.round(Double.parseDouble(newValue)); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Parameter should be a valid numeric String"); } - setPipelineFieldValue(value); - + setFieldValue(index, value); beforeValue = value; } @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; + public void setFieldValue(int index, Object value) throws IllegalAccessException { + if(value instanceof Number) { + this.value = ((Number) value).longValue(); + } else { + this.value = (long)value; + } + setPipelineFieldValue(this.value); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/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'