From 27642c93686ab4ea5fdb52fa405e6ab06ead0ee7 Mon Sep 17 00:00:00 2001 From: Alessandro Date: Mon, 20 Feb 2023 09:54:42 -0700 Subject: [PATCH 01/46] fixed video source serialization + a gui typo --- EOCV-Sim/build.gradle | 2 +- .../github/serivesmejia/eocvsim/EOCVSim.kt | 36 +++++++------ .../com/github/serivesmejia/eocvsim/Main.kt | 51 ++++++++++++------- .../eocvsim/gui/dialog/Configuration.java | 2 +- .../eocvsim/input/InputSource.java | 15 +++--- .../eocvsim/input/InputSourceManager.java | 6 +-- .../eocvsim/input/source/VideoSource.java | 23 ++++----- .../ftc/teamcode/SimpleThresholdPipeline.java | 3 +- 8 files changed, 81 insertions(+), 57 deletions(-) diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 1284e5e3..edc856b2 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -96,4 +96,4 @@ task(writeBuildClassJava) { "}" } -build.dependsOn writeBuildClassJava +build.dependsOn writeBuildClassJava \ No newline at end of file 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 a6b5f057..df84a8e8 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 @@ -60,6 +60,7 @@ class EOCVSim(val params: Parameters = Parameters()) { const val VERSION = Build.versionString const val DEFAULT_EOCV_WIDTH = 320 const val DEFAULT_EOCV_HEIGHT = 240 + @JvmField val DEFAULT_EOCV_SIZE = Size(DEFAULT_EOCV_WIDTH.toDouble(), DEFAULT_EOCV_HEIGHT.toDouble()) @@ -87,7 +88,7 @@ class EOCVSim(val params: Parameters = Parameters()) { logger.info("Successfully loaded the OpenCV native lib from specified path") return - } catch(ex: Throwable) { + } catch (ex: Throwable) { logger.error("Failure loading the OpenCV native lib from specified path", ex) logger.info("Retrying with loadLocally...") } @@ -117,12 +118,16 @@ class EOCVSim(val params: Parameters = Parameters()) { @JvmField val configManager = ConfigManager() + @JvmField val inputSourceManager = InputSourceManager(this) + @JvmField val pipelineManager = PipelineManager(this) + @JvmField val tunerManager = TunerManager(this) + @JvmField val workspaceManager = WorkspaceManager(this) @@ -147,10 +152,9 @@ class EOCVSim(val params: Parameters = Parameters()) { fun init() { eocvSimThread = Thread.currentThread() - if(!EOCVSimFolder.couldLock) { + if (!EOCVSimFolder.couldLock) { logger.error( - "Couldn't finally claim lock file in \"${EOCVSimFolder.absolutePath}\"! " + - "Is the folder opened by another EOCV-Sim instance?" + "Couldn't finally claim lock file in \"${EOCVSimFolder.absolutePath}\"! " + "Is the folder opened by another EOCV-Sim instance?" ) logger.error("Unable to continue with the execution, the sim will exit now.") @@ -168,7 +172,7 @@ class EOCVSim(val params: Parameters = Parameters()) { //loading native lib only once in the app runtime loadOpenCvLib(params.opencvNativeLibrary) - if(!hasScanned) { + if (!hasScanned) { classpathScan.asyncScan() hasScanned = true } @@ -188,7 +192,10 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.asyncPleaseWaitDialog( "Current pipeline took too long to ${pipelineManager.lastPipelineAction}", "Falling back to DefaultPipeline", - "Close", Dimension(310, 150), true, true + "Close", + Dimension(310, 150), + true, + true ) } @@ -223,7 +230,7 @@ class EOCVSim(val params: Parameters = Parameters()) { try { pipelineManager.update( - if(inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) { + if (inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) { inputSourceManager.lastMatFromSource } else null ) @@ -233,7 +240,8 @@ class EOCVSim(val params: Parameters = Parameters()) { "To avoid further issues, EOCV-Sim will exit now.", "Ok", Dimension(450, 150), - true, true + true, + true ).onCancel { destroy(DestroyReason.CRASH) //destroy eocv sim when pressing "exit" } @@ -260,14 +268,14 @@ class EOCVSim(val params: Parameters = Parameters()) { fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble() try { fpsLimiter.sync() - } catch(e: InterruptedException) { + } catch (e: InterruptedException) { break } } logger.warn("Main thread interrupted ($hexCode)") - if(isRestarting) { + if (isRestarting) { isRestarting = false EOCVSim(params).init() } @@ -289,8 +297,7 @@ class EOCVSim(val params: Parameters = Parameters()) { eocvSimThread.interrupt() - if(reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) - jvmMainThread.interrupt() + if (reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) jvmMainThread.interrupt() } fun destroy() { @@ -332,8 +339,7 @@ class EOCVSim(val params: Parameters = Parameters()) { logger.info("Recording session stopped") DialogFactory.createFileChooser( - visualizer.frame, - DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, FileFilters.recordedVideoFilter + visualizer.frame, DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, FileFilters.recordedVideoFilter ).addCloseListener { _: Int, file: File?, selectedFileFilter: FileFilter? -> onMainUpdate.doOnce { if (file != null) { @@ -402,4 +408,4 @@ class EOCVSim(val params: Parameters = Parameters()) { var opencvNativeLibrary: File? = null } -} +} \ No newline at end of file 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 0ec06269..d10ca12e 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 @@ -1,8 +1,8 @@ @file:JvmName("Main") + package com.github.serivesmejia.eocvsim import com.github.serivesmejia.eocvsim.pipeline.PipelineSource -import com.github.serivesmejia.eocvsim.util.loggerForThis import picocli.CommandLine import java.io.File import java.nio.file.Paths @@ -22,31 +22,48 @@ fun main(args: Array) { @CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString]) class EOCVSimCommandInterface : Runnable { - @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"]) - @JvmField var workspacePath: String? = null - - @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]) - @JvmField var initialPipeline: String? = null - @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]) - @JvmField var initialPipelineSource = PipelineSource.CLASSPATH - - @CommandLine.Option(names = ["-o", "--opencvpath"], description = ["Specifies an alternative path for the OpenCV native to be loaded at runtime"]) - @JvmField var opencvNativePath: String? = null + @CommandLine.Option( + names = ["-w", "--workspace"], + description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"] + ) + @JvmField + var workspacePath: String? = null + + @CommandLine.Option( + names = ["-p", "--pipeline"], + description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"] + ) + @JvmField + var initialPipeline: String? = null + + @CommandLine.Option( + names = ["-s", "--source"], + description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"] + ) + @JvmField + var initialPipelineSource = PipelineSource.CLASSPATH + + @CommandLine.Option( + names = ["-o", "--opencvpath"], + description = ["Specifies an alternative path for the OpenCV native to be loaded at runtime"] + ) + @JvmField + var opencvNativePath: String? = null override fun run() { val parameters = EOCVSim.Parameters() - if(workspacePath != null) { + if (workspacePath != null) { parameters.initialWorkspace = checkPath("Workspace", workspacePath!!, true) } - if(initialPipeline != null) { + if (initialPipeline != null) { parameters.initialPipelineName = initialPipeline parameters.initialPipelineSource = initialPipelineSource } - if(opencvNativePath != null) { + if (opencvNativePath != null) { parameters.opencvNativeLibrary = checkPath("OpenCV Native", opencvNativePath!!, false) } @@ -56,16 +73,16 @@ class EOCVSimCommandInterface : Runnable { private fun checkPath(parameter: String, path: String, shouldBeDirectory: Boolean): File { var file = File(path) - if(!file.exists()) { + if (!file.exists()) { file = Paths.get(System.getProperty("user.dir"), path).toFile() - if(!file.exists()) { + if (!file.exists()) { System.err.println("$parameter path is not valid, it doesn't exist (tried in \"$path\" and \"${file.absolutePath})\"") exitProcess(1) } } - if(shouldBeDirectory && !file.isDirectory) { + if (shouldBeDirectory && !file.isDirectory) { System.err.println("$parameter path is not valid, the specified path is not a folder") exitProcess(1) } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java index 58f6b473..bd7cf767 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java @@ -94,7 +94,7 @@ private void initConfiguration() { themePanel.add(this.themeComboBox); uiPanel.add(themePanel); - tabbedPane.addTab("Inteface", uiPanel); + tabbedPane.addTab("Interface", uiPanel); /* INPUT SOURCES TAB diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java index a1a384a7..95ff57a8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java @@ -30,20 +30,23 @@ public abstract class InputSource implements Comparable { - public transient boolean isDefault = false; - public transient EOCVSim eocvSim = null; + public transient boolean isDefault; + public transient EOCVSim eocvSim; protected transient String name = ""; - protected transient boolean isPaused = false; - private transient boolean beforeIsPaused = false; + protected transient boolean isPaused; + private transient boolean beforeIsPaused; - protected long createdOn = -1L; + protected transient long createdOn = -1L; public abstract boolean init(); + public abstract void reset(); + public abstract void close(); public abstract void onPause(); + public abstract void onResume(); public Mat update() { @@ -51,7 +54,7 @@ public Mat update() { } public final InputSource cloneSource() { - InputSource source = internalCloneSource(); + final InputSource source = internalCloneSource(); source.createdOn = createdOn; return source; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index d025f4b3..47499b45 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.Visualizer; import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; import com.github.serivesmejia.eocvsim.input.source.ImageSource; +import com.github.serivesmejia.eocvsim.input.source.VideoSource; import com.github.serivesmejia.eocvsim.pipeline.PipelineManager; import com.github.serivesmejia.eocvsim.util.SysUtil; import org.opencv.core.Mat; @@ -79,13 +80,13 @@ public void init() { inputSourceLoader.loadInputSourcesFromFile(); for (Map.Entry entry : inputSourceLoader.loadedInputSources.entrySet()) { + logger.info("loaded input source " + entry.getKey()); addInputSource(entry.getKey(), entry.getValue()); } } private void createDefaultImgInputSource(String resourcePath, String fileName, String sourceName, Size imgSize) { try { - InputStream is = InputSource.class.getResourceAsStream(resourcePath); File f = SysUtil.copyFileIsTemp(is, fileName, true).file; @@ -94,7 +95,6 @@ private void createDefaultImgInputSource(String resourcePath, String fileName, S src.createdOn = sources.size(); addInputSource(sourceName, src); - } catch (IOException e) { e.printStackTrace(); } @@ -322,4 +322,4 @@ public InputSource[] getSortedInputSources() { return sources.toArray(new InputSource[0]); } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index aad57415..b5dab77f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -23,7 +23,6 @@ package com.github.serivesmejia.eocvsim.input.source; -import com.github.serivesmejia.eocvsim.gui.Visualizer; import com.github.serivesmejia.eocvsim.input.InputSource; import com.github.serivesmejia.eocvsim.util.FileFilters; import com.google.gson.annotations.Expose; @@ -37,19 +36,18 @@ import org.slf4j.LoggerFactory; import javax.swing.filechooser.FileFilter; -import java.util.Objects; public class VideoSource extends InputSource { @Expose private String videoPath = null; - private transient VideoCapture video = null; + private transient VideoCapture video; - private transient MatRecycler.RecyclableMat lastFramePaused = null; - private transient MatRecycler.RecyclableMat lastFrame = null; + private transient MatRecycler.RecyclableMat lastFramePaused; + private transient MatRecycler.RecyclableMat lastFrame; - private transient boolean initialized = false; + private transient boolean initialized; @Expose private volatile Size size; @@ -60,9 +58,10 @@ public class VideoSource extends InputSource { private transient long capTimeNanos = 0; - Logger logger = LoggerFactory.getLogger(getClass()); + private transient Logger logger = LoggerFactory.getLogger(getClass()); - public VideoSource() {} + public VideoSource() { + } public VideoSource(String videoPath, Size size) { this.videoPath = videoPath; @@ -109,9 +108,9 @@ public void reset() { if (video != null && video.isOpened()) video.release(); - if(lastFrame != null && lastFrame.isCheckedOut()) + if (lastFrame != null && lastFrame.isCheckedOut()) lastFrame.returnMat(); - if(lastFramePaused != null && lastFramePaused.isCheckedOut()) + if (lastFramePaused != null && lastFramePaused.isCheckedOut()) lastFramePaused.returnMat(); matRecycler.releaseAll(); @@ -124,8 +123,8 @@ public void reset() { @Override public void close() { - if(video != null && video.isOpened()) video.release(); - if(lastFrame != null) lastFrame.returnMat(); + if (video != null && video.isOpened()) video.release(); + if (lastFrame != null) lastFrame.returnMat(); if (lastFramePaused != null) { lastFramePaused.returnMat(); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java index fea2744e..de6bf360 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java @@ -31,7 +31,6 @@ import org.opencv.imgproc.Imgproc; import org.openftc.easyopencv.OpenCvPipeline; -@Disabled public class SimpleThresholdPipeline extends OpenCvPipeline { /* @@ -170,4 +169,4 @@ public Mat processFrame(Mat input) { return maskedInputMat; } -} +} \ No newline at end of file From 5a8f87a7cb6e389470e0e65fcfc3bb1c327db2b9 Mon Sep 17 00:00:00 2001 From: Alessandro Marcolini <93690992+amarcolini@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:09:13 -0800 Subject: [PATCH 02/46] Update InputSourceManager.java --- .../github/serivesmejia/eocvsim/input/InputSourceManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 47499b45..f3c56159 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -80,7 +80,7 @@ public void init() { inputSourceLoader.loadInputSourcesFromFile(); for (Map.Entry entry : inputSourceLoader.loadedInputSources.entrySet()) { - logger.info("loaded input source " + entry.getKey()); + logger.info("Loaded input source " + entry.getKey()); addInputSource(entry.getKey(), entry.getValue()); } } From d8c65a3063391831c0ca536de7728d67c54a0289 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Tue, 18 Apr 2023 20:53:14 -0600 Subject: [PATCH 03/46] Fix video looping --- .../serivesmejia/eocvsim/input/source/VideoSource.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index b5dab77f..f5921501 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -157,7 +157,9 @@ public Mat update() { //in next update if (newFrame.empty()) { newFrame.returnMat(); - video.set(Videoio.CAP_PROP_POS_FRAMES, 0); + + this.reset() + this.init(); return lastFrame; } @@ -219,4 +221,4 @@ public String toString() { return "VideoSource(" + videoPath + ", " + (size != null ? size.toString() : "null") + ")"; } -} \ No newline at end of file +} From e6ffe2924338ac7c1faa807fdc8448524a12d747 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Tue, 18 Apr 2023 21:18:45 -0600 Subject: [PATCH 04/46] add semicolon --- .../github/serivesmejia/eocvsim/input/source/VideoSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index f5921501..300b8e61 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -158,7 +158,7 @@ public Mat update() { if (newFrame.empty()) { newFrame.returnMat(); - this.reset() + this.reset(); this.init(); return lastFrame; } From 063884904168c61fca39f242fd14d9dc7f7640f7 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sun, 30 Jul 2023 19:02:36 -0600 Subject: [PATCH 05/46] Fix File.plus operator not adding separator --- .../com/github/serivesmejia/eocvsim/util/extension/FileExt.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt index c4f8e8a5..a5108883 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt @@ -26,5 +26,5 @@ package com.github.serivesmejia.eocvsim.util.extension import java.io.File operator fun File.plus(str: String): File { - return File(this.absolutePath + str) -} \ No newline at end of file + return File(this.absolutePath, str) +} From d2547b98f37b32bede45e2eb07aa2334134ea93b Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sun, 30 Jul 2023 19:45:10 -0600 Subject: [PATCH 06/46] Update apriltags to 1.2.1, adding M1 support --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 785af733..70b3b15e 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { slf4j_version = "1.7.32" log4j_version = "2.17.1" opencv_version = "4.5.5-1" - apriltag_plugin_version = "1.2.0" + apriltag_plugin_version = "1.2.1" classgraph_version = "4.8.108" opencsv_version = "5.5.2" From 14e00ff485be15d0b0923f3ecfb5844313a89f70 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sun, 30 Jul 2023 19:49:41 -0600 Subject: [PATCH 07/46] Update version to 3.4.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 70b3b15e..e582e633 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ buildscript { allprojects { group 'com.github.deltacv' - version '3.4.3' + version '3.4.4' ext { standardVersion = version From 38eca6761fc28272bd0610f0a8d70f15ff4c0dd3 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 5 Aug 2023 18:06:42 -0600 Subject: [PATCH 08/46] Start implementing new VisionPortal API --- Vision/build.gradle | 31 ++ .../ftc/vision/VisionPortal.java | 482 +++++++++++++++++ .../ftc/vision/VisionPortalImpl.java | 501 +++++++++++++++++ .../ftc/vision/VisionProcessor.java | 39 ++ .../ftc/vision/VisionProcessorInternal.java | 49 ++ .../apriltag/AprilTagCanvasAnnotator.java | 290 ++++++++++ .../vision/apriltag/AprilTagDetection.java | 85 +++ .../vision/apriltag/AprilTagGameDatabase.java | 90 ++++ .../ftc/vision/apriltag/AprilTagLibrary.java | 189 +++++++ .../ftc/vision/apriltag/AprilTagMetadata.java | 84 +++ .../ftc/vision/apriltag/AprilTagPoseFtc.java | 102 ++++ .../ftc/vision/apriltag/AprilTagPoseRaw.java | 59 ++ .../vision/apriltag/AprilTagProcessor.java | 277 ++++++++++ .../apriltag/AprilTagProcessorImpl.java | 504 ++++++++++++++++++ 14 files changed, 2782 insertions(+) create mode 100644 Vision/build.gradle create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java diff --git a/Vision/build.gradle b/Vision/build.gradle new file mode 100644 index 00000000..ca0abd24 --- /dev/null +++ b/Vision/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java' + id 'kotlin' + id 'maven-publish' +} + +apply from: '../build.common.gradle' + +task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = "sources" +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + } + } +} + +dependencies { + implementation project(':Common') + + implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" + + api "org.openpnp:opencv:$opencv_version" + implementation "org.slf4j:slf4j-api:$slf4j_version" + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java new file mode 100644 index 00000000..0d2a0544 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2023 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; + +import android.util.Size; + +import java.util.ArrayList; +import java.util.List; + +import org.firstinspires.ftc.robotcore.external.ClassFactory; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.openftc.easyopencv.OpenCvCameraFactory; +import org.openftc.easyopencv.OpenCvWebcam; + +public abstract class VisionPortal +{ + public static final int DEFAULT_VIEW_CONTAINER_ID = AppUtil.getDefContext().getResources().getIdentifier("cameraMonitorViewId", "id", AppUtil.getDefContext().getPackageName()); + + /** + * StreamFormat is only applicable if using a webcam + */ + public enum StreamFormat + { + /** The only format that was supported historically; it is uncompressed but + * chroma subsampled and uses lots of bandwidth - this limits frame rate + * at higher resolutions and also limits the ability to use two cameras + * on the same bus to lower resolutions + */ + YUY2(OpenCvWebcam.StreamFormat.YUY2), + + /** Compressed motion JPEG stream format; allows for higher resolutions at + * full frame rate, and better ability to use two cameras on the same bus. + * Requires extra CPU time to run decompression routine. + */ + MJPEG(OpenCvWebcam.StreamFormat.MJPEG); + + final OpenCvWebcam.StreamFormat eocvStreamFormat; + + StreamFormat(OpenCvWebcam.StreamFormat eocvStreamFormat) + { + this.eocvStreamFormat = eocvStreamFormat; + } + } + + /** + * If you are using multiple vision portals with live previews concurrently, + * you need to split up the screen to make room for both portals + */ + public enum MultiPortalLayout + { + /** + * Divides the screen vertically + */ + VERTICAL(OpenCvCameraFactory.ViewportSplitMethod.VERTICALLY), + + /** + * Divides the screen horizontally + */ + HORIZONTAL(OpenCvCameraFactory.ViewportSplitMethod.HORIZONTALLY); + + private final OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod; + + MultiPortalLayout(OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod) + { + this.viewportSplitMethod = viewportSplitMethod; + } + } + + /** + * Split up the screen for using multiple vision portals with live views simultaneously + * @param numPortals the number of portals to create space for on the screen + * @param mpl the methodology for laying out the multiple live views on the screen + * @return an array of view IDs, whose elements may be passed to {@link Builder#setCameraMonitorViewId(int)} + */ + public static int[] makeMultiPortalView(int numPortals, MultiPortalLayout mpl) + { + return OpenCvCameraFactory.getInstance().splitLayoutForMultipleViewports( + DEFAULT_VIEW_CONTAINER_ID, numPortals, mpl.viewportSplitMethod + ); + } + + /** + * Create a VisionPortal for an internal camera using default configuration parameters, and + * skipping the use of the {@link Builder} pattern. + * @param cameraDirection the internal camera to use + * @param processors all the processors you want to inject into the portal + * @return a configured, ready to use VisionPortal + */ + public static VisionPortal easyCreateWithDefaults(BuiltinCameraDirection cameraDirection, VisionProcessor... processors) + { + return new Builder() + .setCamera(cameraDirection) + .addProcessors(processors) + .build(); + } + + /** + * Create a VisionPortal for a webcam using default configuration parameters, and + * skipping the use of the {@link Builder} pattern. + * @param processors all the processors you want to inject into the portal + * @return a configured, ready to use VisionPortal + */ + public static VisionPortal easyCreateWithDefaults(CameraName cameraName, VisionProcessor... processors) + { + return new Builder() + .setCamera(cameraName) + .addProcessors(processors) + .build(); + } + + public static class Builder + { + // STATIC ! + private static final ArrayList attachedProcessors = new ArrayList<>(); + + private CameraName camera; + private int cameraMonitorViewId = DEFAULT_VIEW_CONTAINER_ID; // 0 == none + private boolean autoStopLiveView = true; + private Size cameraResolution = new Size(640, 480); + private StreamFormat streamFormat = null; + private StreamFormat STREAM_FORMAT_DEFAULT = StreamFormat.YUY2; + private final List processors = new ArrayList<>(); + + /** + * Configure the portal to use a webcam + * @param camera the WebcamName of the camera to use + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCamera(CameraName camera) + { + this.camera = camera; + return this; + } + + /** + * Configure the portal to use an internal camera + * @param cameraDirection the internal camera to use + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCamera(BuiltinCameraDirection cameraDirection) + { + this.camera = ClassFactory.getInstance().getCameraManager().nameFromCameraDirection(cameraDirection); + return this; + } + + /** + * Configure the vision portal to stream from the camera in a certain image format + * THIS APPLIES TO WEBCAMS ONLY! + * @param streamFormat the desired streaming format + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setStreamFormat(StreamFormat streamFormat) + { + this.streamFormat = streamFormat; + return this; + } + + /** + * Configure the vision portal to use (or not to use) a live camera preview + * @param enableLiveView whether or not to use a live preview + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder enableCameraMonitoring(boolean enableLiveView) + { + int viewId; + if (enableLiveView) + { + viewId = DEFAULT_VIEW_CONTAINER_ID; + } + else + { + viewId = 0; + } + return setCameraMonitorViewId(viewId); + } + + /** + * Configure whether the portal should automatically pause the live camera + * view if all attached processors are disabled; this can save computational resources + * @param autoPause whether to enable this feature or not + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setAutoStopLiveView(boolean autoPause) + { + this.autoStopLiveView = autoPause; + return this; + } + + /** + * A more advanced version of {@link #enableCameraMonitoring(boolean)}; allows you + * to specify a specific view ID to use as a container, rather than just using the default one + * @param cameraMonitorViewId view ID of container for live view + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCameraMonitorViewId(int cameraMonitorViewId) + { + this.cameraMonitorViewId = cameraMonitorViewId; + return this; + } + + /** + * Specify the resolution in which to stream images from the camera. To find out what resolutions + * your camera supports, simply call this with some random numbers (e.g. new Size(4634, 11115)) + * and the error message will provide a list of supported resolutions. + * @param cameraResolution the resolution in which to stream images from the camera + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCameraResolution(Size cameraResolution) + { + this.cameraResolution = cameraResolution; + return this; + } + + /** + * Send a {@link VisionProcessor} into this portal to allow it to process camera frames. + * @param processor the processor to attach + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if the specified processor is already inside another portal + */ + public Builder addProcessor(VisionProcessor processor) + { + synchronized (attachedProcessors) + { + if (attachedProcessors.contains(processor)) + { + throw new RuntimeException("This VisionProcessor has already been attached to a VisionPortal, either a different one or perhaps even this same portal."); + } + else + { + attachedProcessors.add(processor); + } + } + + processors.add(processor); + return this; + } + + /** + * Send multiple {@link VisionProcessor}s into this portal to allow them to process camera frames. + * @param processors the processors to attach + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if the specified processor is already inside another portal + */ + public Builder addProcessors(VisionProcessor... processors) + { + for (VisionProcessor p : processors) + { + addProcessor(p); + } + + return this; + } + + /** + * Actually create the {@link VisionPortal} i.e. spool up the camera and live view + * and begin sending image data to any attached {@link VisionProcessor}s + * @return a configured, ready to use portal + * @throws RuntimeException if you didn't specify what camera to use + * @throws IllegalStateException if you tried to set the stream format when not using a webcam + */ + public VisionPortal build() + { + if (camera == null) + { + throw new RuntimeException("You can't build a vision portal without setting a camera!"); + } + + if (streamFormat != null) + { + if (!camera.isWebcam() && !camera.isSwitchable()) + { + throw new IllegalStateException("setStreamFormat() may only be used with a webcam"); + } + } + else + { + // Only used with webcams, will be ignored for internal camera + streamFormat = STREAM_FORMAT_DEFAULT; + } + + return new VisionPortalImpl( + camera, cameraMonitorViewId, autoStopLiveView, cameraResolution, streamFormat, + processors.toArray(new VisionProcessor[processors.size()])); + } + } + + /** + * Enable or disable a {@link VisionProcessor} that is attached to this portal. + * Disabled processors are not passed new image data and do not consume any computational + * resources. Of course, they also don't give you any useful data when disabled. + * This takes effect immediately (on the next frame interval) + * @param processor the processor to enable or disable + * @param enabled should it be enabled or disabled? + * @throws IllegalArgumentException if the processor specified isn't inside this portal + */ + public abstract void setProcessorEnabled(VisionProcessor processor, boolean enabled); + + /** + * Queries whether a given processor is enabled + * @param processor the processor in question + * @return whether the processor in question is enabled + * @throws IllegalArgumentException if the processor specified isn't inside this portal + */ + public abstract boolean getProcessorEnabled(VisionProcessor processor); + + /** + * The various states that the camera may be in at any given time + */ + public enum CameraState + { + /** + * The camera device handle is being opened + */ + OPENING_CAMERA_DEVICE, + + /** + * The camera device handle has been opened and the camera + * is now ready to start streaming + */ + CAMERA_DEVICE_READY, + + /** + * The camera stream is starting + */ + STARTING_STREAM, + + /** + * The camera streaming session is in flight and providing image data + * to any attached {@link VisionProcessor}s + */ + STREAMING, + + /** + * The camera stream is being shut down + */ + STOPPING_STREAM, + + /** + * The camera device handle is being closed + */ + CLOSING_CAMERA_DEVICE, + + /** + * The camera device handle has been closed; you must create a new + * portal if you wish to use the camera again + */ + CAMERA_DEVICE_CLOSED, + + /** + * The camera was having a bad day and refused to cooperate with configuration for either + * opening the device handle or starting the streaming session + */ + ERROR + } + + /** + * Query the current state of the camera (e.g. is a streaming session in flight?) + * @return the current state of the camera + */ + public abstract CameraState getCameraState(); + + public abstract void saveNextFrameRaw(String filename); + + /** + * Stop the streaming session. This is an asynchronous call which does not take effect + * immediately. You may use {@link #getCameraState()} to monitor for when this command + * has taken effect. If you call {@link #resumeStreaming()} before the operation is complete, + * it will SYNCHRONOUSLY await completion of the stop command + * + * Stopping the streaming session is a good way to save computational resources if there may + * be long (e.g. 10+ second) periods of match play in which vision processing is not required. + * When streaming is stopped, no new image data is acquired from the camera and any attached + * {@link VisionProcessor}s will lie dormant until such time as {@link #resumeStreaming()} is called. + * + * Stopping and starting the stream can take a second or two, and thus is not advised for use + * cases where instantaneously enabling/disabling vision processing is required. + */ + public abstract void stopStreaming(); + + /** + * Resume the streaming session if previously stopped by {@link #stopStreaming()}. This is + * an asynchronous call which does not take effect immediately. If you call {@link #stopStreaming()} + * before the operation is complete, it will SYNCHRONOUSLY await completion of the resume command. + * + * See notes about use case on {@link #stopStreaming()} + */ + public abstract void resumeStreaming(); + + /** + * Temporarily stop the live view on the RC screen. This DOES NOT affect the ability to get + * a camera frame on the Driver Station's "Camera Stream" feature. + * + * This has no effect if you didn't set up a live view. + * + * Stopping the live view is recommended during competition to save CPU resources when + * a live view is not required for debugging purposes. + */ + public abstract void stopLiveView(); + + /** + * Start the live view again, if it was previously stopped with {@link #stopLiveView()} + * + * This has no effect if you didn't set up a live view. + */ + public abstract void resumeLiveView(); + + /** + * Get the current rate at which frames are passing through the vision portal + * (and all processors therein) per second - frames per second + * @return the current vision frame rate in frames per second + */ + public abstract float getFps(); + + /** + * Get a camera control handle + * ONLY APPLICABLE TO WEBCAMS + * @param controlType the type of control to get + * @return the requested control + * @throws UnsupportedOperationException if you are not using a webcam + */ + public abstract T getCameraControl(Class controlType); + + /** + * Switches the active camera to the indicated camera. + * ONLY APPLICABLE IF USING A SWITCHABLE WEBCAM + * @param webcamName the name of the to-be-activated camera + * @throws UnsupportedOperationException if you are not using a switchable webcam + */ + public abstract void setActiveCamera(WebcamName webcamName); + + /** + * Returns the name of the currently active camera + * ONLY APPLIES IF USING A SWITCHABLE WEBCAM + * @return the name of the currently active camera + * @throws UnsupportedOperationException if you are not using a switchable webcam + */ + public abstract WebcamName getActiveCamera(); + + /** + * Teardown everything prior to the end of the OpMode (perhaps to save resources) at which point + * it will be torn down automagically anyway. + * + * This will stop all vision related processing, shut down the camera, and remove the live view. + * A closed portal may not be re-opened: if you wish to use the camera again, you must make a new portal + */ + public abstract void close(); +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java new file mode 100644 index 00000000..6cbd81a4 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2023 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; + +import android.graphics.Canvas; +import android.util.Size; + +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationHelper; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationIdentity; +import org.firstinspires.ftc.robotcore.internal.camera.delegating.SwitchableCameraName; +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvCamera; +import org.openftc.easyopencv.OpenCvCameraFactory; +import org.openftc.easyopencv.OpenCvCameraRotation; +import org.openftc.easyopencv.OpenCvInternalCamera; +import org.openftc.easyopencv.OpenCvSwitchableWebcam; +import org.openftc.easyopencv.OpenCvWebcam; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +public class VisionPortalImpl extends VisionPortal +{ + protected OpenCvCamera camera; + protected volatile CameraState cameraState = CameraState.CAMERA_DEVICE_CLOSED; + protected VisionProcessor[] processors; + protected volatile boolean[] processorsEnabled; + protected volatile CameraCalibration calibration; + protected final boolean autoPauseCameraMonitor; + protected final Object userStateMtx = new Object(); + protected final Size cameraResolution; + protected final StreamFormat webcamStreamFormat; + protected static final OpenCvCameraRotation CAMERA_ROTATION = OpenCvCameraRotation.SENSOR_NATIVE; + protected String captureNextFrame; + protected final Object captureFrameMtx = new Object(); + + public VisionPortalImpl(CameraName camera, int cameraMonitorViewId, boolean autoPauseCameraMonitor, Size cameraResolution, StreamFormat webcamStreamFormat, VisionProcessor[] processors) + { + this.processors = processors; + this.cameraResolution = cameraResolution; + this.webcamStreamFormat = webcamStreamFormat; + processorsEnabled = new boolean[processors.length]; + + for (int i = 0; i < processors.length; i++) + { + processorsEnabled[i] = true; + } + + this.autoPauseCameraMonitor = autoPauseCameraMonitor; + + createCamera(camera, cameraMonitorViewId); + startCamera(); + } + + protected void startCamera() + { + if (camera == null) + { + throw new IllegalStateException("This should never happen"); + } + + if (cameraResolution == null) // was the user a silly silly + { + throw new IllegalArgumentException("parameters.cameraResolution == null"); + } + + camera.setViewportRenderer(OpenCvCamera.ViewportRenderer.NATIVE_VIEW); + + if(!(camera instanceof OpenCvWebcam)) + { + camera.setViewportRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW); + } + + cameraState = CameraState.OPENING_CAMERA_DEVICE; + camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() + { + @Override + public void onOpened() + { + cameraState = CameraState.CAMERA_DEVICE_READY; + cameraState = CameraState.STARTING_STREAM; + + if (camera instanceof OpenCvWebcam) + { + ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); + } + else + { + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); + } + + if (camera instanceof OpenCvWebcam) + { + CameraCalibrationIdentity identity = ((OpenCvWebcam) camera).getCalibrationIdentity(); + + if (identity != null) + { + calibration = CameraCalibrationHelper.getInstance().getCalibration(identity, cameraResolution.getWidth(), cameraResolution.getHeight()); + } + } + + camera.setPipeline(new ProcessingPipeline()); + cameraState = CameraState.STREAMING; + } + + @Override + public void onError(int errorCode) + { + cameraState = CameraState.ERROR; + RobotLog.ee("VisionPortalImpl", "Camera opening failed."); + } + }); + } + + protected void createCamera(CameraName cameraName, int cameraMonitorViewId) + { + if (cameraName == null) // was the user a silly silly + { + throw new IllegalArgumentException("parameters.camera == null"); + } + else if (cameraName.isWebcam()) // Webcams + { + if (cameraMonitorViewId != 0) + { + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); + } + else + { + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName); + } + } + else if (cameraName.isCameraDirection()) // Internal cameras + { + if (cameraMonitorViewId != 0) + { + camera = OpenCvCameraFactory.getInstance().createInternalCamera( + ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT, cameraMonitorViewId); + } + else + { + camera = OpenCvCameraFactory.getInstance().createInternalCamera( + ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT); + } + } + else if (cameraName.isSwitchable()) + { + SwitchableCameraName switchableCameraName = (SwitchableCameraName) cameraName; + if (switchableCameraName.allMembersAreWebcams()) { + CameraName[] members = switchableCameraName.getMembers(); + WebcamName[] webcamNames = new WebcamName[members.length]; + for (int i = 0; i < members.length; i++) + { + webcamNames[i] = (WebcamName) members[i]; + } + + if (cameraMonitorViewId != 0) + { + camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(cameraMonitorViewId, webcamNames); + } + else + { + camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(webcamNames); + } + } + else + { + throw new IllegalArgumentException("All members of a switchable camera name must be webcam names"); + } + } + else // ¯\_(ツ)_/¯ + { + throw new IllegalArgumentException("Unknown camera name"); + } + } + + @Override + public void setProcessorEnabled(VisionProcessor processor, boolean enabled) + { + int numProcessorsEnabled = 0; + boolean ok = false; + + for (int i = 0; i < processors.length; i++) + { + if (processor == processors[i]) + { + processorsEnabled[i] = enabled; + ok = true; + } + + if (processorsEnabled[i]) + { + numProcessorsEnabled++; + } + } + + if (ok) + { + if (autoPauseCameraMonitor) + { + if (numProcessorsEnabled == 0) + { + camera.pauseViewport(); + } + else + { + camera.resumeViewport(); + } + } + } + else + { + throw new IllegalArgumentException("Processor not attached to this helper!"); + } + } + + @Override + public boolean getProcessorEnabled(VisionProcessor processor) + { + for (int i = 0; i < processors.length; i++) + { + if (processor == processors[i]) + { + return processorsEnabled[i]; + } + } + + throw new IllegalArgumentException("Processor not attached to this helper!"); + } + + @Override + public CameraState getCameraState() + { + return cameraState; + } + + @Override + public void setActiveCamera(WebcamName webcamName) + { + if (camera instanceof OpenCvSwitchableWebcam) + { + ((OpenCvSwitchableWebcam) camera).setActiveCamera(webcamName); + } + else + { + throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); + } + } + + @Override + public WebcamName getActiveCamera() + { + if (camera instanceof OpenCvSwitchableWebcam) + { + return ((OpenCvSwitchableWebcam) camera).getActiveCamera(); + } + else + { + throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); + } + } + + @Override + public T getCameraControl(Class controlType) + { + if (cameraState == CameraState.STREAMING) + { + if (camera instanceof OpenCvWebcam) + { + return ((OpenCvWebcam) camera).getControl(controlType); + } + else + { + throw new UnsupportedOperationException("Getting controls is only supported for webcams"); + } + } + else + { + throw new IllegalStateException("You cannot use camera controls until the camera is streaming"); + } + } + + class ProcessingPipeline extends TimestampedOpenCvPipeline + { + @Override + public void init(Mat firstFrame) + { + for (VisionProcessor processor : processors) + { + processor.init(firstFrame.width(), firstFrame.height(), calibration); + } + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) + { + synchronized (captureFrameMtx) + { + if (captureNextFrame != null) + { + saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); + } + + captureNextFrame = null; + } + + Object[] processorDrawCtxes = new Object[processors.length]; // cannot re-use frome to frame + + for (int i = 0; i < processors.length; i++) + { + if (processorsEnabled[i]) + { + processorDrawCtxes[i] = processors[i].processFrame(input, captureTimeNanos); + } + } + + requestViewportDrawHook(processorDrawCtxes); + + return input; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + Object[] ctx = (Object[]) userContext; + + for (int i = 0; i < processors.length; i++) + { + if (processorsEnabled[i]) + { + processors[i].onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, ctx[i]); + } + } + } + } + + @Override + public void saveNextFrameRaw(String filepath) + { + synchronized (captureFrameMtx) + { + captureNextFrame = filepath; + } + } + + @Override + public void stopStreaming() + { + synchronized (userStateMtx) + { + if (cameraState == CameraState.STREAMING || cameraState == CameraState.STARTING_STREAM) + { + cameraState = CameraState.STOPPING_STREAM; + new Thread(() -> + { + synchronized (userStateMtx) + { + camera.stopStreaming(); + cameraState = CameraState.CAMERA_DEVICE_READY; + } + }).start(); + } + else if (cameraState == CameraState.STOPPING_STREAM + || cameraState == CameraState.CAMERA_DEVICE_READY + || cameraState == CameraState.CLOSING_CAMERA_DEVICE) + { + // be idempotent + } + else + { + throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); + } + } + } + + @Override + public void resumeStreaming() + { + synchronized (userStateMtx) + { + if (cameraState == CameraState.CAMERA_DEVICE_READY || cameraState == CameraState.STOPPING_STREAM) + { + cameraState = CameraState.STARTING_STREAM; + new Thread(() -> + { + synchronized (userStateMtx) + { + if (camera instanceof OpenCvWebcam) + { + ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); + } + else + { + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); + } + cameraState = CameraState.STREAMING; + } + }).start(); + } + else if (cameraState == CameraState.STREAMING + || cameraState == CameraState.STARTING_STREAM + || cameraState == CameraState.OPENING_CAMERA_DEVICE) // we start streaming automatically after we open + { + // be idempotent + } + else + { + throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); + } + } + } + + @Override + public void stopLiveView() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + camera.pauseViewport(); + } + } + + @Override + public void resumeLiveView() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + camera.resumeViewport(); + } + } + + @Override + public float getFps() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + return cameraSafe.getFps(); + } + else + { + return 0; + } + } + + @Override + public void close() + { + synchronized (userStateMtx) + { + cameraState = CameraState.CLOSING_CAMERA_DEVICE; + + if (camera != null) + { + camera.closeCameraDeviceAsync(() -> cameraState = CameraState.CAMERA_DEVICE_CLOSED); + } + + camera = null; + } + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java new file mode 100644 index 00000000..73784e91 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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; + +/** + * May be attached to a {@link VisionPortal} to run image processing + */ +public interface VisionProcessor extends VisionProcessorInternal {} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java new file mode 100644 index 00000000..620412a3 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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; + +import android.graphics.Canvas; + +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.opencv.core.Mat; + +/** + * Internal interface + */ +interface VisionProcessorInternal +{ + void init(int width, int height, CameraCalibration calibration); + Object processFrame(Mat frame, long captureTimeNanos); + void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext); +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java new file mode 100644 index 00000000..ad9be96b --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; + +import org.opencv.calib3d.Calib3d; +import org.opencv.core.Mat; +import org.opencv.core.MatOfDouble; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.MatOfPoint3f; +import org.opencv.core.Point; +import org.opencv.core.Point3; + +public class AprilTagCanvasAnnotator +{ + final Mat cameraMatrix; + float bmpPxToCanvasPx; + float canvasDensityScale; + + LinePaint redAxisPaint = new LinePaint(Color.RED); + LinePaint greenAxisPaint = new LinePaint(Color.GREEN); + LinePaint blueAxisPaint = new LinePaint(Color.BLUE); + + LinePaint boxPillarPaint = new LinePaint(Color.rgb(7,197,235)); + LinePaint boxTopPaint = new LinePaint(Color.GREEN); + + static class LinePaint extends Paint + { + public LinePaint(int color) + { + setColor(color); + setAntiAlias(true); + setStrokeCap(Paint.Cap.ROUND); + } + } + + Paint textPaint; + Paint rectPaint; + + public AprilTagCanvasAnnotator(Mat cameraMatrix) + { + this.cameraMatrix = cameraMatrix; + + textPaint = new Paint(); + textPaint.setColor(Color.WHITE); + textPaint.setAntiAlias(true); + textPaint.setTypeface(Typeface.DEFAULT_BOLD); + + rectPaint = new Paint(); + rectPaint.setAntiAlias(true); + rectPaint.setColor(Color.rgb(12, 145, 201)); + rectPaint.setStyle(Paint.Style.FILL); + } + + public void noteDrawParams(float bmpPxToCanvasPx, float canvasDensityScale) + { + if (bmpPxToCanvasPx != this.bmpPxToCanvasPx || canvasDensityScale != this.canvasDensityScale) + { + this.bmpPxToCanvasPx = bmpPxToCanvasPx; + this.canvasDensityScale = canvasDensityScale; + + textPaint.setTextSize(40*canvasDensityScale); + } + } + + /** + * Draw a 3D axis marker on a detection. (Similar to what Vuforia does) + * + * @param detection the detection to draw + * @param canvas the canvas to draw on + * @param tagsize size of the tag in SAME UNITS as pose + */ + void drawAxisMarker(AprilTagDetection detection, Canvas canvas, double tagsize) + { + //Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsize, tagsize); + AprilTagProcessorImpl.Pose pose = AprilTagProcessorImpl.aprilTagPoseToOpenCvPose(detection.rawPose); + + // in meters, actually.... will be mapped to screen coords + float axisLength = (float) (tagsize / 2.0); + + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(0,0,0), + new Point3(-axisLength,0,0), + new Point3(0,-axisLength,0), + new Point3(0,0,-axisLength) + ); + + // Project those points onto the image + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, pose.rvec, pose.tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // The projection we did was good for the original resolution image, but now + // we need to scale those to their locations on the canvas. + for (Point p : projectedPoints) + { + p.x *= bmpPxToCanvasPx; + p.y *= bmpPxToCanvasPx; + } + + // Use the 3D distance to the target, as well as the physical size of the + // target in the real world to scale the thickness of lines. + double dist3d = Math.sqrt(Math.pow(detection.rawPose.x, 2) + Math.pow(detection.rawPose.y, 2) + Math.pow(detection.rawPose.z, 2)); + float axisThickness = (float) ((5 / dist3d) * (tagsize / 0.166) * bmpPxToCanvasPx); // looks about right I guess + + redAxisPaint.setStrokeWidth(axisThickness); + greenAxisPaint.setStrokeWidth(axisThickness); + blueAxisPaint.setStrokeWidth(axisThickness); + + // Now draw the axes + canvas.drawLine((float)projectedPoints[0].x,(float)projectedPoints[0].y, (float)projectedPoints[1].x, (float)projectedPoints[1].y, redAxisPaint); + canvas.drawLine((float)projectedPoints[0].x,(float)projectedPoints[0].y, (float)projectedPoints[2].x, (float)projectedPoints[2].y, greenAxisPaint); + canvas.drawLine((float)projectedPoints[0].x,(float)projectedPoints[0].y, (float)projectedPoints[3].x, (float)projectedPoints[3].y, blueAxisPaint); + } + + /** + * Draw a 3D cube marker on a detection + * + * @param detection the detection to draw + * @param canvas the canvas to draw on + * @param tagsize size of the tag in SAME UNITS as pose + */ + void draw3dCubeMarker(AprilTagDetection detection, Canvas canvas, double tagsize) + { + //Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsize, tagsize); + AprilTagProcessorImpl.Pose pose = AprilTagProcessorImpl.aprilTagPoseToOpenCvPose(detection.rawPose); + + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(-tagsize/2, tagsize/2,0), + new Point3( tagsize/2, tagsize/2,0), + new Point3( tagsize/2,-tagsize/2,0), + new Point3(-tagsize/2,-tagsize/2,0), + new Point3(-tagsize/2, tagsize/2,-tagsize), + new Point3( tagsize/2, tagsize/2,-tagsize), + new Point3( tagsize/2,-tagsize/2,-tagsize), + new Point3(-tagsize/2,-tagsize/2,-tagsize)); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, pose.rvec, pose.tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // The projection we did was good for the original resolution image, but now + // we need to scale those to their locations on the canvas. + for (Point p : projectedPoints) + { + p.x *= bmpPxToCanvasPx; + p.y *= bmpPxToCanvasPx; + } + + // Use the 3D distance to the target, as well as the physical size of the + // target in the real world to scale the thickness of lines. + double dist3d = Math.sqrt(Math.pow(detection.rawPose.x, 2) + Math.pow(detection.rawPose.y, 2) + Math.pow(detection.rawPose.z, 2)); + float thickness = (float) ((3.5 / dist3d) * (tagsize / 0.166) * bmpPxToCanvasPx); // looks about right I guess + + boxPillarPaint.setStrokeWidth(thickness); + boxTopPaint.setStrokeWidth(thickness); + + float[] pillarPts = new float[16]; + + // Pillars + for(int i = 0; i < 4; i++) + { + pillarPts[i*4+0] = (float) projectedPoints[i].x; + pillarPts[i*4+1] = (float) projectedPoints[i].y; + + pillarPts[i*4+2] = (float) projectedPoints[i+4].x; + pillarPts[i*4+3] = (float) projectedPoints[i+4].y; + } + + canvas.drawLines(pillarPts, boxPillarPaint); + + // Top lines + float[] topPts = new float[] { + (float) projectedPoints[4].x, (float) projectedPoints[4].y, (float) projectedPoints[5].x, (float) projectedPoints[5].y, + (float) projectedPoints[5].x, (float) projectedPoints[5].y, (float) projectedPoints[6].x, (float) projectedPoints[6].y, + (float) projectedPoints[6].x, (float) projectedPoints[6].y, (float) projectedPoints[7].x, (float) projectedPoints[7].y, + (float) projectedPoints[4].x, (float) projectedPoints[4].y, (float) projectedPoints[7].x, (float) projectedPoints[7].y + }; + + canvas.drawLines(topPts, boxTopPaint); + } + + /** + * Draw an outline marker on the detection + * @param detection the detection to draw + * @param canvas the canvas to draw on + * @param tagsize size of the tag in SAME UNITS as pose + */ + void drawOutlineMarker(AprilTagDetection detection, Canvas canvas, double tagsize) + { + // Use the 3D distance to the target, as well as the physical size of the + // target in the real world to scale the thickness of lines. + double dist3d = Math.sqrt(Math.pow(detection.rawPose.x, 2) + Math.pow(detection.rawPose.y, 2) + Math.pow(detection.rawPose.z, 2)); + float axisThickness = (float) ((5 / dist3d) * (tagsize / 0.166) * bmpPxToCanvasPx); // looks about right I guess + + redAxisPaint.setStrokeWidth(axisThickness); + greenAxisPaint.setStrokeWidth(axisThickness); + blueAxisPaint.setStrokeWidth(axisThickness); + + canvas.drawLine( + (float)detection.corners[0].x*bmpPxToCanvasPx,(float)detection.corners[0].y*bmpPxToCanvasPx, + (float)detection.corners[1].x*bmpPxToCanvasPx, (float)detection.corners[1].y*bmpPxToCanvasPx, + redAxisPaint); + + canvas.drawLine( + (float)detection.corners[1].x*bmpPxToCanvasPx,(float)detection.corners[1].y*bmpPxToCanvasPx, + (float)detection.corners[2].x*bmpPxToCanvasPx, (float)detection.corners[2].y*bmpPxToCanvasPx, + greenAxisPaint); + + canvas.drawLine( + (float)detection.corners[0].x*bmpPxToCanvasPx,(float)detection.corners[0].y*bmpPxToCanvasPx, + (float)detection.corners[3].x*bmpPxToCanvasPx, (float)detection.corners[3].y*bmpPxToCanvasPx, + blueAxisPaint); + + canvas.drawLine( + (float)detection.corners[2].x*bmpPxToCanvasPx,(float)detection.corners[2].y*bmpPxToCanvasPx, + (float)detection.corners[3].x*bmpPxToCanvasPx, (float)detection.corners[3].y*bmpPxToCanvasPx, + blueAxisPaint); + } + + /** + * Draw the Tag's ID on the tag + * @param detection the detection to draw + * @param canvas the canvas to draw on + */ + void drawTagID(AprilTagDetection detection, Canvas canvas) + { + float cornerRound = 5 * canvasDensityScale; + + float tag_id_width = 120*canvasDensityScale; + float tag_id_height = 50*canvasDensityScale; + + float id_x = (float) detection.center.x * bmpPxToCanvasPx - tag_id_width/2; + float id_y = (float) detection.center.y * bmpPxToCanvasPx - tag_id_height/2; + + float tag_id_text_x = id_x + 10*canvasDensityScale; + float tag_id_text_y = id_y + 40*canvasDensityScale; + + Point lowerLeft = detection.corners[0]; + Point lowerRight = detection.corners[1]; + + canvas.save(); + canvas.rotate((float) Math.toDegrees(Math.atan2(lowerRight.y - lowerLeft.y, lowerRight.x-lowerLeft.x)), (float) detection.center.x*bmpPxToCanvasPx, (float) detection.center.y*bmpPxToCanvasPx); + + canvas.drawRoundRect(id_x, id_y, id_x+tag_id_width, id_y+tag_id_height, cornerRound, cornerRound, rectPaint); + canvas.drawText(String.format("ID %02d", detection.id), tag_id_text_x, tag_id_text_y, textPaint); + + canvas.restore(); + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java new file mode 100644 index 00000000..e8c95e12 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import org.opencv.core.Point; + +public class AprilTagDetection +{ + /** + * The numerical ID of the detection + */ + public int id; + + /** + * The number of bits corrected when reading the tag ID payload + */ + public int hamming; + + /* + * How much margin remains before the detector would decide to reject a tag + */ + public float decisionMargin; + + /* + * The image pixel coordinates of the center of the tag + */ + public Point center; + + /* + * The image pixel coordinates of the corners of the tag + */ + public Point[] corners; + + /* + * Metadata known about this tag from the tag library set on the detector; + * will be NULL if the tag was not in the tag library + */ + public AprilTagMetadata metadata; + + /* + * 6DOF pose data formatted in useful ways for FTC gameplay + */ + public AprilTagPoseFtc ftcPose; + + /* + * Raw translation vector and orientation matrix returned by the pose solver + */ + public AprilTagPoseRaw rawPose; + + /* + * Timestamp of when the image in which this detection was found was acquired + */ + public long frameAcquisitionNanoTime; +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java new file mode 100644 index 00000000..44281c39 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import org.firstinspires.ftc.robotcore.external.matrices.VectorF; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; + +public class AprilTagGameDatabase +{ + /** + * Get the {@link AprilTagLibrary} for the current season game, plus sample tags + * @return the {@link AprilTagLibrary} for the current season game, plus sample tags + */ + public static AprilTagLibrary getCurrentGameTagLibrary() + { + return new AprilTagLibrary.Builder() + .addTags(getSampleTagLibrary()) + .addTags(getCenterStageTagLibrary()) + .build(); + } + + /** + * Get the {@link AprilTagLibrary} for the Center Stage FTC game + * @return the {@link AprilTagLibrary} for the Center Stage FTC game + */ + public static AprilTagLibrary getCenterStageTagLibrary() + { + return new AprilTagLibrary.Builder() + .addTag(0, "MEOW", + 0.166, new VectorF(0,0,0), DistanceUnit.METER, + Quaternion.identityQuaternion()) + .addTag(1, "WOOF", + 0.322, new VectorF(0,0,0), DistanceUnit.METER, + Quaternion.identityQuaternion()) + .addTag(2, "OINK", + 0.166, new VectorF(0,0,0), DistanceUnit.METER, + Quaternion.identityQuaternion()) + .build(); + } + + /** + * Get the {@link AprilTagLibrary} for the tags used in the sample OpModes + * @return the {@link AprilTagLibrary} for the tags used in the sample OpModes + */ + public static AprilTagLibrary getSampleTagLibrary() + { + return new AprilTagLibrary.Builder() + .addTag(583, "Nemo", + 4, DistanceUnit.INCH) + .addTag(584, "Jonah", + 4, DistanceUnit.INCH) + .addTag(585, "Cousteau", + 6, DistanceUnit.INCH) + .addTag(586, "Ariel", + 6, DistanceUnit.INCH) + .build(); + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java new file mode 100644 index 00000000..f6ba3161 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import org.firstinspires.ftc.robotcore.external.matrices.VectorF; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; +import org.firstinspires.ftc.vision.VisionPortal; + +import java.util.ArrayList; + +/** + * A tag library contains metadata about tags such as + * their ID, name, size, and 6DOF position on the field + */ +public class AprilTagLibrary +{ + private final AprilTagMetadata[] data; + + private AprilTagLibrary(AprilTagMetadata[] data) + { + this.data = data; + } + + /** + * Get the metadata of all tags in this library + * @return the metadata of all tags in this library + */ + public AprilTagMetadata[] getAllTags() + { + return data; + } + + /** + * Get the metadata for a specific tag in this library + * @param id the ID of the tag in question + * @return either {@link AprilTagMetadata} for the tag, or + * NULL if it isn't in this library + */ + public AprilTagMetadata lookupTag(int id) + { + for (AprilTagMetadata tagMetadata : data) + { + if (tagMetadata.id == id) + { + return tagMetadata; + } + } + + return null; + } + + public static class Builder + { + private ArrayList data = new ArrayList<>(); + private boolean allowOverwrite = false; + + /** + * Set whether to allow overwriting an existing entry in the tag + * library with a new entry of the same ID + * @param allowOverwrite whether to allow overwrite + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setAllowOverwrite(boolean allowOverwrite) + { + this.allowOverwrite = allowOverwrite; + return this; + } + + /** + * Add a tag to this tag library + * @param aprilTagMetadata the tag to add + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if trying to add a tag that already exists + * in this library, unless you called {@link #setAllowOverwrite(boolean)} + */ + public Builder addTag(AprilTagMetadata aprilTagMetadata) + { + for (AprilTagMetadata m : data) + { + if (m.id == aprilTagMetadata.id) + { + if (allowOverwrite) + { + // This is ONLY safe bc we immediately stop iteration here + data.remove(m); + break; + } + else + { + throw new RuntimeException("You attempted to add a tag to the library when it already contains a tag with that ID. You can call .setAllowOverwrite(true) to allow overwriting the existing entry"); + } + } + } + + data.add(aprilTagMetadata); + return this; + } + + /** + * Add a tag to this tag library + * @param id the ID of the tag + * @param name a text name for the tag + * @param size the physical size of the tag in the real world (measured black edge to black edge) + * @param fieldPosition a vector describing the tag's 3d translation on the field + * @param distanceUnit the units used for size and fieldPosition + * @param fieldOrientation a quaternion describing the tag's orientation on the field + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if trying to add a tag that already exists + * in this library, unless you called {@link #setAllowOverwrite(boolean)} + */ + public Builder addTag(int id, String name, double size, VectorF fieldPosition, DistanceUnit distanceUnit, Quaternion fieldOrientation) + { + return addTag(new AprilTagMetadata(id, name, size, fieldPosition, distanceUnit, fieldOrientation)); + } + + /** + * Add a tag to this tag library + * @param id the ID of the tag + * @param name a text name for the tag + * @param size the physical size of the tag in the real world (measured black edge to black edge) + * @param distanceUnit the units used for size and fieldPosition + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if trying to add a tag that already exists + * in this library, unless you called {@link #setAllowOverwrite(boolean)} + */ + public Builder addTag(int id, String name, double size, DistanceUnit distanceUnit) + { + return addTag(new AprilTagMetadata(id, name, size, new VectorF(0,0,0), distanceUnit, Quaternion.identityQuaternion())); + } + + /** + * Add multiple tags to this tag library + * @param library an existing tag library to add to this one + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if trying to add a tag that already exists + * in this library, unless you called {@link #setAllowOverwrite(boolean)} + */ + public Builder addTags(AprilTagLibrary library) + { + for (AprilTagMetadata m : library.getAllTags()) + { + // Delegate to this implementation so we get duplicate checking for free + addTag(m); + } + return this; + } + + /** + * Create an {@link AprilTagLibrary} object from the specified tags + * @return an {@link AprilTagLibrary} object + */ + public AprilTagLibrary build() + { + return new AprilTagLibrary(data.toArray(new AprilTagMetadata[0])); + } + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java new file mode 100644 index 00000000..40762e99 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import org.firstinspires.ftc.robotcore.external.matrices.VectorF; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; + +public class AprilTagMetadata +{ + public final int id; + public final double tagsize; + public final String name; + public final DistanceUnit distanceUnit; + public final VectorF fieldPosition; + public final Quaternion fieldOrientation; + + /** + * Add a tag to this tag library + * @param id the ID of the tag + * @param name a text name for the tag + * @param tagsize the physical size of the tag in the real world (measured black edge to black edge) + * @param fieldPosition a vector describing the tag's 3d translation on the field + * @param distanceUnit the units used for size and fieldPosition + * @param fieldOrientation a quaternion describing the tag's orientation on the field + */ + public AprilTagMetadata(int id, String name, double tagsize, VectorF fieldPosition, DistanceUnit distanceUnit, Quaternion fieldOrientation) + { + this.id = id; + this.name = name; + this.tagsize = tagsize; + this.fieldOrientation = fieldOrientation; + this.fieldPosition = fieldPosition; + this.distanceUnit = distanceUnit; + } + + /** + * Add a tag to this tag library + * @param id the ID of the tag + * @param name a text name for the tag + * @param tagsize the physical size of the tag in the real world (measured black edge to black edge) + * @param distanceUnit the units used for size and fieldPosition + */ + public AprilTagMetadata(int id, String name, double tagsize, DistanceUnit distanceUnit) + { + this.id = id; + this.name = name; + this.tagsize = tagsize; + this.fieldOrientation = Quaternion.identityQuaternion(); + this.fieldPosition = new VectorF(0,0,0); + this.distanceUnit = distanceUnit; + } +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java new file mode 100644 index 00000000..16300f7f --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 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.apriltag; + +/** + * AprilTagPoseFtc represents the AprilTag's position in space, relative to the camera. + * It is a realignment of the raw AprilTag Pose to be consistent with a forward-looking camera on an FTC robot.
+ * Also includes additional derived values to simplify driving towards any Tag.

+ * + * Note: These member definitions describe the camera Lens as the reference point for axis measurements.
+ * This measurement may be off by the distance from the lens to the image sensor itself. For most webcams this is a reasonable approximation. + * + * @see apriltag-detection-values.pdf + */ +public class AprilTagPoseFtc +{ + + /** + * X translation of AprilTag, relative to camera lens. Measured sideways (Horizontally in camera image) the positive X axis extends out to the right of the camera viewpoint.
+ * An x value of zero implies that the Tag is centered between the left and right sides of the Camera image. + */ + public double x; + + /** + * Y translation of AprilTag, relative to camera lens. Measured forwards (Horizontally in camera image) the positive Y axis extends out in the direction the camera is pointing.
+ * A y value of zero implies that the Tag is touching (aligned with) the lens of the camera, which is physically unlikley. This value should always be positive.
+ */ + public double y; + + /** + * Z translation of AprilTag, relative to camera lens. Measured upwards (Vertically in camera image) the positive Z axis extends Upwards in the camera viewpoint.
+ * A z value of zero implies that the Tag is centered between the top and bottom of the camera image. + */ + public double z; + + /** + * Rotation of AprilTag around the Z axis. Right-Hand-Rule defines positive Yaw rotation as Counter-Clockwise when viewed from above.
+ * A yaw value of zero implies that the camera is directly in front of the Tag, as viewed from above. + */ + public double yaw; + + /** + * Rotation of AprilTag around the X axis. Right-Hand-Rule defines positive Pitch rotation as the Tag Image face twisting down when viewed from the camera.
+ * A pitch value of zero implies that the camera is directly in front of the Tag, as viewed from the side. + */ + public double pitch; + + /** + * Rotation of AprilTag around the Y axis. Right-Hand-Rule defines positive Roll rotation as the Tag Image rotating Clockwise when viewed from the camera.
+ * A roll value of zero implies that the Tag image is alligned squarely and upright, when viewed in the camera image frame. + */ + public double roll; + + /** + * Range, (Distance), from the Camera lens to the center of the Tag, as measured along the X-Y plane (across the ground). + */ + public double range; + + /** + * Bearing, or Horizontal Angle, from the "camera center-line", to the "line joining the Camera lens and the Center of the Tag".
+ * This angle is measured across the X-Y plane (across the ground).
+ * A positive Bearing indicates that the robot must employ a positive Yaw (rotate counter clockwise) in order to point towards the target. + */ + public double bearing; + + /** + * Elevation, (Vertical Angle), from "the camera center-line", to "the line joining the Camera Lens and the Center of the Tag".
+ * A positive Elevation indicates that the robot must employ a positive Pitch (tilt up) in order to point towards the target. + */ + public double elevation; +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java new file mode 100644 index 00000000..edd67664 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import org.firstinspires.ftc.robotcore.external.matrices.MatrixF; + +public class AprilTagPoseRaw +{ + /** + * X translation + */ + public double x; + + /** + * Y translation + */ + public double y; + + /** + * Z translation + */ + public double z; + + /** + * 3x3 rotation matrix + */ + public MatrixF R; +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java new file mode 100644 index 00000000..a76471b9 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.vision.VisionProcessor; +import org.opencv.calib3d.Calib3d; +import org.openftc.apriltag.AprilTagDetectorJNI; + +import java.util.ArrayList; + +public abstract class AprilTagProcessor implements VisionProcessor +{ + public static final int THREADS_DEFAULT = 3; + + public enum TagFamily + { + TAG_36h11(AprilTagDetectorJNI.TagFamily.TAG_36h11), + TAG_25h9(AprilTagDetectorJNI.TagFamily.TAG_25h9), + TAG_16h5(AprilTagDetectorJNI.TagFamily.TAG_16h5), + TAG_standard41h12(AprilTagDetectorJNI.TagFamily.TAG_standard41h12); + + final AprilTagDetectorJNI.TagFamily ATLibTF; + + TagFamily(AprilTagDetectorJNI.TagFamily ATLibTF) + { + this.ATLibTF = ATLibTF; + } + } + + public static AprilTagProcessor easyCreateWithDefaults() + { + return new AprilTagProcessor.Builder().build(); + } + + public static class Builder + { + private double fx, fy, cx, cy; + private TagFamily tagFamily = TagFamily.TAG_36h11; + private AprilTagLibrary tagLibrary = AprilTagGameDatabase.getCurrentGameTagLibrary(); + private DistanceUnit outputUnitsLength = DistanceUnit.INCH; + private AngleUnit outputUnitsAngle = AngleUnit.DEGREES; + private int threads = THREADS_DEFAULT; + + private boolean drawAxes = false; + private boolean drawCube = false; + private boolean drawOutline = true; + private boolean drawTagId = true; + + /** + * Set the camera calibration parameters (needed for accurate 6DOF pose unless the + * SDK has a built in calibration for your camera) + * @param fx see opencv 8 parameter camera model + * @param fy see opencv 8 parameter camera model + * @param cx see opencv 8 parameter camera model + * @param cy see opencv 8 parameter camera model + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setLensIntrinsics(double fx, double fy, double cx, double cy) + { + this.fx = fx; + this.fy = fy; + this.cx = cx; + this.cy = cy; + return this; + } + + /** + * Set the tag family this detector will be used to detect (it can only be used + * for one tag family at a time) + * @param tagFamily the tag family this detector will be used to detect + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setTagFamily(TagFamily tagFamily) + { + this.tagFamily = tagFamily; + return this; + } + + /** + * Inform the detector about known tags. The tag library is used to allow solving + * for 6DOF pose, based on the physical size of the tag. Tags which are not in the + * library will not have their pose solved for + * @param tagLibrary a library of known tags for the detector to use when trying to solve pose + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setTagLibrary(AprilTagLibrary tagLibrary) + { + this.tagLibrary = tagLibrary; + return this; + } + + /** + * Set the units you want translation and rotation data provided in, inside any + * {@link AprilTagPoseRaw} or {@link AprilTagPoseFtc} objects + * @param distanceUnit translational units + * @param angleUnit rotational units + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setOutputUnits(DistanceUnit distanceUnit, AngleUnit angleUnit) + { + this.outputUnitsLength = distanceUnit; + this.outputUnitsAngle = angleUnit; + return this; + } + + /** + * Set whether to draw a 3D crosshair on the tag (what Vuforia did) + * @param drawAxes whether to draw it + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setDrawAxes(boolean drawAxes) + { + this.drawAxes = drawAxes; + return this; + } + + /** + * Set whether to draw a 3D cube projecting from the tag + * @param drawCube whether to draw it lol + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setDrawCubeProjection(boolean drawCube) + { + this.drawCube = drawCube; + return this; + } + + /** + * Set whether to draw a 2D outline around the tag detection + * @param drawOutline whether to draw it + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setDrawTagOutline(boolean drawOutline) + { + this.drawOutline = drawOutline; + return this; + } + + /** + * Set whether to annotate the tag detection with its ID + * @param drawTagId whether to annotate the tag with its ID + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setDrawTagID(boolean drawTagId) + { + this.drawTagId = drawTagId; + return this; + } + + /** + * Set the number of threads the tag detector should use + * @param threads the number of threads the tag detector should use + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setNumThreads(int threads) + { + this.threads = threads; + return this; + } + + /** + * Create a {@link VisionProcessor} object which may be attached to + * a {@link org.firstinspires.ftc.vision.VisionPortal} using + * {@link org.firstinspires.ftc.vision.VisionPortal.Builder#addProcessor(VisionProcessor)} + * @return a {@link VisionProcessor} object + */ + public AprilTagProcessor build() + { + if (tagLibrary == null) + { + throw new RuntimeException("Cannot create AprilTagProcessor without setting tag library!"); + } + + if (tagFamily == null) + { + throw new RuntimeException("Cannot create AprilTagProcessor without setting tag family!"); + } + + return new AprilTagProcessorImpl( + fx, fy, cx, cy, + outputUnitsLength, outputUnitsAngle, tagLibrary, + drawAxes, drawCube, drawOutline, drawTagId, + tagFamily, threads + ); + } + } + + /** + * Set the detector decimation + * + * Higher decimation increases frame rate at the expense of reduced range + * + * @param decimation detector decimation + */ + public abstract void setDecimation(float decimation); + + public enum PoseSolver + { + APRILTAG_BUILTIN(-1), + OPENCV_ITERATIVE(Calib3d.SOLVEPNP_ITERATIVE), + OPENCV_SOLVEPNP_EPNP(Calib3d.SOLVEPNP_EPNP), + OPENCV_IPPE(Calib3d.SOLVEPNP_IPPE), + OPENCV_IPPE_SQUARE(Calib3d.SOLVEPNP_IPPE_SQUARE), + OPENCV_SQPNP(Calib3d.SOLVEPNP_SQPNP); + + final int code; + + PoseSolver(int code) + { + this.code = code; + } + } + + /** + * Specify the method used to calculate 6DOF pose from the tag corner positions once + * found by the AprilTag algorithm + * @param poseSolver the pose solver to use + */ + public abstract void setPoseSolver(PoseSolver poseSolver); + + /** + * Get the average time in milliseconds the currently set pose solver is taking + * to converge on a solution PER TAG. Some pose solvers are much more expensive + * than others... + * @return average time to converge on a solution per tag in milliseconds + */ + public abstract int getPerTagAvgPoseSolveTime(); + + /** + * Get a list containing the latest detections, which may be stale + * i.e. the same as the last time you called this + * @return a list containing the latest detections. + */ + public abstract ArrayList getDetections(); + + /** + * Get a list containing detections that were detected SINCE THE PREVIOUS CALL to this method, + * or NULL if no new detections are available. This is useful to avoid re-processing the same + * detections multiple times. + * @return a list containing fresh detections, or NULL. + */ + public abstract ArrayList getFreshDetections(); +} + diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java new file mode 100644 index 00000000..97a39cbc --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java @@ -0,0 +1,504 @@ +/* + * Copyright (c) 2023 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.apriltag; + +import android.graphics.Canvas; +import android.util.Log; + +import com.qualcomm.robotcore.util.MovingStatistics; +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.robotcore.external.matrices.GeneralMatrixF; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; +import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.Orientation; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.opencv.calib3d.Calib3d; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfDouble; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.MatOfPoint3f; +import org.opencv.core.Point; +import org.opencv.core.Point3; +import org.opencv.imgproc.Imgproc; +import org.openftc.apriltag.AprilTagDetectorJNI; +import org.openftc.apriltag.ApriltagDetectionJNI; + +import java.util.ArrayList; + +public class AprilTagProcessorImpl extends AprilTagProcessor +{ + public static final String TAG = "AprilTagProcessorImpl"; + + private long nativeApriltagPtr; + private Mat grey = new Mat(); + private ArrayList detections = new ArrayList<>(); + + private ArrayList detectionsUpdate = new ArrayList<>(); + private final Object detectionsUpdateSync = new Object(); + private boolean drawAxes; + private boolean drawCube; + private boolean drawOutline; + private boolean drawTagID; + + private Mat cameraMatrix; + + private double fx; + private double fy; + private double cx; + private double cy; + + private final AprilTagLibrary tagLibrary; + + private float decimation; + private boolean needToSetDecimation; + private final Object decimationSync = new Object(); + + private AprilTagCanvasAnnotator canvasAnnotator; + + private final DistanceUnit outputUnitsLength; + private final AngleUnit outputUnitsAngle; + + private volatile PoseSolver poseSolver = PoseSolver.OPENCV_ITERATIVE; + + public AprilTagProcessorImpl(double fx, double fy, double cx, double cy, DistanceUnit outputUnitsLength, AngleUnit outputUnitsAngle, AprilTagLibrary tagLibrary, boolean drawAxes, boolean drawCube, boolean drawOutline, boolean drawTagID, TagFamily tagFamily, int threads) + { + this.fx = fx; + this.fy = fy; + this.cx = cx; + this.cy = cy; + + this.tagLibrary = tagLibrary; + this.outputUnitsLength = outputUnitsLength; + this.outputUnitsAngle = outputUnitsAngle; + this.drawAxes = drawAxes; + this.drawCube = drawCube; + this.drawOutline = drawOutline; + this.drawTagID = drawTagID; + + // Allocate a native context object. See the corresponding deletion in the finalizer + nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(tagFamily.ATLibTF.string, 3, threads); + } + + @Override + protected void finalize() + { + // Might be null if createApriltagDetector() threw an exception + if(nativeApriltagPtr != 0) + { + // Delete the native context we created in the constructor + AprilTagDetectorJNI.releaseApriltagDetector(nativeApriltagPtr); + nativeApriltagPtr = 0; + } + else + { + System.out.println("AprilTagDetectionPipeline.finalize(): nativeApriltagPtr was NULL"); + } + } + + @Override + public void init(int width, int height, CameraCalibration calibration) + { + // If the user didn't give us a calibration, but we have one built in, + // then go ahead and use it!! + if (calibration != null && fx == 0 && fy == 0 && cx == 0 && cy == 0 + && !(calibration.focalLengthX == 0 && calibration.focalLengthY == 0 && calibration.principalPointX == 0 && calibration.principalPointY == 0)) // needed because we may get an all zero calibration to indicate none, instead of null + { + fx = calibration.focalLengthX; + fy = calibration.focalLengthY; + cx = calibration.principalPointX; + cy = calibration.principalPointY; + + Log.d(TAG, String.format("User did not provide a camera calibration; but we DO have a built in calibration we can use.\n [%dx%d] (may be scaled) %s\nfx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", + calibration.getSize().getWidth(), calibration.getSize().getHeight(), calibration.getIdentity().toString(), fx, fy, cx, cy)); + } + else if (fx == 0 && fy == 0 && cx == 0 && cy == 0) + { + // set it to *something* so we don't crash the native code + + String warning = "User did not provide a camera calibration, nor was a built-in calibration found for this camera; 6DOF pose data will likely be inaccurate."; + Log.d(TAG, warning); + RobotLog.addGlobalWarningMessage(warning); + + fx = 578.272; + fy = 578.272; + cx = width/2; + cy = height/2; + } + else + { + Log.d(TAG, String.format("User provided their own camera calibration fx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", + fx, fy, cx, cy)); + } + + constructMatrix(); + + canvasAnnotator = new AprilTagCanvasAnnotator(cameraMatrix); + } + + @Override + public Object processFrame(Mat input, long captureTimeNanos) + { + // Convert to greyscale + Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); + + synchronized (decimationSync) + { + if(needToSetDecimation) + { + AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); + needToSetDecimation = false; + } + } + + // Run AprilTag + detections = runAprilTagDetectorForMultipleTagSizes(captureTimeNanos); + + synchronized (detectionsUpdateSync) + { + detectionsUpdate = detections; + } + + // TODO do we need to deep copy this so the user can't mess with it before use in onDrawFrame()? + return detections; + } + + private MovingStatistics solveTime = new MovingStatistics(50); + + // We cannot use runAprilTagDetectorSimple because we cannot assume tags are all the same size + ArrayList runAprilTagDetectorForMultipleTagSizes(long captureTimeNanos) + { + long ptrDetectionArray = AprilTagDetectorJNI.runApriltagDetector(nativeApriltagPtr, grey.dataAddr(), grey.width(), grey.height()); + if (ptrDetectionArray != 0) + { + long[] detectionPointers = ApriltagDetectionJNI.getDetectionPointers(ptrDetectionArray); + ArrayList detections = new ArrayList<>(detectionPointers.length); + + for (long ptrDetection : detectionPointers) + { + AprilTagDetection detection = new AprilTagDetection(); + detection.frameAcquisitionNanoTime = captureTimeNanos; + + detection.id = ApriltagDetectionJNI.getId(ptrDetection); + + AprilTagMetadata metadata = tagLibrary.lookupTag(detection.id); + detection.metadata = metadata; + + detection.hamming = ApriltagDetectionJNI.getHamming(ptrDetection); + detection.decisionMargin = ApriltagDetectionJNI.getDecisionMargin(ptrDetection); + double[] center = ApriltagDetectionJNI.getCenterpoint(ptrDetection); + detection.center = new Point(center[0], center[1]); + double[][] corners = ApriltagDetectionJNI.getCorners(ptrDetection); + + detection.corners = new Point[4]; + for (int p = 0; p < 4; p++) + { + detection.corners[p] = new Point(corners[p][0], corners[p][1]); + } + + if (metadata != null) + { + PoseSolver solver = poseSolver; // snapshot, can change + + detection.rawPose = new AprilTagPoseRaw(); + + long startSolveTime = System.currentTimeMillis(); + + if (solver == PoseSolver.APRILTAG_BUILTIN) + { + // Translation + double[] pose = ApriltagDetectionJNI.getPoseEstimate( + ptrDetection, + outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize), + fx, fy, cx, cy); + + detection.rawPose.x = pose[0]; + detection.rawPose.y = pose[1]; + detection.rawPose.z = pose[2]; + + // Rotation + float[] rotMtxVals = new float[3 * 3]; + for (int i = 0; i < 9; i++) + { + rotMtxVals[i] = (float) pose[3 + i]; + } + detection.rawPose.R = new GeneralMatrixF(3, 3, rotMtxVals); + } + else + { + Pose opencvPose = poseFromTrapezoid( + detection.corners, + cameraMatrix, + outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize), + solver.code); + + detection.rawPose.x = opencvPose.tvec.get(0,0)[0]; + detection.rawPose.y = opencvPose.tvec.get(1,0)[0]; + detection.rawPose.z = opencvPose.tvec.get(2,0)[0]; + + Mat R = new Mat(3, 3, CvType.CV_32F); + Calib3d.Rodrigues(opencvPose.rvec, R); + + float[] tmp2 = new float[9]; + R.get(0,0, tmp2); + detection.rawPose.R = new GeneralMatrixF(3,3, tmp2); + } + + long endSolveTime = System.currentTimeMillis(); + solveTime.add(endSolveTime-startSolveTime); + } + else + { + // We don't know anything about the tag size so we can't solve the pose + detection.rawPose = null; + } + + if (detection.rawPose != null) + { + detection.ftcPose = new AprilTagPoseFtc(); + + detection.ftcPose.x = detection.rawPose.x; + detection.ftcPose.y = detection.rawPose.z; + detection.ftcPose.z = -detection.rawPose.y; + + Orientation rot = Orientation.getOrientation(detection.rawPose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, outputUnitsAngle); + detection.ftcPose.yaw = -rot.firstAngle; + detection.ftcPose.roll = rot.thirdAngle; + detection.ftcPose.pitch = rot.secondAngle; + + detection.ftcPose.range = Math.hypot(detection.ftcPose.x, detection.ftcPose.y); + detection.ftcPose.bearing = outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(-detection.ftcPose.x, detection.ftcPose.y)); + detection.ftcPose.elevation = outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(detection.ftcPose.z, detection.ftcPose.y)); + } + + detections.add(detection); + } + + ApriltagDetectionJNI.freeDetectionList(ptrDetectionArray); + return detections; + } + + return new ArrayList<>(); + } + + private final Object drawSync = new Object(); + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + // Only one draw operation at a time thank you very much. + // (we could be called from two different threads - viewport or camera stream) + synchronized (drawSync) + { + if ((drawAxes || drawCube || drawOutline || drawTagID) && userContext != null) + { + canvasAnnotator.noteDrawParams(scaleBmpPxToCanvasPx, scaleCanvasDensity); + + ArrayList dets = (ArrayList) userContext; + + // For fun, draw 6DOF markers on the image. + for(AprilTagDetection detection : dets) + { + if (drawTagID) + { + canvasAnnotator.drawTagID(detection, canvas); + } + + // Could be null if we couldn't solve the pose earlier due to not knowing tag size + if (detection.rawPose != null) + { + AprilTagMetadata metadata = tagLibrary.lookupTag(detection.id); + double tagSize = outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize); + + if (drawOutline) + { + canvasAnnotator.drawOutlineMarker(detection, canvas, tagSize); + } + if (drawAxes) + { + canvasAnnotator.drawAxisMarker(detection, canvas, tagSize); + } + if (drawCube) + { + canvasAnnotator.draw3dCubeMarker(detection, canvas, tagSize); + } + } + } + } + } + } + + public void setDecimation(float decimation) + { + synchronized (decimationSync) + { + this.decimation = decimation; + needToSetDecimation = true; + } + } + + @Override + public void setPoseSolver(PoseSolver poseSolver) + { + this.poseSolver = poseSolver; + } + + @Override + public int getPerTagAvgPoseSolveTime() + { + return (int) Math.round(solveTime.getMean()); + } + + public ArrayList getDetections() + { + return detections; + } + + public ArrayList getFreshDetections() + { + synchronized (detectionsUpdateSync) + { + ArrayList ret = detectionsUpdate; + detectionsUpdate = null; + return ret; + } + } + + void constructMatrix() + { + // Construct the camera matrix. + // + // -- -- + // | fx 0 cx | + // | 0 fy cy | + // | 0 0 1 | + // -- -- + // + + cameraMatrix = new Mat(3,3, CvType.CV_32FC1); + + cameraMatrix.put(0,0, fx); + cameraMatrix.put(0,1,0); + cameraMatrix.put(0,2, cx); + + cameraMatrix.put(1,0,0); + cameraMatrix.put(1,1,fy); + cameraMatrix.put(1,2,cy); + + cameraMatrix.put(2, 0, 0); + cameraMatrix.put(2,1,0); + cameraMatrix.put(2,2,1); + } + + /** + * Converts an AprilTag pose to an OpenCV pose + * @param aprilTagPose pose to convert + * @return OpenCV output pose + */ + static Pose aprilTagPoseToOpenCvPose(AprilTagPoseRaw aprilTagPose) + { + Pose pose = new Pose(); + pose.tvec.put(0,0, aprilTagPose.x); + pose.tvec.put(1,0, aprilTagPose.y); + pose.tvec.put(2,0, aprilTagPose.z); + + Mat R = new Mat(3, 3, CvType.CV_32F); + + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + R.put(i,j, aprilTagPose.R.get(i,j)); + } + } + + Calib3d.Rodrigues(R, pose.rvec); + + return pose; + } + + /** + * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the + * original size of the tag. + * + * @param points the points which form the trapezoid + * @param cameraMatrix the camera intrinsics matrix + * @param tagsize the original length of the tag + * @return the 6DOF pose of the camera relative to the tag + */ + static Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsize, int solveMethod) + { + // The actual 2d points of the tag detected in the image + MatOfPoint2f points2d = new MatOfPoint2f(points); + + // The 3d points of the tag in an 'ideal projection' + Point3[] arrayPoints3d = new Point3[4]; + arrayPoints3d[0] = new Point3(-tagsize/2, tagsize/2, 0); + arrayPoints3d[1] = new Point3(tagsize/2, tagsize/2, 0); + arrayPoints3d[2] = new Point3(tagsize/2, -tagsize/2, 0); + arrayPoints3d[3] = new Point3(-tagsize/2, -tagsize/2, 0); + MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); + + // Using this information, actually solve for pose + Pose pose = new Pose(); + Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false, solveMethod); + + return pose; + } + + /* + * A simple container to hold both rotation and translation + * vectors, which together form a 6DOF pose. + */ + static class Pose + { + Mat rvec; + Mat tvec; + + public Pose() + { + rvec = new Mat(3, 1, CvType.CV_32F); + tvec = new Mat(3, 1, CvType.CV_32F); + } + + public Pose(Mat rvec, Mat tvec) + { + this.rvec = rvec; + this.tvec = tvec; + } + } +} \ No newline at end of file From ea049ec97ae932cfcb6ce9bdafcfbc6dda9066ed Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 5 Aug 2023 18:10:24 -0600 Subject: [PATCH 09/46] Include Vision module --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 626bd667..1056fa46 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,4 +11,4 @@ rootProject.name = 'EOCV-Sim' include 'TeamCode' include 'EOCV-Sim' include 'Common' - +Include 'Vision' \ No newline at end of file From 885a76305ad591e3ce38a9d14d507d56c0820ebe Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 5 Aug 2023 18:11:33 -0600 Subject: [PATCH 10/46] Fix typo in settings.gradle --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 1056fa46..51b5ae1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,4 +11,4 @@ rootProject.name = 'EOCV-Sim' include 'TeamCode' include 'EOCV-Sim' include 'Common' -Include 'Vision' \ No newline at end of file +include 'Vision' \ No newline at end of file From e3e0731987c4ab0346c8c6765d04baeb8b0c0df6 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 9 Aug 2023 03:16:05 -0600 Subject: [PATCH 11/46] android.graphics partly ported - code builds --- .../java/android/annotation/AnyThread.java | 44 + .../java/android/annotation/ColorInt.java | 37 + .../java/android/annotation/ColorLong.java | 44 + .../java/android/annotation/HalfFloat.java | 45 + .../main/java/android/annotation/IntDef.java | 55 + .../java/android/annotation/IntRange.java | 45 + .../main/java/android/annotation/NonNull.java | 34 + .../java/android/annotation/Nullable.java | 45 + .../main/java/android/annotation/Size.java | 49 + .../android/annotation/SuppressAutoDoc.java | 35 + .../java/android/annotation/SuppressLint.java | 35 + .../java/android/annotation/SystemApi.java | 74 + .../main/java/android/hardware/DataSpace.java | 630 +++ .../src/main/java/android/util/ArraySet.java | 903 ++++ .../java/android/util/ContainerHelpers.java | 51 + Common/src/main/java/android/util/Half.java | 847 ++++ .../java/android/util/MapCollections.java | 487 +++ .../java/android/util/SparseIntArray.java | 284 ++ .../java/androidx/annotation/NonNull.java | 36 + .../java/androidx/annotation/StringRes.java | 33 + .../com/android/internal/util/ArrayUtils.java | 834 ++++ .../internal/util/GrowingArrayUtils.java | 186 + .../exception/RobotCoreException.java | 54 + .../com/qualcomm/robotcore/util/RobotLog.java | 17 + .../main/java/dalvik/system/VMRuntime.java | 22 + .../main/java/libcore/util/EmptyArray.java | 72 + Common/src/main/java/libcore/util/FP16.java | 794 ++++ .../robotcore/external/android/util/Size.java | 152 + .../camera/calibration/CameraCalibration.java | 172 + .../CameraCalibrationIdentity.java | 38 + .../camera/calibration/CameraIntrinsics.java | 117 + .../VendorProductCalibrationIdentity.java | 84 + .../robotcore/internal/system/AppUtil.java | 70 + .../ftc/robotcore/internal/system/Misc.java | 475 +++ EOCV-Sim/build.gradle | 3 +- TeamCode/build.gradle | 1 - Vision/build.gradle | 21 +- .../main/java/android/graphics/Bitmap.java | 34 + .../main/java/android/graphics/Canvas.java | 81 + .../src/main/java/android/graphics/Color.java | 1415 +++++++ .../java/android/graphics/ColorSpace.java | 3642 +++++++++++++++++ .../main/java/android/graphics/FontCache.java | 26 + .../src/main/java/android/graphics/Paint.java | 275 ++ .../src/main/java/android/graphics/Rect.java | 616 +++ .../main/java/android/graphics/Typeface.java | 32 + .../ftc/vision/VisionPortal.java | 482 --- .../ftc/vision/VisionPortalImpl.java | 501 --- .../apriltag/AprilTagCanvasAnnotator.java | 2 +- .../ftc/vision/apriltag/AprilTagLibrary.java | 1 - .../apriltag/AprilTagProcessorImpl.java | 16 +- build.gradle | 7 +- 51 files changed, 13056 insertions(+), 999 deletions(-) create mode 100644 Common/src/main/java/android/annotation/AnyThread.java create mode 100644 Common/src/main/java/android/annotation/ColorInt.java create mode 100644 Common/src/main/java/android/annotation/ColorLong.java create mode 100644 Common/src/main/java/android/annotation/HalfFloat.java create mode 100644 Common/src/main/java/android/annotation/IntDef.java create mode 100644 Common/src/main/java/android/annotation/IntRange.java create mode 100644 Common/src/main/java/android/annotation/NonNull.java create mode 100644 Common/src/main/java/android/annotation/Nullable.java create mode 100644 Common/src/main/java/android/annotation/Size.java create mode 100644 Common/src/main/java/android/annotation/SuppressAutoDoc.java create mode 100644 Common/src/main/java/android/annotation/SuppressLint.java create mode 100644 Common/src/main/java/android/annotation/SystemApi.java create mode 100644 Common/src/main/java/android/hardware/DataSpace.java create mode 100644 Common/src/main/java/android/util/ArraySet.java create mode 100644 Common/src/main/java/android/util/ContainerHelpers.java create mode 100644 Common/src/main/java/android/util/Half.java create mode 100644 Common/src/main/java/android/util/MapCollections.java create mode 100644 Common/src/main/java/android/util/SparseIntArray.java create mode 100644 Common/src/main/java/androidx/annotation/NonNull.java create mode 100644 Common/src/main/java/androidx/annotation/StringRes.java create mode 100644 Common/src/main/java/com/android/internal/util/ArrayUtils.java create mode 100644 Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java create mode 100644 Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java create mode 100644 Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java create mode 100644 Common/src/main/java/dalvik/system/VMRuntime.java create mode 100644 Common/src/main/java/libcore/util/EmptyArray.java create mode 100644 Common/src/main/java/libcore/util/FP16.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java create mode 100644 Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java create mode 100644 Vision/src/main/java/android/graphics/Bitmap.java create mode 100644 Vision/src/main/java/android/graphics/Canvas.java create mode 100644 Vision/src/main/java/android/graphics/Color.java create mode 100644 Vision/src/main/java/android/graphics/ColorSpace.java create mode 100644 Vision/src/main/java/android/graphics/FontCache.java create mode 100644 Vision/src/main/java/android/graphics/Paint.java create mode 100644 Vision/src/main/java/android/graphics/Rect.java create mode 100644 Vision/src/main/java/android/graphics/Typeface.java delete mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java delete mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java diff --git a/Common/src/main/java/android/annotation/AnyThread.java b/Common/src/main/java/android/annotation/AnyThread.java new file mode 100644 index 00000000..ff7a2b0f --- /dev/null +++ b/Common/src/main/java/android/annotation/AnyThread.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 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 android.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; +/** + * Denotes that the annotated method can be called from any thread (e.g. it is "thread safe".) + * If the annotated element is a class, then all methods in the class can be called + * from any thread. + *

+ * The main purpose of this method is to indicate that you believe a method can be called + * from any thread; static tools can then check that nothing you call from within this method + * or class have more strict threading requirements. + *

+ * Example: + *


+ *  @AnyThread
+ *  public void deliverResult(D data) { ... }
+ * 
+ */ +@Documented +@Retention(CLASS) +@Target({METHOD,CONSTRUCTOR,TYPE}) +public @interface AnyThread { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/ColorInt.java b/Common/src/main/java/android/annotation/ColorInt.java new file mode 100644 index 00000000..b378bade --- /dev/null +++ b/Common/src/main/java/android/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 android.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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; +/** + * 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/android/annotation/ColorLong.java b/Common/src/main/java/android/annotation/ColorLong.java new file mode 100644 index 00000000..395fc468 --- /dev/null +++ b/Common/src/main/java/android/annotation/ColorLong.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 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 android.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.SOURCE; +/** + *

Denotes that the annotated element represents a packed color + * long. If applied to a long array, every element in the array + * represents a color long. For more information on how colors + * are packed in a long, please refer to the documentation of + * the {link android.graphics.Color} class.

+ * + *

Example:

+ * + *
{@code
+ *  public void setFillColor(@ColorLong long color);
+ * }
+ * + * see android.graphics.Color + * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD}) +public @interface ColorLong { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/HalfFloat.java b/Common/src/main/java/android/annotation/HalfFloat.java new file mode 100644 index 00000000..3f25dc23 --- /dev/null +++ b/Common/src/main/java/android/annotation/HalfFloat.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 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 android.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.SOURCE; +/** + *

Denotes that the annotated element represents a half-precision floating point + * value. Such values are stored in short data types and can be manipulated with + * the {@link android.util.Half} class. If applied to an array of short, every + * element in the array represents a half-precision float.

+ * + *

Example:

+ * + *
{@code
+ * public abstract void setPosition(@HalfFloat short x, @HalfFloat short y, @HalfFloat short z);
+ * }
+ * + * @see android.util.Half + * @see android.util.Half#toHalf(float) + * @see android.util.Half#toFloat(short) + * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) +public @interface HalfFloat { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/IntDef.java b/Common/src/main/java/android/annotation/IntDef.java new file mode 100644 index 00000000..2f425535 --- /dev/null +++ b/Common/src/main/java/android/annotation/IntDef.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 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 android.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; +/** + * Denotes that the annotated element of integer type, represents + * a logical type and that its value should be one of the explicitly + * named constants. If the {@link #flag()} attribute is set to true, + * multiple constants can be combined. + *

+ *


+ *  @Retention(SOURCE)
+ *  @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ *  public @interface NavigationMode {}
+ *  public static final int NAVIGATION_MODE_STANDARD = 0;
+ *  public static final int NAVIGATION_MODE_LIST = 1;
+ *  public static final int NAVIGATION_MODE_TABS = 2;
+ *  ...
+ *  public abstract void setNavigationMode(@NavigationMode int mode);
+ *  @NavigationMode
+ *  public abstract int getNavigationMode();
+ * 
+ * For a flag, set the flag attribute: + *

+ *  @IntDef(
+ *      flag = true
+ *      value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * 
+ * + * @hide + */ +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface IntDef { + /** Defines the allowed constants for this element */ + long[] value() default {}; + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/IntRange.java b/Common/src/main/java/android/annotation/IntRange.java new file mode 100644 index 00000000..cf842f0b --- /dev/null +++ b/Common/src/main/java/android/annotation/IntRange.java @@ -0,0 +1,45 @@ +/* + * 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 android.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +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.SOURCE; +/** + * Denotes that the annotated element should be an int or long in the given range + *

+ * Example: + *


+ *  @IntRange(from=0,to=255)
+ *  public int getAlpha() {
+ *      ...
+ *  }
+ * 
+ * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE}) +public @interface IntRange { + /** Smallest value, inclusive */ + long from() default Long.MIN_VALUE; + /** Largest value, inclusive */ + long to() default Long.MAX_VALUE; +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/NonNull.java b/Common/src/main/java/android/annotation/NonNull.java new file mode 100644 index 00000000..cdd06669 --- /dev/null +++ b/Common/src/main/java/android/annotation/NonNull.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 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 android.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Denotes that a parameter, field or method return value can never be null. + *

+ * This is a marker annotation and it has no specific attributes. + */ +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +public @interface NonNull { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/Nullable.java b/Common/src/main/java/android/annotation/Nullable.java new file mode 100644 index 00000000..d6839263 --- /dev/null +++ b/Common/src/main/java/android/annotation/Nullable.java @@ -0,0 +1,45 @@ + +/* + * Copyright (C) 2013 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 android.annotation; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; +// import android.annotation.SystemApi.Client; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * Denotes that a parameter, field or method return value can be null. + *

+ * When decorating a method call parameter, this denotes that the parameter can + * legitimately be null and the method will gracefully deal with it. Typically + * used on optional parameters. + *

+ * When decorating a method, this denotes the method might legitimately return + * null. + *

+ * This is a marker annotation and it has no specific attributes. + * + * @paramDoc This value may be {@code null}. + * @returnDoc This value may be {@code null}. + * @hide + */ +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +// @SystemApi(client = Client.MODULE_LIBRARIES) +public @interface Nullable { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/Size.java b/Common/src/main/java/android/annotation/Size.java new file mode 100644 index 00000000..96d6f211 --- /dev/null +++ b/Common/src/main/java/android/annotation/Size.java @@ -0,0 +1,49 @@ +/* + * 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 android.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.SOURCE; +/** + * Denotes that the annotated element should have a given size or length. + * Note that "-1" means "unset". Typically used with a parameter or + * return value of type array or collection. + *

+ * Example: + *

{@code
+ *  public void getLocationInWindow(@Size(2) int[] location) {
+ *      ...
+ *  }
+ * }
+ * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER,LOCAL_VARIABLE,METHOD,FIELD}) +public @interface Size { + /** An exact size (or -1 if not specified) */ + long value() default -1; + /** A minimum size, inclusive */ + long min() default Long.MIN_VALUE; + /** A maximum size, inclusive */ + long max() default Long.MAX_VALUE; + /** The size must be a multiple of this factor */ + long multiple() default 1; +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/SuppressAutoDoc.java b/Common/src/main/java/android/annotation/SuppressAutoDoc.java new file mode 100644 index 00000000..6c32a1ca --- /dev/null +++ b/Common/src/main/java/android/annotation/SuppressAutoDoc.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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 android.annotation; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * Denotes that any automatically generated documentation should be suppressed + * for the annotated method, parameter, or field. + * + * @hide + */ +@Retention(SOURCE) +@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) +public @interface SuppressAutoDoc { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/SuppressLint.java b/Common/src/main/java/android/annotation/SuppressLint.java new file mode 100644 index 00000000..09ed0c61 --- /dev/null +++ b/Common/src/main/java/android/annotation/SuppressLint.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 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 android.annotation; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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.ElementType.TYPE; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** Indicates that Lint should ignore the specified warnings for the annotated element. */ +@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) +@Retention(RetentionPolicy.CLASS) +public @interface SuppressLint { + /** + * The set of warnings (identified by the lint issue id) that should be + * ignored by lint. It is not an error to specify an unrecognized name. + */ + String[] value(); +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/SystemApi.java b/Common/src/main/java/android/annotation/SystemApi.java new file mode 100644 index 00000000..7c34de56 --- /dev/null +++ b/Common/src/main/java/android/annotation/SystemApi.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 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 android.annotation; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Indicates an API is exposed for use by bundled system applications. + *

+ * These APIs are not guaranteed to remain consistent release-to-release, + * and are not for use by apps linking against the Android SDK. + *

+ * This annotation should only appear on API that is already marked

@hide
. + *

+ * + * @hide + */ +@Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(SystemApi.Container.class) // TODO(b/146727827): make this non-repeatable +public @interface SystemApi { + enum Client { + /** + * Specifies that the intended clients of a SystemApi are privileged apps. + * This is the default value for {@link #client}. + */ + PRIVILEGED_APPS, + /** + * Specifies that the intended clients of a SystemApi are used by classes in + *
BOOTCLASSPATH
in mainline modules. Mainline modules can also expose + * this type of system APIs too when they're used only by the non-updatable + * platform code. + */ + MODULE_LIBRARIES, + /** + * Specifies that the system API is available only in the system server process. + * Use this to expose APIs from code loaded by the system server process but + * not in
BOOTCLASSPATH
. + */ + SYSTEM_SERVER + } + /** + * The intended client of this SystemAPI. + */ + Client client() default android.annotation.SystemApi.Client.PRIVILEGED_APPS; + /** + * Container for {@link SystemApi} that allows it to be applied repeatedly to types. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(TYPE) + @interface Container { + SystemApi[] value(); + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/hardware/DataSpace.java b/Common/src/main/java/android/hardware/DataSpace.java new file mode 100644 index 00000000..d0cf98cb --- /dev/null +++ b/Common/src/main/java/android/hardware/DataSpace.java @@ -0,0 +1,630 @@ +/* + * Copyright 2021 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 android.hardware; +import android.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +/** + * DataSpace identifies three components of colors - standard (primaries), transfer and range. + * + *

A DataSpace describes how buffer data, such as from an {link android.media.Image Image} + * or a {link android.hardware.HardwareBuffer HardwareBuffer} + * should be interpreted by both applications and typical hardware.

+ * + *

As buffer information is not guaranteed to be representative of color information, + * while DataSpace is typically used to describe three aspects of interpreting colors, + * some DataSpaces may describe other typical interpretations of buffer data + * such as depth information.

+ * + *

Note that while {link android.graphics.ColorSpace ColorSpace} and {@code DataSpace} + * are similar concepts, they are not equivalent. Not all ColorSpaces, + * such as {link android.graphics.ColorSpace.Named#ACES ColorSpace.Named.ACES}, + * are able to be understood by typical hardware blocks so they cannot be DataSpaces.

+ * + *

Standard aspect

+ * + *

Defines the chromaticity coordinates of the source primaries in terms of + * the CIE 1931 definition of x and y specified in ISO 11664-1.

+ * + *

Transfer aspect

+ * + *

Transfer characteristics are the opto-electronic transfer characteristic + * at the source as a function of linear optical intensity (luminance).

+ * + *

For digital signals, E corresponds to the recorded value. Normally, the + * transfer function is applied in RGB space to each of the R, G and B + * components independently. This may result in color shift that can be + * minized by applying the transfer function in Lab space only for the L + * component. Implementation may apply the transfer function in RGB space + * for all pixel formats if desired.

+ * + *

Range aspect

+ * + *

Defines the range of values corresponding to the unit range of {@code 0-1}.

+ */ +public final class DataSpace { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + STANDARD_UNSPECIFIED, + STANDARD_BT709, + STANDARD_BT601_625, + STANDARD_BT601_625_UNADJUSTED, + STANDARD_BT601_525, + STANDARD_BT601_525_UNADJUSTED, + STANDARD_BT2020, + STANDARD_BT2020_CONSTANT_LUMINANCE, + STANDARD_BT470M, + STANDARD_FILM, + STANDARD_DCI_P3, + STANDARD_ADOBE_RGB + }) + public @interface DataSpaceStandard {}; + private static final int STANDARD_MASK = 63 << 16; + /** + * Chromacity coordinates are unknown or are determined by the application. + */ + public static final int STANDARD_UNSPECIFIED = 0 << 16; + /** + * Use the unadjusted {@code KR = 0.2126}, {@code KB = 0.0722} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.300   0.600
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT709 = 1 << 16; + /** + * Use the adjusted {@code KR = 0.299}, {@code KB = 0.114} luminance interpretation + * for RGB conversion from the one purely determined by the primaries + * to minimize the color shift into RGB space that uses BT.709 + * primaries. + * + *
+     * Primaries:       x       y
+     *  green           0.290   0.600
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_625 = 2 << 16; + /** + * Use the unadjusted {@code KR = 0.222}, {@code KB = 0.071} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.290   0.600
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_625_UNADJUSTED = 3 << 16; + /** + * Use the adjusted {@code KR = 0.299}, {@code KB = 0.114} luminance interpretation + * for RGB conversion from the one purely determined by the primaries + * to minimize the color shift into RGB space that uses BT.709 + * primaries. + * + *
+     * Primaries:       x       y
+     *  green           0.310   0.595
+     *  blue            0.155   0.070
+     *  red             0.630   0.340
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_525 = 4 << 16; + /** + * Use the unadjusted {@code KR = 0.212}, {@code KB = 0.087} luminance interpretation + * for RGB conversion (as in SMPTE 240M). + * + *
+     * Primaries:       x       y
+     *  green           0.310   0.595
+     *  blue            0.155   0.070
+     *  red             0.630   0.340
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_525_UNADJUSTED = 5 << 16; + /** + * Use the unadjusted {@code KR = 0.2627}, {@code KB = 0.0593} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.170   0.797
+     *  blue            0.131   0.046
+     *  red             0.708   0.292
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT2020 = 6 << 16; + /** + * Use the unadjusted {@code KR = 0.2627}, {@code KB = 0.0593} luminance interpretation + * for RGB conversion using the linear domain. + * + *
+     * Primaries:       x       y
+     *  green           0.170   0.797
+     *  blue            0.131   0.046
+     *  red             0.708   0.292
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT2020_CONSTANT_LUMINANCE = 7 << 16; + /** + * Use the unadjusted {@code KR = 0.30}, {@code KB = 0.11} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x      y
+     *  green           0.21   0.71
+     *  blue            0.14   0.08
+     *  red             0.67   0.33
+     *  white (C)       0.310  0.316 
+ */ + public static final int STANDARD_BT470M = 8 << 16; + /** + * Use the unadjusted {@code KR = 0.254}, {@code KB = 0.068} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.243   0.692
+     *  blue            0.145   0.049
+     *  red             0.681   0.319
+     *  white (C)       0.310   0.316 
+ */ + public static final int STANDARD_FILM = 9 << 16; + /** + * SMPTE EG 432-1 and SMPTE RP 431-2. + * + *
+     * Primaries:       x       y
+     *  green           0.265   0.690
+     *  blue            0.150   0.060
+     *  red             0.680   0.320
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_DCI_P3 = 10 << 16; + /** + * Adobe RGB primaries. + * + *
+     * Primaries:       x       y
+     *  green           0.210   0.710
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_ADOBE_RGB = 11 << 16; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + TRANSFER_UNSPECIFIED, + TRANSFER_LINEAR, + TRANSFER_SRGB, + TRANSFER_SMPTE_170M, + TRANSFER_GAMMA2_2, + TRANSFER_GAMMA2_6, + TRANSFER_GAMMA2_8, + TRANSFER_ST2084, + TRANSFER_HLG + }) + public @interface DataSpaceTransfer {}; + private static final int TRANSFER_MASK = 31 << 22; + /** + * Transfer characteristics are unknown or are determined by the + * application. + */ + public static final int TRANSFER_UNSPECIFIED = 0 << 22; + /** + * Linear transfer. + * + *
{@code
+     * Transfer characteristic curve:
+     *  E = L
+     *      L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *      E - corresponding electrical signal}
+ */ + public static final int TRANSFER_LINEAR = 1 << 22; + /** + * sRGB transfer. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = 1.055 * L^(1/2.4) - 0.055  for 0.0031308 <= L <= 1
+     *   = 12.92 * L                  for 0 <= L < 0.0031308
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ * + * Use for RGB formats. + */ + public static final int TRANSFER_SRGB = 2 << 22; + /** + * SMPTE 170M transfer. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = 1.099 * L ^ 0.45 - 0.099  for 0.018 <= L <= 1
+     *   = 4.500 * L                 for 0 <= L < 0.018
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ * + * Use for YCbCr formats. + */ + public static final int TRANSFER_SMPTE_170M = 3 << 22; + /** + * Display gamma 2.2. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = L ^ (1/2.2)
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_GAMMA2_2 = 4 << 22; + /** + * Display gamma 2.6. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = L ^ (1/2.6)
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_GAMMA2_6 = 5 << 22; + /** + * Display gamma 2.8. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = L ^ (1/2.8)
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_GAMMA2_8 = 6 << 22; + /** + * SMPTE ST 2084 (Dolby Perceptual Quantizer). + * + *
{@code
+     * Transfer characteristic curve:
+     * E = ((c1 + c2 * L^n) / (1 + c3 * L^n)) ^ m
+     * c1 = c3 - c2 + 1 = 3424 / 4096 = 0.8359375
+     * c2 = 32 * 2413 / 4096 = 18.8515625
+     * c3 = 32 * 2392 / 4096 = 18.6875
+     * m = 128 * 2523 / 4096 = 78.84375
+     * n = 0.25 * 2610 / 4096 = 0.1593017578125
+     *     L - luminance of image 0 <= L <= 1 for HDR colorimetry.
+     *         L = 1 corresponds to 10000 cd/m2
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_ST2084 = 7 << 22; + /** + * ARIB STD-B67 Hybrid Log Gamma. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = r * L^0.5                 for 0 <= L <= 1
+     *   = a * ln(L - b) + c         for 1 < L
+     * a = 0.17883277
+     * b = 0.28466892
+     * c = 0.55991073
+     * r = 0.5
+     *     L - luminance of image 0 <= L for HDR colorimetry. L = 1 corresponds
+     *         to reference white level of 100 cd/m2
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_HLG = 8 << 22; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + RANGE_UNSPECIFIED, + RANGE_FULL, + RANGE_LIMITED, + RANGE_EXTENDED + }) + public @interface DataSpaceRange {}; + private static final int RANGE_MASK = 7 << 27; + /** + * Range characteristics are unknown or are determined by the application. + */ + public static final int RANGE_UNSPECIFIED = 0 << 27; + /** + * Full range uses all values for Y, Cb and Cr from + * {@code 0} to {@code 2^b-1}, where b is the bit depth of the color format. + */ + public static final int RANGE_FULL = 1 << 27; + /** + * Limited range uses values {@code 16/256*2^b} to {@code 235/256*2^b} for Y, and + * {@code 1/16*2^b} to {@code 15/16*2^b} for Cb, Cr, R, G and B, where b is the bit depth of + * the color format. + * + *

E.g. For 8-bit-depth formats: + * Luma (Y) samples should range from 16 to 235, inclusive + * Chroma (Cb, Cr) samples should range from 16 to 240, inclusive + * + * For 10-bit-depth formats: + * Luma (Y) samples should range from 64 to 940, inclusive + * Chroma (Cb, Cr) samples should range from 64 to 960, inclusive.

+ */ + public static final int RANGE_LIMITED = 2 << 27; + /** + * Extended range is used for scRGB only. + * + *

Intended for use with floating point pixel formats. [0.0 - 1.0] is the standard + * sRGB space. Values outside the range [0.0 - 1.0] can encode + * color outside the sRGB gamut. [-0.5, 7.5] is the scRGB range. + * Used to blend/merge multiple dataspaces on a single display.

+ */ + public static final int RANGE_EXTENDED = 3 << 27; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + DATASPACE_UNKNOWN, + DATASPACE_SCRGB_LINEAR, + DATASPACE_SRGB, + DATASPACE_SCRGB, + DATASPACE_DISPLAY_P3, + DATASPACE_BT2020_PQ, + DATASPACE_ADOBE_RGB, + DATASPACE_JFIF, + DATASPACE_BT601_625, + DATASPACE_BT601_525, + DATASPACE_BT2020, + DATASPACE_BT709, + DATASPACE_DCI_P3, + DATASPACE_SRGB_LINEAR + }) + public @interface NamedDataSpace {}; + /** + * Default-assumption data space, when not explicitly specified. + * + *

It is safest to assume a buffer is an image with sRGB primaries and + * encoding ranges, but the consumer and/or the producer of the data may + * simply be using defaults. No automatic gamma transform should be + * expected, except for a possible display gamma transform when drawn to a + * screen.

+ */ + public static final int DATASPACE_UNKNOWN = 0; + /** + * scRGB linear encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_LINEAR
+     *   Range: RANGE_EXTENDED
+ * + * The values are floating point. + * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. + * Values beyond the range [0.0 - 1.0] would correspond to other colors + * spaces and/or HDR content. + */ + public static final int DATASPACE_SCRGB_LINEAR = 406913024; + /** + * sRGB gamma encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_SRGB
+     *   Range: RANGE_FULL
+ * + * When written, the inverse transformation is performed. + * + * The alpha component, if present, is always stored in linear space and + * is left unmodified when read or written. + */ + public static final int DATASPACE_SRGB = 142671872; + /** + * scRGB gamma encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_SRGB
+     *   Range: RANGE_EXTENDED
+ * + * The values are floating point. + * + * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. + * Values beyond the range [0.0 - 1.0] would correspond to other colors + * spaces and/or HDR content. + */ + public static final int DATASPACE_SCRGB = 411107328; + /** + * Display P3 encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_DCI_P3
+     *   Transfer: TRANSFER_SRGB
+     *   Range: RANGE_FULL
+ */ + public static final int DATASPACE_DISPLAY_P3 = 143261696; + /** + * ITU-R Recommendation 2020 (BT.2020) + * + * Ultra High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT2020
+     *   Transfer: TRANSFER_ST2084
+     *   Range: RANGE_FULL
+ */ + public static final int DATASPACE_BT2020_PQ = 163971072; + /** + * Adobe RGB encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_ADOBE_RGB
+     *   Transfer: TRANSFER_GAMMA2_2
+     *   Range: RANGE_FULL
+ * + * Note: Application is responsible for gamma encoding the data. + */ + public static final int DATASPACE_ADOBE_RGB = 151715840; + /** + * JPEG File Interchange Format (JFIF). + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT601_625
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_FULL
+ * + * Same model as BT.601-625, but all values (Y, Cb, Cr) range from {@code 0} to {@code 255} + */ + public static final int DATASPACE_JFIF = 146931712; + /** + * ITU-R Recommendation 601 (BT.601) - 525-line + * + * Standard-definition television, 525 Lines (NTSC). + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT601_625
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_LIMITED
+ */ + public static final int DATASPACE_BT601_625 = 281149440; + /** + * ITU-R Recommendation 709 (BT.709) + * + * High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT601_525
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_LIMITED
+ */ + public static final int DATASPACE_BT601_525 = 281280512; + /** + * ITU-R Recommendation 2020 (BT.2020) + * + * Ultra High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT2020
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_FULL
+ */ + public static final int DATASPACE_BT2020 = 147193856; + /** + * ITU-R Recommendation 709 (BT.709) + * + * High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_LIMITED
+ */ + public static final int DATASPACE_BT709 = 281083904; + /** + * SMPTE EG 432-1 and SMPTE RP 431-2 + * + * Digital Cinema DCI-P3. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_DCI_P3
+     *   Transfer: TRANSFER_GAMMA2_6
+     *   Range: RANGE_FULL
+ * + * Note: Application is responsible for gamma encoding the data as + * a 2.6 gamma encoding is not supported in HW. + */ + public static final int DATASPACE_DCI_P3 = 155844608; + /** + * sRGB linear encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_LINEAR
+     *   Range: RANGE_FULL
+ * + * The values are encoded using the full range ([0,255] for 8-bit) for all + * components. + */ + public static final int DATASPACE_SRGB_LINEAR = 138477568; + private DataSpace() {} + /** + * Pack the dataSpace value using standard, transfer and range field value. + * Field values should be in the correct bits place. + * + * @param standard Chromaticity coordinates of source primaries + * @param transfer Opto-electronic transfer characteristic at the source + * @param range The range of values + * + * @return The int dataspace packed by standard, transfer and range value + */ + public static @NamedDataSpace int pack(@DataSpaceStandard int standard, + @DataSpaceTransfer int transfer, + @DataSpaceRange int range) { + if ((standard & STANDARD_MASK) != standard) { + throw new IllegalArgumentException("Invalid standard " + standard); + } + if ((transfer & TRANSFER_MASK) != transfer) { + throw new IllegalArgumentException("Invalid transfer " + transfer); + } + if ((range & RANGE_MASK) != range) { + throw new IllegalArgumentException("Invalid range " + range); + } + return standard | transfer | range; + } + /** + * Unpack the standard field value from the packed dataSpace value. + * + * @param dataSpace The packed dataspace value + * + * @return The standard aspect + */ + public static @DataSpaceStandard int getStandard(@NamedDataSpace int dataSpace) { + @DataSpaceStandard int standard = dataSpace & STANDARD_MASK; + return standard; + } + /** + * Unpack the transfer field value from the packed dataSpace value + * + * @param dataSpace The packed dataspace value + * + * @return The transfer aspect + */ + public static @DataSpaceTransfer int getTransfer(@NamedDataSpace int dataSpace) { + @DataSpaceTransfer int transfer = dataSpace & TRANSFER_MASK; + return transfer; + } + /** + * Unpack the range field value from the packed dataSpace value + * + * @param dataSpace The packed dataspace value + * + * @return The range aspect + */ + public static @DataSpaceRange int getRange(@NamedDataSpace int dataSpace) { + @DataSpaceRange int range = dataSpace & RANGE_MASK; + return range; + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/util/ArraySet.java b/Common/src/main/java/android/util/ArraySet.java new file mode 100644 index 00000000..97644c4b --- /dev/null +++ b/Common/src/main/java/android/util/ArraySet.java @@ -0,0 +1,903 @@ +/* + * Copyright (C) 2013 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 android.util; +import android.annotation.Nullable; + +import libcore.util.EmptyArray; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +/** + * ArraySet is a generic set data structure that is designed to be more memory efficient than a + * traditional {@link java.util.HashSet}. The design is very similar to + * {link ArrayMap}, with all of the caveats described there. This implementation is + * separate from ArrayMap, however, so the Object array contains only one item for each + * entry in the set (instead of a pair for a mapping). + * + *

Note that this implementation is not intended to be appropriate for data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashSet, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

+ * + *

Because this container is intended to better balance memory use, unlike most other + * standard Java containers it will shrink its array as items are removed from it. Currently + * you have no control over this shrinking -- if you set a capacity and then remove an + * item, it may reduce the capacity to better match the current size. In the future an + * explicit call to set the capacity should turn off this aggressive shrinking behavior.

+ * + *

This structure is NOT thread-safe.

+ */ +public final class ArraySet implements Collection, Set { + private static final boolean DEBUG = false; + private static final String TAG = "ArraySet"; + /** + * The minimum amount by which the capacity of a ArraySet will increase. + * This is tuned to be relatively space-efficient. + */ + private static final int BASE_SIZE = 4; + /** + * Maximum number of entries to have in array caches. + */ + private static final int CACHE_SIZE = 10; + /** + * Caches of small array objects to avoid spamming garbage. The cache + * Object[] variable is a pointer to a linked list of array objects. + * The first entry in the array is a pointer to the next array in the + * list; the second entry is a pointer to the int[] hash code array for it. + */ + static Object[] sBaseCache; + static int sBaseCacheSize; + static Object[] sTwiceBaseCache; + static int sTwiceBaseCacheSize; + /** + * Separate locks for each cache since each can be accessed independently of the other without + * risk of a deadlock. + */ + private static final Object sBaseCacheLock = new Object(); + private static final Object sTwiceBaseCacheLock = new Object(); + private final boolean mIdentityHashCode; + + int[] mHashes; + + Object[] mArray; + + int mSize; + + private MapCollections mCollections; + + private int binarySearch(int[] hashes, int hash) { + try { + return ContainerHelpers.binarySearch(hashes, mSize, hash); + } catch (ArrayIndexOutOfBoundsException e) { + throw new ConcurrentModificationException(); + } + } + + private int indexOf(Object key, int hash) { + final int N = mSize; + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + int index = binarySearch(mHashes, hash); + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + // If the key at the returned index matches, that's what we want. + if (key.equals(mArray[index])) { + return index; + } + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == hash; end++) { + if (key.equals(mArray[end])) return end; + } + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { + if (key.equals(mArray[i])) return i; + } + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private int indexOfNull() { + final int N = mSize; + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + int index = binarySearch(mHashes, 0); + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + // If the key at the returned index matches, that's what we want. + if (null == mArray[index]) { + return index; + } + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == 0; end++) { + if (null == mArray[end]) return end; + } + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { + if (null == mArray[i]) return i; + } + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private void allocArrays(final int size) { + if (size == (BASE_SIZE * 2)) { + synchronized (sTwiceBaseCacheLock) { + if (sTwiceBaseCache != null) { + final Object[] array = sTwiceBaseCache; + try { + mArray = array; + sTwiceBaseCache = (Object[]) array[0]; + mHashes = (int[]) array[1]; + if (mHashes != null) { + array[0] = array[1] = null; + sTwiceBaseCacheSize--; + if (DEBUG) { + // Log.d(TAG, "Retrieving 2x cache " + Arrays.toString(mHashes) + // + " now have " + sTwiceBaseCacheSize + " entries"); + } + return; + } + } catch (ClassCastException e) { + } + // Whoops! Someone trampled the array (probably due to not protecting + // their access with a lock). Our cache is corrupt; report and give up. + //Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0] + // + " [1]=" + array[1]); + sTwiceBaseCache = null; + sTwiceBaseCacheSize = 0; + } + } + } else if (size == BASE_SIZE) { + synchronized (sBaseCacheLock) { + if (sBaseCache != null) { + final Object[] array = sBaseCache; + try { + mArray = array; + sBaseCache = (Object[]) array[0]; + mHashes = (int[]) array[1]; + if (mHashes != null) { + array[0] = array[1] = null; + sBaseCacheSize--; + if (DEBUG) { + //Log.d(TAG, "Retrieving 1x cache " + Arrays.toString(mHashes) + // + " now have " + sBaseCacheSize + " entries"); + } + return; + } + } catch (ClassCastException e) { + } + // Whoops! Someone trampled the array (probably due to not protecting + // their access with a lock). Our cache is corrupt; report and give up. + //Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0] + // + " [1]=" + array[1]); + sBaseCache = null; + sBaseCacheSize = 0; + } + } + } + mHashes = new int[size]; + mArray = new Object[size]; + } + /** + * Make sure NOT to call this method with arrays that can still be modified. In other + * words, don't pass mHashes or mArray in directly. + */ + private static void freeArrays(final int[] hashes, final Object[] array, final int size) { + if (hashes.length == (BASE_SIZE * 2)) { + synchronized (sTwiceBaseCacheLock) { + if (sTwiceBaseCacheSize < CACHE_SIZE) { + array[0] = sTwiceBaseCache; + array[1] = hashes; + for (int i = size - 1; i >= 2; i--) { + array[i] = null; + } + sTwiceBaseCache = array; + sTwiceBaseCacheSize++; + if (DEBUG) { + //Log.d(TAG, "Storing 2x cache " + Arrays.toString(array) + " now have " + // + sTwiceBaseCacheSize + " entries"); + } + } + } + } else if (hashes.length == BASE_SIZE) { + synchronized (sBaseCacheLock) { + if (sBaseCacheSize < CACHE_SIZE) { + array[0] = sBaseCache; + array[1] = hashes; + for (int i = size - 1; i >= 2; i--) { + array[i] = null; + } + sBaseCache = array; + sBaseCacheSize++; + if (DEBUG) { + //Log.d(TAG, "Storing 1x cache " + Arrays.toString(array) + " now have " + // + sBaseCacheSize + " entries"); + } + } + } + } + } + /** + * Create a new empty ArraySet. The default capacity of an array map is 0, and + * will grow once items are added to it. + */ + public ArraySet() { + this(0, false); + } + /** + * Create a new ArraySet with a given initial capacity. + */ + public ArraySet(int capacity) { + this(capacity, false); + } + /** {@hide} */ + public ArraySet(int capacity, boolean identityHashCode) { + mIdentityHashCode = identityHashCode; + if (capacity == 0) { + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + } else { + allocArrays(capacity); + } + mSize = 0; + } + /** + * Create a new ArraySet with the mappings from the given ArraySet. + */ + public ArraySet(ArraySet set) { + this(); + if (set != null) { + addAll(set); + } + } + /** + * Create a new ArraySet with items from the given collection. + */ + public ArraySet(Collection set) { + this(); + if (set != null) { + addAll(set); + } + } + /** + * Create a new ArraySet with items from the given array + */ + public ArraySet(@Nullable E[] array) { + this(); + if (array != null) { + for (E value : array) { + add(value); + } + } + } + /** + * Make the array map empty. All storage is released. + */ + @Override + public void clear() { + if (mSize != 0) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + final int osize = mSize; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + freeArrays(ohashes, oarray, osize); + } + if (mSize != 0) { + throw new ConcurrentModificationException(); + } + } + /** + * Ensure the array map can hold at least minimumCapacity + * items. + */ + public void ensureCapacity(int minimumCapacity) { + final int oSize = mSize; + if (mHashes.length < minimumCapacity) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(minimumCapacity); + if (mSize > 0) { + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize); + } + freeArrays(ohashes, oarray, mSize); + } + if (mSize != oSize) { + throw new ConcurrentModificationException(); + } + } + /** + * Check whether a value exists in the set. + * + * @param key The value to search for. + * @return Returns true if the value exists, else false. + */ + @Override + public boolean contains(Object key) { + return indexOf(key) >= 0; + } + /** + * Returns the index of a value in the set. + * + * @param key The value to search for. + * @return Returns the index of the value if it exists, else a negative integer. + */ + public int indexOf(Object key) { + return key == null ? indexOfNull() + : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode()); + } + /** + * Return the value at the given index in the array. + * + *

For indices outside of the range 0...size()-1, the behavior is undefined for + * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {link android.os.Build.VERSION_CODES#Q} and later.

+ * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public E valueAt(int index) { + if (index >= mSize) { + // The array might be slightly bigger than mSize, in which case, indexing won't fail. + // Check if exception should be thrown outside of the critical path. + throw new ArrayIndexOutOfBoundsException(index); + } + return valueAtUnchecked(index); + } + /** + * Returns the value at the given index in the array without checking that the index is within + * bounds. This allows testing values at the end of the internal array, outside of the + * [0, mSize) bounds. + * + * @hide + */ + public E valueAtUnchecked(int index) { + return (E) mArray[index]; + } + /** + * Return true if the array map contains no items. + */ + @Override + public boolean isEmpty() { + return mSize <= 0; + } + /** + * Adds the specified object to this set. The set is not modified if it + * already contains the object. + * + * @param value the object to add. + * @return {@code true} if this set is modified, {@code false} otherwise. + */ + @Override + public boolean add(E value) { + final int oSize = mSize; + final int hash; + int index; + if (value == null) { + hash = 0; + index = indexOfNull(); + } else { + hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode(); + index = indexOf(value, hash); + } + if (index >= 0) { + return false; + } + index = ~index; + if (oSize >= mHashes.length) { + final int n = oSize >= (BASE_SIZE * 2) ? (oSize + (oSize >> 1)) + : (oSize >= BASE_SIZE ? (BASE_SIZE * 2) : BASE_SIZE); + //if (DEBUG) Log.d(TAG, "add: grow from " + mHashes.length + " to " + n); + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + if (oSize != mSize) { + throw new ConcurrentModificationException(); + } + if (mHashes.length > 0) { + //if (DEBUG) Log.d(TAG, "add: copy 0-" + oSize + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); + System.arraycopy(oarray, 0, mArray, 0, oarray.length); + } + freeArrays(ohashes, oarray, oSize); + } + if (index < oSize) { + if (DEBUG) { + //Log.d(TAG, "add: move " + index + "-" + (oSize - index) + " to " + (index + 1)); + } + System.arraycopy(mHashes, index, mHashes, index + 1, oSize - index); + System.arraycopy(mArray, index, mArray, index + 1, oSize - index); + } + if (oSize != mSize || index >= mHashes.length) { + throw new ConcurrentModificationException(); + } + mHashes[index] = hash; + mArray[index] = value; + mSize++; + return true; + } + /** + * Special fast path for appending items to the end of the array without validation. + * The array must already be large enough to contain the item. + * @hide + */ + public void append(E value) { + final int oSize = mSize; + final int index = mSize; + final int hash = value == null ? 0 + : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode()); + if (index >= mHashes.length) { + throw new IllegalStateException("Array is full"); + } + if (index > 0 && mHashes[index - 1] > hash) { + // Cannot optimize since it would break the sorted order - fallback to add() + if (DEBUG) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + //Log.w(TAG, "New hash " + hash + // + " is before end of array hash " + mHashes[index - 1] + // + " at index " + index, e); + } + add(value); + return; + } + if (oSize != mSize) { + throw new ConcurrentModificationException(); + } + mSize = index + 1; + mHashes[index] = hash; + mArray[index] = value; + } + /** + * Perform a {@link #add(Object)} of all values in array + * @param array The array whose contents are to be retrieved. + */ + public void addAll(ArraySet array) { + final int N = array.mSize; + ensureCapacity(mSize + N); + if (mSize == 0) { + if (N > 0) { + System.arraycopy(array.mHashes, 0, mHashes, 0, N); + System.arraycopy(array.mArray, 0, mArray, 0, N); + if (0 != mSize) { + throw new ConcurrentModificationException(); + } + mSize = N; + } + } else { + for (int i = 0; i < N; i++) { + add(array.valueAt(i)); + } + } + } + /** + * Removes the specified object from this set. + * + * @param object the object to remove. + * @return {@code true} if this set was modified, {@code false} otherwise. + */ + @Override + public boolean remove(Object object) { + final int index = indexOf(object); + if (index >= 0) { + removeAt(index); + return true; + } + return false; + } + /** Returns true if the array size should be decreased. */ + private boolean shouldShrink() { + return mHashes.length > (BASE_SIZE * 2) && mSize < mHashes.length / 3; + } + /** + * Returns the new size the array should have. Is only valid if {@link #shouldShrink} returns + * true. + */ + private int getNewShrunkenSize() { + // We don't allow it to shrink smaller than (BASE_SIZE*2) to avoid flapping between that + // and BASE_SIZE. + return mSize > (BASE_SIZE * 2) ? (mSize + (mSize >> 1)) : (BASE_SIZE * 2); + } + /** + * Remove the key/value mapping at the given index. + * + *

For indices outside of the range 0...size()-1, the behavior is undefined for + * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {link android.os.Build.VERSION_CODES#Q} and later.

+ * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public E removeAt(int index) { + if (index >= mSize) { + // The array might be slightly bigger than mSize, in which case, indexing won't fail. + // Check if exception should be thrown outside of the critical path. + throw new ArrayIndexOutOfBoundsException(index); + } + final int oSize = mSize; + final Object old = mArray[index]; + if (oSize <= 1) { + // Now empty. + //if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); + clear(); + } else { + final int nSize = oSize - 1; + if (shouldShrink()) { + // Shrunk enough to reduce size of arrays. + final int n = getNewShrunkenSize(); + //if (DEBUG) //Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + if (index > 0) { + //if (DEBUG) //Log.d(TAG, "remove: copy from 0-" + index + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, index); + System.arraycopy(oarray, 0, mArray, 0, index); + } + if (index < nSize) { + if (DEBUG) { + //Log.d(TAG, "remove: copy from " + (index + 1) + "-" + nSize + // + " to " + index); + } + System.arraycopy(ohashes, index + 1, mHashes, index, nSize - index); + System.arraycopy(oarray, index + 1, mArray, index, nSize - index); + } + } else { + if (index < nSize) { + if (DEBUG) { + //Log.d(TAG, "remove: move " + (index + 1) + "-" + nSize + " to " + index); + } + System.arraycopy(mHashes, index + 1, mHashes, index, nSize - index); + System.arraycopy(mArray, index + 1, mArray, index, nSize - index); + } + mArray[nSize] = null; + } + if (oSize != mSize) { + throw new ConcurrentModificationException(); + } + mSize = nSize; + } + return (E) old; + } + /** + * Perform a {@link #remove(Object)} of all values in array + * @param array The array whose contents are to be removed. + */ + public boolean removeAll(ArraySet array) { + // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first + // pass, use the property that the sets are sorted by hash to make this linear passes + // (except for hash collisions, which means worst case still n*m), then do one + // collection pass into a new array. This avoids binary searches and excessive memcpy. + final int N = array.mSize; + // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all + // the single results, compare size before and after. + final int originalSize = mSize; + for (int i = 0; i < N; i++) { + remove(array.valueAt(i)); + } + return originalSize != mSize; + } + /** + * Removes all values that satisfy the predicate. This implementation avoids using the + * {@link #iterator()}. + * + * @param filter A predicate which returns true for elements to be removed + */ + @Override + public boolean removeIf(Predicate filter) { + if (mSize == 0) { + return false; + } + // Intentionally not using removeAt() to avoid unnecessary intermediate resizing. + int replaceIndex = 0; + int numRemoved = 0; + for (int i = 0; i < mSize; ++i) { + if (filter.test((E) mArray[i])) { + numRemoved++; + } else { + if (replaceIndex != i) { + mArray[replaceIndex] = mArray[i]; + mHashes[replaceIndex] = mHashes[i]; + } + replaceIndex++; + } + } + if (numRemoved == 0) { + return false; + } else if (numRemoved == mSize) { + clear(); + return true; + } + mSize -= numRemoved; + if (shouldShrink()) { + // Shrunk enough to reduce size of arrays. + final int n = getNewShrunkenSize(); + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize); + } else { + // Null out values at the end of the array. Not doing it in the loop above to avoid + // writing twice to the same index or writing unnecessarily if the array would have been + // discarded anyway. + for (int i = mSize; i < mArray.length; ++i) { + mArray[i] = null; + } + } + return true; + } + /** + * Return the number of items in this array map. + */ + @Override + public int size() { + return mSize; + } + /** + * Performs the given action for all elements in the stored order. This implementation overrides + * the default implementation to avoid using the {@link #iterator()}. + * + * @param action The action to be performed for each element + */ + @Override + public void forEach(Consumer action) { + if (action == null) { + throw new NullPointerException("action must not be null"); + } + for (int i = 0; i < mSize; ++i) { + action.accept(valueAt(i)); + } + } + @Override + public Object[] toArray() { + Object[] result = new Object[mSize]; + System.arraycopy(mArray, 0, result, 0, mSize); + return result; + } + @Override + public T[] toArray(T[] array) { + if (array.length < mSize) { + @SuppressWarnings("unchecked") T[] newArray = + (T[]) Array.newInstance(array.getClass().getComponentType(), mSize); + array = newArray; + } + System.arraycopy(mArray, 0, array, 0, mSize); + if (array.length > mSize) { + array[mSize] = null; + } + return array; + } + /** + * {@inheritDoc} + * + *

This implementation returns false if the object is not a set, or + * if the sets have different sizes. Otherwise, for each value in this + * set, it checks to make sure the value also exists in the other set. + * If any value doesn't exist, the method returns false; otherwise, it + * returns true. + */ + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } + if (object instanceof Set) { + Set set = (Set) object; + if (size() != set.size()) { + return false; + } + try { + for (int i = 0; i < mSize; i++) { + E mine = valueAt(i); + if (!set.contains(mine)) { + return false; + } + } + } catch (NullPointerException ignored) { + return false; + } catch (ClassCastException ignored) { + return false; + } + return true; + } + return false; + } + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + final int[] hashes = mHashes; + int result = 0; + for (int i = 0, s = mSize; i < s; i++) { + result += hashes[i]; + } + return result; + } + /** + * {@inheritDoc} + * + *

This implementation composes a string by iterating over its values. If + * this set contains itself as a value, the string "(this Set)" + * will appear in its place. + */ + @Override + public String toString() { + if (isEmpty()) { + return "{}"; + } + StringBuilder buffer = new StringBuilder(mSize * 14); + buffer.append('{'); + for (int i = 0; i < mSize; i++) { + if (i > 0) { + buffer.append(", "); + } + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Set)"); + } + } + buffer.append('}'); + return buffer.toString(); + } + // ------------------------------------------------------------------------ + // Interop with traditional Java containers. Not as efficient as using + // specialized collection APIs. + // ------------------------------------------------------------------------ + private MapCollections getCollection() { + if (mCollections == null) { + mCollections = new MapCollections() { + @Override + protected int colGetSize() { + return mSize; + } + @Override + protected Object colGetEntry(int index, int offset) { + return mArray[index]; + } + @Override + protected int colIndexOfKey(Object key) { + return indexOf(key); + } + @Override + protected int colIndexOfValue(Object value) { + return indexOf(value); + } + @Override + protected Map colGetMap() { + throw new UnsupportedOperationException("not a map"); + } + @Override + protected void colPut(E key, E value) { + add(key); + } + @Override + protected E colSetValue(int index, E value) { + throw new UnsupportedOperationException("not a map"); + } + @Override + protected void colRemoveAt(int index) { + removeAt(index); + } + @Override + protected void colClear() { + clear(); + } + }; + } + return mCollections; + } + /** + * Return an {@link java.util.Iterator} over all values in the set. + * + *

Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects and allocates additional state + * information associated with the container that will remain for the life of the container.

+ */ + @Override + public Iterator iterator() { + return getCollection().getKeySet().iterator(); + } + /** + * Determine if the array set contains all of the values in the given collection. + * @param collection The collection whose contents are to be checked against. + * @return Returns true if this array set contains a value for every entry + * in collection, else returns false. + */ + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + /** + * Perform an {@link #add(Object)} of all values in collection + * @param collection The collection whose contents are to be retrieved. + */ + @Override + public boolean addAll(Collection collection) { + ensureCapacity(mSize + collection.size()); + boolean added = false; + for (E value : collection) { + added |= add(value); + } + return added; + } + /** + * Remove all values in the array set that exist in the given collection. + * @param collection The collection whose contents are to be used to remove values. + * @return Returns true if any values were removed from the array set, else false. + */ + @Override + public boolean removeAll(Collection collection) { + boolean removed = false; + for (Object value : collection) { + removed |= remove(value); + } + return removed; + } + /** + * Remove all values in the array set that do not exist in the given collection. + * @param collection The collection whose contents are to be used to determine which + * values to keep. + * @return Returns true if any values were removed from the array set, else false. + */ + @Override + public boolean retainAll(Collection collection) { + boolean removed = false; + for (int i = mSize - 1; i >= 0; i--) { + if (!collection.contains(mArray[i])) { + removeAt(i); + removed = true; + } + } + return removed; + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/util/ContainerHelpers.java b/Common/src/main/java/android/util/ContainerHelpers.java new file mode 100644 index 00000000..de76e430 --- /dev/null +++ b/Common/src/main/java/android/util/ContainerHelpers.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 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 android.util; +class ContainerHelpers { + // This is Arrays.binarySearch(), but doesn't do any argument validation. + static int binarySearch(int[] array, int size, int value) { + int lo = 0; + int hi = size - 1; + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final int midVal = array[mid]; + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } + static int binarySearch(long[] array, int size, long value) { + int lo = 0; + int hi = size - 1; + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final long midVal = array[mid]; + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/util/Half.java b/Common/src/main/java/android/util/Half.java new file mode 100644 index 00000000..8ac29a99 --- /dev/null +++ b/Common/src/main/java/android/util/Half.java @@ -0,0 +1,847 @@ +/* + * Copyright (C) 2016 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 android.util; +import android.annotation.HalfFloat; +import android.annotation.NonNull; +import android.annotation.Nullable; +import libcore.util.FP16; +/** + *

The {@code Half} class is a wrapper and a utility class to manipulate half-precision 16-bit + * IEEE 754 + * floating point data types (also called fp16 or binary16). A half-precision float can be + * created from or converted to single-precision floats, and is stored in a short data type. + * To distinguish short values holding half-precision floats from regular short values, + * it is recommended to use the @HalfFloat annotation.

+ * + *

The IEEE 754 standard specifies an fp16 as having the following format:

+ *
    + *
  • Sign bit: 1 bit
  • + *
  • Exponent width: 5 bits
  • + *
  • Significand: 10 bits
  • + *
+ * + *

The format is laid out as follows:

+ *
+ * 1   11111   1111111111
+ * ^   --^--   -----^----
+ * sign  |          |_______ significand
+ *       |
+ *       -- exponent
+ * 
+ * + *

Half-precision floating points can be useful to save memory and/or + * bandwidth at the expense of range and precision when compared to single-precision + * floating points (fp32).

+ *

To help you decide whether fp16 is the right storage type for you need, please + * refer to the table below that shows the available precision throughout the range of + * possible values. The precision column indicates the step size between two + * consecutive numbers in a specific part of the range.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Range startPrecision
01 ⁄ 16,777,216
1 ⁄ 16,3841 ⁄ 16,777,216
1 ⁄ 8,1921 ⁄ 8,388,608
1 ⁄ 4,0961 ⁄ 4,194,304
1 ⁄ 2,0481 ⁄ 2,097,152
1 ⁄ 1,0241 ⁄ 1,048,576
1 ⁄ 5121 ⁄ 524,288
1 ⁄ 2561 ⁄ 262,144
1 ⁄ 1281 ⁄ 131,072
1 ⁄ 641 ⁄ 65,536
1 ⁄ 321 ⁄ 32,768
1 ⁄ 161 ⁄ 16,384
1 ⁄ 81 ⁄ 8,192
1 ⁄ 41 ⁄ 4,096
1 ⁄ 21 ⁄ 2,048
11 ⁄ 1,024
21 ⁄ 512
41 ⁄ 256
81 ⁄ 128
161 ⁄ 64
321 ⁄ 32
641 ⁄ 16
1281 ⁄ 8
2561 ⁄ 4
5121 ⁄ 2
1,0241
2,0482
4,0964
8,1928
16,38416
32,76832
+ * + *

This table shows that numbers higher than 1024 lose all fractional precision.

+ */ +@SuppressWarnings("SimplifiableIfStatement") +public final class Half extends Number implements Comparable { + /** + * The number of bits used to represent a half-precision float value. + */ + public static final int SIZE = 16; + /** + * Epsilon is the difference between 1.0 and the next value representable + * by a half-precision floating-point. + */ + public static final @HalfFloat short EPSILON = (short) 0x1400; + /** + * Maximum exponent a finite half-precision float may have. + */ + public static final int MAX_EXPONENT = 15; + /** + * Minimum exponent a normalized half-precision float may have. + */ + public static final int MIN_EXPONENT = -14; + /** + * Smallest negative value a half-precision float may have. + */ + public static final @HalfFloat short LOWEST_VALUE = (short) 0xfbff; + /** + * Maximum positive finite value a half-precision float may have. + */ + public static final @HalfFloat short MAX_VALUE = (short) 0x7bff; + /** + * Smallest positive normal value a half-precision float may have. + */ + public static final @HalfFloat short MIN_NORMAL = (short) 0x0400; + /** + * Smallest positive non-zero value a half-precision float may have. + */ + public static final @HalfFloat short MIN_VALUE = (short) 0x0001; + /** + * A Not-a-Number representation of a half-precision float. + */ + public static final @HalfFloat short NaN = (short) 0x7e00; + /** + * Negative infinity of type half-precision float. + */ + public static final @HalfFloat short NEGATIVE_INFINITY = (short) 0xfc00; + /** + * Negative 0 of type half-precision float. + */ + public static final @HalfFloat short NEGATIVE_ZERO = (short) 0x8000; + /** + * Positive infinity of type half-precision float. + */ + public static final @HalfFloat short POSITIVE_INFINITY = (short) 0x7c00; + /** + * Positive 0 of type half-precision float. + */ + public static final @HalfFloat short POSITIVE_ZERO = (short) 0x0000; + private final @HalfFloat short mValue; + /** + * Constructs a newly allocated {@code Half} object that represents the + * half-precision float type argument. + * + * @param value The value to be represented by the {@code Half} + */ + public Half(@HalfFloat short value) { + mValue = value; + } + /** + * Constructs a newly allocated {@code Half} object that represents the + * argument converted to a half-precision float. + * + * @param value The value to be represented by the {@code Half} + * + * @see #toHalf(float) + */ + public Half(float value) { + mValue = toHalf(value); + } + /** + * Constructs a newly allocated {@code Half} object that + * represents the argument converted to a half-precision float. + * + * @param value The value to be represented by the {@code Half} + * + * @see #toHalf(float) + */ + public Half(double value) { + mValue = toHalf((float) value); + } + /** + *

Constructs a newly allocated {@code Half} object that represents the + * half-precision float value represented by the string. + * The string is converted to a half-precision float value as if by the + * {@link #valueOf(String)} method.

+ * + *

Calling this constructor is equivalent to calling:

+ *
+     *     new Half(Float.parseFloat(value))
+     * 
+ * + * @param value A string to be converted to a {@code Half} + * @throws NumberFormatException if the string does not contain a parsable number + * + * @see Float#valueOf(java.lang.String) + * @see #toHalf(float) + */ + public Half(@NonNull String value) throws NumberFormatException { + mValue = toHalf(Float.parseFloat(value)); + } + /** + * Returns the half-precision value of this {@code Half} as a {@code short} + * containing the bit representation described in {@link Half}. + * + * @return The half-precision float value represented by this object + */ + public @HalfFloat short halfValue() { + return mValue; + } + /** + * Returns the value of this {@code Half} as a {@code byte} after + * a narrowing primitive conversion. + * + * @return The half-precision float value represented by this object + * converted to type {@code byte} + */ + @Override + public byte byteValue() { + return (byte) toFloat(mValue); + } + /** + * Returns the value of this {@code Half} as a {@code short} after + * a narrowing primitive conversion. + * + * @return The half-precision float value represented by this object + * converted to type {@code short} + */ + @Override + public short shortValue() { + return (short) toFloat(mValue); + } + /** + * Returns the value of this {@code Half} as a {@code int} after + * a narrowing primitive conversion. + * + * @return The half-precision float value represented by this object + * converted to type {@code int} + */ + @Override + public int intValue() { + return (int) toFloat(mValue); + } + /** + * Returns the value of this {@code Half} as a {@code long} after + * a narrowing primitive conversion. + * + * @return The half-precision float value represented by this object + * converted to type {@code long} + */ + @Override + public long longValue() { + return (long) toFloat(mValue); + } + /** + * Returns the value of this {@code Half} as a {@code float} after + * a widening primitive conversion. + * + * @return The half-precision float value represented by this object + * converted to type {@code float} + */ + @Override + public float floatValue() { + return toFloat(mValue); + } + /** + * Returns the value of this {@code Half} as a {@code double} after + * a widening primitive conversion. + * + * @return The half-precision float value represented by this object + * converted to type {@code double} + */ + @Override + public double doubleValue() { + return toFloat(mValue); + } + /** + * Returns true if this {@code Half} value represents a Not-a-Number, + * false otherwise. + * + * @return True if the value is a NaN, false otherwise + */ + public boolean isNaN() { + return isNaN(mValue); + } + /** + * Compares this object against the specified object. The result is {@code true} + * if and only if the argument is not {@code null} and is a {@code Half} object + * that represents the same half-precision value as the this object. Two + * half-precision values are considered to be the same if and only if the method + * {@link #halfToIntBits(short)} returns an identical {@code int} value for both. + * + * @param o The object to compare + * @return True if the objects are the same, false otherwise + * + * @see #halfToIntBits(short) + */ + @Override + public boolean equals(@Nullable Object o) { + return (o instanceof Half) && + (halfToIntBits(((Half) o).mValue) == halfToIntBits(mValue)); + } + /** + * Returns a hash code for this {@code Half} object. The result is the + * integer bit representation, exactly as produced by the method + * {@link #halfToIntBits(short)}, of the primitive half-precision float + * value represented by this {@code Half} object. + * + * @return A hash code value for this object + */ + @Override + public int hashCode() { + return hashCode(mValue); + } + /** + * Returns a string representation of the specified half-precision + * float value. See {@link #toString(short)} for more information. + * + * @return A string representation of this {@code Half} object + */ + @NonNull + @Override + public String toString() { + return toString(mValue); + } + /** + *

Compares the two specified half-precision float values. The following + * conditions apply during the comparison:

+ * + *
    + *
  • {@link #NaN} is considered by this method to be equal to itself and greater + * than all other half-precision float values (including {@code #POSITIVE_INFINITY})
  • + *
  • {@link #POSITIVE_ZERO} is considered by this method to be greater than + * {@link #NEGATIVE_ZERO}.
  • + *
+ * + * @param h The half-precision float value to compare to the half-precision value + * represented by this {@code Half} object + * + * @return The value {@code 0} if {@code x} is numerically equal to {@code y}; a + * value less than {@code 0} if {@code x} is numerically less than {@code y}; + * and a value greater than {@code 0} if {@code x} is numerically greater + * than {@code y} + */ + @Override + public int compareTo(@NonNull Half h) { + return compare(mValue, h.mValue); + } + /** + * Returns a hash code for a half-precision float value. + * + * @param h The value to hash + * + * @return A hash code value for a half-precision float value + */ + public static int hashCode(@HalfFloat short h) { + return halfToIntBits(h); + } + /** + *

Compares the two specified half-precision float values. The following + * conditions apply during the comparison:

+ * + *
    + *
  • {@link #NaN} is considered by this method to be equal to itself and greater + * than all other half-precision float values (including {@code #POSITIVE_INFINITY})
  • + *
  • {@link #POSITIVE_ZERO} is considered by this method to be greater than + * {@link #NEGATIVE_ZERO}.
  • + *
+ * + * @param x The first half-precision float value to compare. + * @param y The second half-precision float value to compare + * + * @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a + * value less than {@code 0} if {@code x} is numerically less than {@code y}, + * and a value greater than {@code 0} if {@code x} is numerically greater + * than {@code y} + */ + public static int compare(@HalfFloat short x, @HalfFloat short y) { + return FP16.compare(x, y); + } + /** + *

Returns a representation of the specified half-precision float value + * according to the bit layout described in {@link Half}.

+ * + *

Similar to {@link #halfToIntBits(short)}, this method collapses all + * possible Not-a-Number values to a single canonical Not-a-Number value + * defined by {@link #NaN}.

+ * + * @param h A half-precision float value + * @return The bits that represent the half-precision float value + * + * @see #halfToIntBits(short) + */ + public static @HalfFloat short halfToShortBits(@HalfFloat short h) { + return (h & FP16.EXPONENT_SIGNIFICAND_MASK) > FP16.POSITIVE_INFINITY ? NaN : h; + } + /** + *

Returns a representation of the specified half-precision float value + * according to the bit layout described in {@link Half}.

+ * + *

Unlike {@link #halfToRawIntBits(short)}, this method collapses all + * possible Not-a-Number values to a single canonical Not-a-Number value + * defined by {@link #NaN}.

+ * + * @param h A half-precision float value + * @return The bits that represent the half-precision float value + * + * @see #halfToRawIntBits(short) + * @see #halfToShortBits(short) + * @see #intBitsToHalf(int) + */ + public static int halfToIntBits(@HalfFloat short h) { + return (h & FP16.EXPONENT_SIGNIFICAND_MASK) > FP16.POSITIVE_INFINITY ? NaN : h & 0xffff; + } + /** + *

Returns a representation of the specified half-precision float value + * according to the bit layout described in {@link Half}.

+ * + *

The argument is considered to be a representation of a half-precision + * float value according to the bit layout described in {@link Half}. The 16 + * most significant bits of the returned value are set to 0.

+ * + * @param h A half-precision float value + * @return The bits that represent the half-precision float value + * + * @see #halfToIntBits(short) + * @see #intBitsToHalf(int) + */ + public static int halfToRawIntBits(@HalfFloat short h) { + return h & 0xffff; + } + /** + *

Returns the half-precision float value corresponding to a given + * bit representation.

+ * + *

The argument is considered to be a representation of a half-precision + * float value according to the bit layout described in {@link Half}. The 16 + * most significant bits of the argument are ignored.

+ * + * @param bits An integer + * @return The half-precision float value with the same bit pattern + */ + public static @HalfFloat short intBitsToHalf(int bits) { + return (short) (bits & 0xffff); + } + /** + * Returns the first parameter with the sign of the second parameter. + * This method treats NaNs as having a sign. + * + * @param magnitude A half-precision float value providing the magnitude of the result + * @param sign A half-precision float value providing the sign of the result + * @return A value with the magnitude of the first parameter and the sign + * of the second parameter + */ + public static @HalfFloat short copySign(@HalfFloat short magnitude, @HalfFloat short sign) { + return (short) ((sign & FP16.SIGN_MASK) | (magnitude & FP16.EXPONENT_SIGNIFICAND_MASK)); + } + /** + * Returns the absolute value of the specified half-precision float. + * Special values are handled in the following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is positive zero (see {@link #POSITIVE_ZERO})
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is positive infinity (see {@link #POSITIVE_INFINITY})
  • + *
+ * + * @param h A half-precision float value + * @return The absolute value of the specified half-precision float + */ + public static @HalfFloat short abs(@HalfFloat short h) { + return (short) (h & FP16.EXPONENT_SIGNIFICAND_MASK); + } + /** + * Returns the closest integral half-precision float value to the specified + * half-precision float value. Special values are handled in the + * following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + *

+ * Note: Unlike the identically named + * int java.lang.Math.round(float) method, + * this returns a Half value stored in a short, not an + * actual short integer result. + * + * @param h A half-precision float value + * @return The value of the specified half-precision float rounded to the nearest + * half-precision float value + */ + public static @HalfFloat short round(@HalfFloat short h) { + return FP16.rint(h); + } + /** + * Returns the smallest half-precision float value toward negative infinity + * greater than or equal to the specified half-precision float value. + * Special values are handled in the following ways: + *

    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The smallest half-precision float value toward negative infinity + * greater than or equal to the specified half-precision float value + */ + public static @HalfFloat short ceil(@HalfFloat short h) { + return FP16.ceil(h); + } + /** + * Returns the largest half-precision float value toward positive infinity + * less than or equal to the specified half-precision float value. + * Special values are handled in the following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The largest half-precision float value toward positive infinity + * less than or equal to the specified half-precision float value + */ + public static @HalfFloat short floor(@HalfFloat short h) { + return FP16.floor(h); + } + /** + * Returns the truncated half-precision float value of the specified + * half-precision float value. Special values are handled in the following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The truncated half-precision float value of the specified + * half-precision float value + */ + public static @HalfFloat short trunc(@HalfFloat short h) { + return FP16.trunc(h); + } + /** + * Returns the smaller of two half-precision float values (the value closest + * to negative infinity). Special values are handled in the following ways: + *
    + *
  • If either value is NaN, the result is NaN
  • + *
  • {@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}
  • + *
+ * + * @param x The first half-precision value + * @param y The second half-precision value + * @return The smaller of the two specified half-precision values + */ + public static @HalfFloat short min(@HalfFloat short x, @HalfFloat short y) { + return FP16.min(x, y); + } + /** + * Returns the larger of two half-precision float values (the value closest + * to positive infinity). Special values are handled in the following ways: + *
    + *
  • If either value is NaN, the result is NaN
  • + *
  • {@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}
  • + *
+ * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return The larger of the two specified half-precision values + */ + public static @HalfFloat short max(@HalfFloat short x, @HalfFloat short y) { + return FP16.max(x, y); + } + /** + * Returns true if the first half-precision float value is less (smaller + * toward negative infinity) than the second half-precision float value. + * If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is less than y, false otherwise + */ + public static boolean less(@HalfFloat short x, @HalfFloat short y) { + return FP16.less(x, y); + } + /** + * Returns true if the first half-precision float value is less (smaller + * toward negative infinity) than or equal to the second half-precision + * float value. If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is less than or equal to y, false otherwise + */ + public static boolean lessEquals(@HalfFloat short x, @HalfFloat short y) { + return FP16.lessEquals(x, y); + } + /** + * Returns true if the first half-precision float value is greater (larger + * toward positive infinity) than the second half-precision float value. + * If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is greater than y, false otherwise + */ + public static boolean greater(@HalfFloat short x, @HalfFloat short y) { + return FP16.greater(x, y); + } + /** + * Returns true if the first half-precision float value is greater (larger + * toward positive infinity) than or equal to the second half-precision float + * value. If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is greater than y, false otherwise + */ + public static boolean greaterEquals(@HalfFloat short x, @HalfFloat short y) { + return FP16.greaterEquals(x, y); + } + /** + * Returns true if the two half-precision float values are equal. + * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO} + * and {@link #NEGATIVE_ZERO} are considered equal. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is equal to y, false otherwise + */ + public static boolean equals(@HalfFloat short x, @HalfFloat short y) { + return FP16.equals(x, y); + } + /** + * Returns the sign of the specified half-precision float. + * + * @param h A half-precision float value + * @return 1 if the value is positive, -1 if the value is negative + */ + public static int getSign(@HalfFloat short h) { + return (h & FP16.SIGN_MASK) == 0 ? 1 : -1; + } + /** + * Returns the unbiased exponent used in the representation of + * the specified half-precision float value. if the value is NaN + * or infinite, this* method returns {@link #MAX_EXPONENT} + 1. + * If the argument is 0 or a subnormal representation, this method + * returns {@link #MIN_EXPONENT} - 1. + * + * @param h A half-precision float value + * @return The unbiased exponent of the specified value + */ + public static int getExponent(@HalfFloat short h) { + return ((h >>> FP16.EXPONENT_SHIFT) & FP16.SHIFTED_EXPONENT_MASK) - FP16.EXPONENT_BIAS; + } + /** + * Returns the significand, or mantissa, used in the representation + * of the specified half-precision float value. + * + * @param h A half-precision float value + * @return The significand, or significand, of the specified vlaue + */ + public static int getSignificand(@HalfFloat short h) { + return h & FP16.SIGNIFICAND_MASK; + } + /** + * Returns true if the specified half-precision float value represents + * infinity, false otherwise. + * + * @param h A half-precision float value + * @return True if the value is positive infinity or negative infinity, + * false otherwise + */ + public static boolean isInfinite(@HalfFloat short h) { + return FP16.isInfinite(h); + } + /** + * Returns true if the specified half-precision float value represents + * a Not-a-Number, false otherwise. + * + * @param h A half-precision float value + * @return True if the value is a NaN, false otherwise + */ + public static boolean isNaN(@HalfFloat short h) { + return FP16.isNaN(h); + } + /** + * Returns true if the specified half-precision float value is normalized + * (does not have a subnormal representation). If the specified value is + * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY}, + * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal + * number, this method returns false. + * + * @param h A half-precision float value + * @return True if the value is normalized, false otherwise + */ + public static boolean isNormalized(@HalfFloat short h) { + return FP16.isNormalized(h); + } + /** + *

Converts the specified half-precision float value into a + * single-precision float value. The following special cases are handled:

+ *
    + *
  • If the input is {@link #NaN}, the returned value is {@link Float#NaN}
  • + *
  • If the input is {@link #POSITIVE_INFINITY} or + * {@link #NEGATIVE_INFINITY}, the returned value is respectively + * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}
  • + *
  • If the input is 0 (positive or negative), the returned value is +/-0.0f
  • + *
  • Otherwise, the returned value is a normalized single-precision float value
  • + *
+ * + * @param h The half-precision float value to convert to single-precision + * @return A normalized single-precision float value + */ + public static float toFloat(@HalfFloat short h) { + return FP16.toFloat(h); + } + /** + *

Converts the specified single-precision float value into a + * half-precision float value. The following special cases are handled:

+ *
    + *
  • If the input is NaN (see {@link Float#isNaN(float)}), the returned + * value is {@link #NaN}
  • + *
  • If the input is {@link Float#POSITIVE_INFINITY} or + * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively + * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}
  • + *
  • If the input is 0 (positive or negative), the returned value is + * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
  • + *
  • If the input is a less than {@link #MIN_VALUE}, the returned value + * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
  • + *
  • If the input is a less than {@link #MIN_NORMAL}, the returned value + * is a denorm half-precision float
  • + *
  • Otherwise, the returned value is rounded to the nearest + * representable half-precision float value
  • + *
+ * + * @param f The single-precision float value to convert to half-precision + * @return A half-precision float value + */ + @SuppressWarnings("StatementWithEmptyBody") + public static @HalfFloat short toHalf(float f) { + return FP16.toHalf(f); + } + /** + * Returns a {@code Half} instance representing the specified + * half-precision float value. + * + * @param h A half-precision float value + * @return a {@code Half} instance representing {@code h} + */ + public static @NonNull Half valueOf(@HalfFloat short h) { + return new Half(h); + } + /** + * Returns a {@code Half} instance representing the specified float value. + * + * @param f A float value + * @return a {@code Half} instance representing {@code f} + */ + public static @NonNull Half valueOf(float f) { + return new Half(f); + } + /** + * Returns a {@code Half} instance representing the specified string value. + * Calling this method is equivalent to calling + * toHalf(Float.parseString(h)). See {@link Float#valueOf(String)} + * for more information on the format of the string representation. + * + * @param s The string to be parsed + * @return a {@code Half} instance representing {@code h} + * @throws NumberFormatException if the string does not contain a parsable + * half-precision float value + */ + public static @NonNull Half valueOf(@NonNull String s) { + return new Half(s); + } + /** + * Returns the half-precision float value represented by the specified string. + * Calling this method is equivalent to calling + * toHalf(Float.parseString(h)). See {@link Float#valueOf(String)} + * for more information on the format of the string representation. + * + * @param s The string to be parsed + * @return A half-precision float value represented by the string + * @throws NumberFormatException if the string does not contain a parsable + * half-precision float value + */ + public static @HalfFloat short parseHalf(@NonNull String s) throws NumberFormatException { + return toHalf(Float.parseFloat(s)); + } + /** + * Returns a string representation of the specified half-precision + * float value. Calling this method is equivalent to calling + * Float.toString(toFloat(h)). See {@link Float#toString(float)} + * for more information on the format of the string representation. + * + * @param h A half-precision float value + * @return A string representation of the specified value + */ + @NonNull + public static String toString(@HalfFloat short h) { + return Float.toString(toFloat(h)); + } + /** + *

Returns a hexadecimal string representation of the specified half-precision + * float value. If the value is a NaN, the result is "NaN", + * otherwise the result follows this format:

+ *
    + *
  • If the sign is positive, no sign character appears in the result
  • + *
  • If the sign is negative, the first character is '-'
  • + *
  • If the value is inifinity, the string is "Infinity"
  • + *
  • If the value is 0, the string is "0x0.0p0"
  • + *
  • If the value has a normalized representation, the exponent and + * significand are represented in the string in two fields. The significand + * starts with "0x1." followed by its lowercase hexadecimal + * representation. Trailing zeroes are removed unless all digits are 0, then + * a single zero is used. The significand representation is followed by the + * exponent, represented by "p", itself followed by a decimal + * string of the unbiased exponent
  • + *
  • If the value has a subnormal representation, the significand starts + * with "0x0." followed by its lowercase hexadecimal + * representation. Trailing zeroes are removed unless all digits are 0, then + * a single zero is used. The significand representation is followed by the + * exponent, represented by "p-14"
  • + *
+ * + * @param h A half-precision float value + * @return A hexadecimal string representation of the specified value + */ + @NonNull + public static String toHexString(@HalfFloat short h) { + return FP16.toHexString(h); + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/util/MapCollections.java b/Common/src/main/java/android/util/MapCollections.java new file mode 100644 index 00000000..f15afcec --- /dev/null +++ b/Common/src/main/java/android/util/MapCollections.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2013 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 android.util; +import android.annotation.Nullable; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +/** + * Helper for writing standard Java collection interfaces to a data + * structure like {link ArrayMap}. + * @hide + */ +abstract class MapCollections { + EntrySet mEntrySet; + KeySet mKeySet; + ValuesCollection mValues; + final class ArrayIterator implements Iterator { + final int mOffset; + int mSize; + int mIndex; + boolean mCanRemove = false; + ArrayIterator(int offset) { + mOffset = offset; + mSize = colGetSize(); + } + @Override + public boolean hasNext() { + return mIndex < mSize; + } + @Override + public T next() { + if (!hasNext()) throw new NoSuchElementException(); + Object res = colGetEntry(mIndex, mOffset); + mIndex++; + mCanRemove = true; + return (T)res; + } + @Override + public void remove() { + if (!mCanRemove) { + throw new IllegalStateException(); + } + mIndex--; + mSize--; + mCanRemove = false; + colRemoveAt(mIndex); + } + } + final class MapIterator implements Iterator>, Map.Entry { + int mEnd; + int mIndex; + boolean mEntryValid = false; + MapIterator() { + mEnd = colGetSize() - 1; + mIndex = -1; + } + @Override + public boolean hasNext() { + return mIndex < mEnd; + } + @Override + public Map.Entry next() { + if (!hasNext()) throw new NoSuchElementException(); + mIndex++; + mEntryValid = true; + return this; + } + @Override + public void remove() { + if (!mEntryValid) { + throw new IllegalStateException(); + } + colRemoveAt(mIndex); + mIndex--; + mEnd--; + mEntryValid = false; + } + @Override + public K getKey() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (K)colGetEntry(mIndex, 0); + } + @Override + public V getValue() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (V)colGetEntry(mIndex, 1); + } + @Override + public V setValue(V object) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return colSetValue(mIndex, object); + } + @Override + public final boolean equals(Object o) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + return Objects.equals(e.getKey(), colGetEntry(mIndex, 0)) + && Objects.equals(e.getValue(), colGetEntry(mIndex, 1)); + } + @Override + public final int hashCode() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + final Object key = colGetEntry(mIndex, 0); + final Object value = colGetEntry(mIndex, 1); + return (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()); + } + @Override + public final String toString() { + return getKey() + "=" + getValue(); + } + } + final class EntrySet implements Set> { + @Override + public boolean add(Map.Entry object) { + throw new UnsupportedOperationException(); + } + @Override + public boolean addAll(Collection> collection) { + int oldSize = colGetSize(); + for (Map.Entry entry : collection) { + colPut(entry.getKey(), entry.getValue()); + } + return oldSize != colGetSize(); + } + @Override + public void clear() { + colClear(); + } + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + int index = colIndexOfKey(e.getKey()); + if (index < 0) { + return false; + } + Object foundVal = colGetEntry(index, 1); + return Objects.equals(foundVal, e.getValue()); + } + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + @Override + public Iterator> iterator() { + return new MapIterator(); + } + @Override + public boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + @Override + public boolean removeAll(Collection collection) { + throw new UnsupportedOperationException(); + } + @Override + public boolean retainAll(Collection collection) { + throw new UnsupportedOperationException(); + } + @Override + public int size() { + return colGetSize(); + } + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + @Override + public T[] toArray(T[] array) { + throw new UnsupportedOperationException(); + } + @Override + public boolean equals(@Nullable Object object) { + return equalsSetHelper(this, object); + } + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + final Object key = colGetEntry(i, 0); + final Object value = colGetEntry(i, 1); + result += ( (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()) ); + } + return result; + } + }; + final class KeySet implements Set { + @Override + public boolean add(K object) { + throw new UnsupportedOperationException(); + } + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + @Override + public void clear() { + colClear(); + } + @Override + public boolean contains(Object object) { + return colIndexOfKey(object) >= 0; + } + @Override + public boolean containsAll(Collection collection) { + return containsAllHelper(colGetMap(), collection); + } + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + @Override + public Iterator iterator() { + return new ArrayIterator(0); + } + @Override + public boolean remove(Object object) { + int index = colIndexOfKey(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + @Override + public boolean removeAll(Collection collection) { + return removeAllHelper(colGetMap(), collection); + } + @Override + public boolean retainAll(Collection collection) { + return retainAllHelper(colGetMap(), collection); + } + @Override + public int size() { + return colGetSize(); + } + @Override + public Object[] toArray() { + return toArrayHelper(0); + } + @Override + public T[] toArray(T[] array) { + return toArrayHelper(array, 0); + } + @Override + public boolean equals(@Nullable Object object) { + return equalsSetHelper(this, object); + } + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + Object obj = colGetEntry(i, 0); + result += obj == null ? 0 : obj.hashCode(); + } + return result; + } + }; + final class ValuesCollection implements Collection { + @Override + public boolean add(V object) { + throw new UnsupportedOperationException(); + } + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + @Override + public void clear() { + colClear(); + } + @Override + public boolean contains(Object object) { + return colIndexOfValue(object) >= 0; + } + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + @Override + public Iterator iterator() { + return new ArrayIterator(1); + } + @Override + public boolean remove(Object object) { + int index = colIndexOfValue(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + @Override + public boolean removeAll(Collection collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i T[] toArray(T[] array) { + return toArrayHelper(array, 1); + } + }; + public static boolean containsAllHelper(Map map, Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!map.containsKey(it.next())) { + return false; + } + } + return true; + } + public static boolean removeAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = collection.iterator(); + while (it.hasNext()) { + map.remove(it.next()); + } + return oldSize != map.size(); + } + public static boolean retainAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = map.keySet().iterator(); + while (it.hasNext()) { + if (!collection.contains(it.next())) { + it.remove(); + } + } + return oldSize != map.size(); + } + public Object[] toArrayHelper(int offset) { + final int N = colGetSize(); + Object[] result = new Object[N]; + for (int i=0; i T[] toArrayHelper(T[] array, int offset) { + final int N = colGetSize(); + if (array.length < N) { + @SuppressWarnings("unchecked") T[] newArray + = (T[]) Array.newInstance(array.getClass().getComponentType(), N); + array = newArray; + } + for (int i=0; i N) { + array[N] = null; + } + return array; + } + public static boolean equalsSetHelper(Set set, Object object) { + if (set == object) { + return true; + } + if (object instanceof Set) { + Set s = (Set) object; + try { + return set.size() == s.size() && set.containsAll(s); + } catch (NullPointerException ignored) { + return false; + } catch (ClassCastException ignored) { + return false; + } + } + return false; + } + public Set> getEntrySet() { + if (mEntrySet == null) { + mEntrySet = new EntrySet(); + } + return mEntrySet; + } + public Set getKeySet() { + if (mKeySet == null) { + mKeySet = new KeySet(); + } + return mKeySet; + } + public Collection getValues() { + if (mValues == null) { + mValues = new ValuesCollection(); + } + return mValues; + } + protected abstract int colGetSize(); + protected abstract Object colGetEntry(int index, int offset); + protected abstract int colIndexOfKey(Object key); + protected abstract int colIndexOfValue(Object key); + protected abstract Map colGetMap(); + protected abstract void colPut(K key, V value); + protected abstract V colSetValue(int index, V value); + protected abstract void colRemoveAt(int index); + protected abstract void colClear(); +} \ No newline at end of file diff --git a/Common/src/main/java/android/util/SparseIntArray.java b/Common/src/main/java/android/util/SparseIntArray.java new file mode 100644 index 00000000..675efaae --- /dev/null +++ b/Common/src/main/java/android/util/SparseIntArray.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2006 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; +import libcore.util.EmptyArray; +import java.util.Arrays; +/** + * SparseIntArrays map integers to integers. Unlike a normal array of integers, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Integers to Integers, both because it avoids + * auto-boxing keys and values and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

+ * + *

It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

+ */ +public class SparseIntArray implements Cloneable { + + private int[] mKeys; + + private int[] mValues; + + private int mSize; + /** + * Creates a new SparseIntArray containing no mappings. + */ + public SparseIntArray() { + this(10); + } + /** + * Creates a new SparseIntArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseIntArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.INT; + mValues = EmptyArray.INT; + } else { + mKeys = new int[initialCapacity]; + mValues = new int[mKeys.length]; + } + mSize = 0; + } + @Override + public SparseIntArray clone() { + SparseIntArray clone = null; + try { + clone = (SparseIntArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + /** + * Gets the int mapped from the specified key, or 0 + * if no such mapping has been made. + */ + public int get(int key) { + return get(key, 0); + } + /** + * Gets the int mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public int get(int key, int valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) { + removeAt(i); + } + } + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, int value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return mSize; + } + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseIntArray stores. + * + *

The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

+ * + *

For indices outside of the range 0...size()-1, the behavior is undefined for + * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {link android.os.Build.VERSION_CODES#Q} and later.

+ */ + public int keyAt(int index) { + if (index >= mSize) { + // The array might be slightly bigger than mSize, in which case, indexing won't fail. + // Check if exception should be thrown outside of the critical path. + throw new ArrayIndexOutOfBoundsException(index); + } + return mKeys[index]; + } + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseIntArray stores. + * + *

The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

+ * + *

For indices outside of the range 0...size()-1, the behavior is undefined for + * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {link android.os.Build.VERSION_CODES#Q} and later.

+ */ + public int valueAt(int index) { + if (index >= mSize) { + // The array might be slightly bigger than mSize, in which case, indexing won't fail. + // Check if exception should be thrown outside of the critical path. + throw new ArrayIndexOutOfBoundsException(index); + } + return mValues[index]; + } + /** + * Directly set the value at a particular index. + * + *

For indices outside of the range 0...size()-1, the behavior is undefined for + * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {link android.os.Build.VERSION_CODES#Q} and later.

+ */ + public void setValueAt(int index, int value) { + if (index >= mSize) { + // The array might be slightly bigger than mSize, in which case, indexing won't fail. + // Check if exception should be thrown outside of the critical path. + throw new ArrayIndexOutOfBoundsException(index); + } + mValues[index] = value; + } + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(int value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + return -1; + } + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + mSize = 0; + } + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, int value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + /** + * Provides a copy of keys. + * + * @hide + * */ + public int[] copyKeys() { + if (size() == 0) { + return null; + } + return Arrays.copyOf(mKeys, size()); + } + /** + * {@inheritDoc} + * + *

This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + int value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } +} \ No newline at end of file diff --git a/Common/src/main/java/androidx/annotation/NonNull.java b/Common/src/main/java/androidx/annotation/NonNull.java new file mode 100644 index 00000000..e8847afe --- /dev/null +++ b/Common/src/main/java/androidx/annotation/NonNull.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013 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.ANNOTATION_TYPE; +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.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * Denotes that a parameter, field or method return value can never be null. + *

+ * This is a marker annotation and it has no specific attributes. + */ +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +public @interface NonNull { +} \ No newline at end of file diff --git a/Common/src/main/java/androidx/annotation/StringRes.java b/Common/src/main/java/androidx/annotation/StringRes.java new file mode 100644 index 00000000..52c0fa00 --- /dev/null +++ b/Common/src/main/java/androidx/annotation/StringRes.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 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.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a String resource reference (e.g. {@code android.R.string.ok}). + */ +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE}) +public @interface StringRes { +} \ No newline at end of file diff --git a/Common/src/main/java/com/android/internal/util/ArrayUtils.java b/Common/src/main/java/com/android/internal/util/ArrayUtils.java new file mode 100644 index 00000000..2e30c597 --- /dev/null +++ b/Common/src/main/java/com/android/internal/util/ArrayUtils.java @@ -0,0 +1,834 @@ +/* + * Copyright (C) 2006 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 com.android.internal.util; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import android.util.ArraySet; + +import dalvik.system.VMRuntime; +import libcore.util.EmptyArray; +import java.io.File; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntFunction; +/** + * Static utility methods for arrays that aren't already included in {@link java.util.Arrays}. + */ +public class ArrayUtils { + private static final int CACHE_SIZE = 73; + private static Object[] sCache = new Object[CACHE_SIZE]; + public static final File[] EMPTY_FILE = new File[0]; + private ArrayUtils() { /* cannot be instantiated */ } + + public static byte[] newUnpaddedByteArray(int minLen) { + return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen); + } + public static char[] newUnpaddedCharArray(int minLen) { + return (char[]) VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen); + } + public static int[] newUnpaddedIntArray(int minLen) { + return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen); + } + public static boolean[] newUnpaddedBooleanArray(int minLen) { + return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen); + } + public static long[] newUnpaddedLongArray(int minLen) { + return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen); + } + public static float[] newUnpaddedFloatArray(int minLen) { + return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen); + } + public static Object[] newUnpaddedObjectArray(int minLen) { + return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen); + } + @SuppressWarnings("unchecked") + public static T[] newUnpaddedArray(Class clazz, int minLen) { + return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen); + } + + /** + * Checks if the beginnings of two byte arrays are equal. + * + * @param array1 the first byte array + * @param array2 the second byte array + * @param length the number of bytes to check + * @return true if they're equal, false otherwise + */ + public static boolean equals(byte[] array1, byte[] array2, int length) { + if (length < 0) { + throw new IllegalArgumentException(); + } + if (array1 == array2) { + return true; + } + if (array1 == null || array2 == null || array1.length < length || array2.length < length) { + return false; + } + for (int i = 0; i < length; i++) { + if (array1[i] != array2[i]) { + return false; + } + } + return true; + } + /** + * Returns an empty array of the specified type. The intent is that + * it will return the same empty array every time to avoid reallocation, + * although this is not guaranteed. + */ + @SuppressWarnings("unchecked") + public static T[] emptyArray(Class kind) { + if (kind == Object.class) { + return (T[]) EmptyArray.OBJECT; + } + int bucket = (kind.hashCode() & 0x7FFFFFFF) % CACHE_SIZE; + Object cache = sCache[bucket]; + if (cache == null || cache.getClass().getComponentType() != kind) { + cache = Array.newInstance(kind, 0); + sCache[bucket] = cache; + // Log.e("cache", "new empty " + kind.getName() + " at " + bucket); + } + return (T[]) cache; + } + /** + * Returns the same array or an empty one if it's null. + */ + public static @NonNull T[] emptyIfNull(@Nullable T[] items, Class kind) { + return items != null ? items : emptyArray(kind); + } + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable Collection array) { + return array == null || array.isEmpty(); + } + /** + * Checks if given map is null or has zero elements. + */ + public static boolean isEmpty(@Nullable Map map) { + return map == null || map.isEmpty(); + } + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable T[] array) { + return array == null || array.length == 0; + } + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable int[] array) { + return array == null || array.length == 0; + } + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable long[] array) { + return array == null || array.length == 0; + } + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable byte[] array) { + return array == null || array.length == 0; + } + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable boolean[] array) { + return array == null || array.length == 0; + } + /** + * Length of the given array or 0 if it's null. + */ + public static int size(@Nullable Object[] array) { + return array == null ? 0 : array.length; + } + /** + * Length of the given collection or 0 if it's null. + */ + public static int size(@Nullable Collection collection) { + return collection == null ? 0 : collection.size(); + } + /** + * Length of the given map or 0 if it's null. + */ + public static int size(@Nullable Map map) { + return map == null ? 0 : map.size(); + } + /** + * Checks that value is present as at least one of the elements of the array. + * @param array the array to check in + * @param value the value to check for + * @return true if the value is present in the array + */ + public static boolean contains(@Nullable T[] array, T value) { + return indexOf(array, value) != -1; + } + /** + * Return first index of {@code value} in {@code array}, or {@code -1} if + * not found. + */ + public static int indexOf(@Nullable T[] array, T value) { + if (array == null) return -1; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) return i; + } + return -1; + } + /** + * Test if all {@code check} items are contained in {@code array}. + */ + public static boolean containsAll(@Nullable T[] array, T[] check) { + if (check == null) return true; + for (T checkItem : check) { + if (!contains(array, checkItem)) { + return false; + } + } + return true; + } + /** + * Test if any {@code check} items are contained in {@code array}. + */ + public static boolean containsAny(@Nullable T[] array, T[] check) { + if (check == null) return false; + for (T checkItem : check) { + if (contains(array, checkItem)) { + return true; + } + } + return false; + } + public static boolean contains(@Nullable int[] array, int value) { + if (array == null) return false; + for (int element : array) { + if (element == value) { + return true; + } + } + return false; + } + public static boolean contains(@Nullable long[] array, long value) { + if (array == null) return false; + for (long element : array) { + if (element == value) { + return true; + } + } + return false; + } + public static boolean contains(@Nullable char[] array, char value) { + if (array == null) return false; + for (char element : array) { + if (element == value) { + return true; + } + } + return false; + } + /** + * Test if all {@code check} items are contained in {@code array}. + */ + public static boolean containsAll(@Nullable char[] array, char[] check) { + if (check == null) return true; + for (char checkItem : check) { + if (!contains(array, checkItem)) { + return false; + } + } + return true; + } + public static long total(@Nullable long[] array) { + long total = 0; + if (array != null) { + for (long value : array) { + total += value; + } + } + return total; + } + /** + * @deprecated use {@code IntArray} instead + */ + @Deprecated + public static int[] convertToIntArray(List list) { + int[] array = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + public static @Nullable long[] convertToLongArray(@Nullable int[] intArray) { + if (intArray == null) return null; + long[] array = new long[intArray.length]; + for (int i = 0; i < intArray.length; i++) { + array[i] = (long) intArray[i]; + } + return array; + } + /** + * Returns the concatenation of the given arrays. Only works for object arrays, not for + * primitive arrays. See {@link #concat(byte[]...)} for a variant that works on byte arrays. + * + * @param kind The class of the array elements + * @param arrays The arrays to concatenate. Null arrays are treated as empty. + * @param The class of the array elements (inferred from kind). + * @return A single array containing all the elements of the parameter arrays. + */ + @SuppressWarnings("unchecked") + public static @NonNull T[] concat(Class kind, @Nullable T[]... arrays) { + if (arrays == null || arrays.length == 0) { + return createEmptyArray(kind); + } + int totalLength = 0; + for (T[] item : arrays) { + if (item == null) { + continue; + } + totalLength += item.length; + } + // Optimization for entirely empty arrays. + if (totalLength == 0) { + return createEmptyArray(kind); + } + final T[] all = (T[]) Array.newInstance(kind, totalLength); + int pos = 0; + for (T[] item : arrays) { + if (item == null || item.length == 0) { + continue; + } + System.arraycopy(item, 0, all, pos, item.length); + pos += item.length; + } + return all; + } + private static @NonNull T[] createEmptyArray(Class kind) { + if (kind == String.class) { + return (T[]) EmptyArray.STRING; + } else if (kind == Object.class) { + return (T[]) EmptyArray.OBJECT; + } + return (T[]) Array.newInstance(kind, 0); + } + /** + * Returns the concatenation of the given byte arrays. Null arrays are treated as empty. + */ + public static @NonNull byte[] concat(@Nullable byte[]... arrays) { + if (arrays == null) { + return new byte[0]; + } + int totalLength = 0; + for (byte[] a : arrays) { + if (a != null) { + totalLength += a.length; + } + } + final byte[] result = new byte[totalLength]; + int pos = 0; + for (byte[] a : arrays) { + if (a != null) { + System.arraycopy(a, 0, result, pos, a.length); + pos += a.length; + } + } + return result; + } + /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + @SuppressWarnings("unchecked") + public static @NonNull T[] appendElement(Class kind, @Nullable T[] array, T element) { + return appendElement(kind, array, element, false); + } + /** + * Adds value to given array. + */ + @SuppressWarnings("unchecked") + public static @NonNull T[] appendElement(Class kind, @Nullable T[] array, T element, + boolean allowDuplicates) { + final T[] result; + final int end; + if (array != null) { + if (!allowDuplicates && contains(array, element)) return array; + end = array.length; + result = (T[])Array.newInstance(kind, end + 1); + System.arraycopy(array, 0, result, 0, end); + } else { + end = 0; + result = (T[])Array.newInstance(kind, 1); + } + result[end] = element; + return result; + } + /** + * Removes value from given array if present, providing set-like behavior. + */ + @SuppressWarnings("unchecked") + public static @Nullable T[] removeElement(Class kind, @Nullable T[] array, T element) { + if (array != null) { + if (!contains(array, element)) return array; + final int length = array.length; + for (int i = 0; i < length; i++) { + if (Objects.equals(array[i], element)) { + if (length == 1) { + return null; + } + T[] result = (T[])Array.newInstance(kind, length - 1); + System.arraycopy(array, 0, result, 0, i); + System.arraycopy(array, i + 1, result, i, length - i - 1); + return result; + } + } + } + return array; + } + /** + * Adds value to given array. + */ + public static @NonNull int[] appendInt(@Nullable int[] cur, int val, + boolean allowDuplicates) { + if (cur == null) { + return new int[] { val }; + } + final int N = cur.length; + if (!allowDuplicates) { + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + return cur; + } + } + } + int[] ret = new int[N + 1]; + System.arraycopy(cur, 0, ret, 0, N); + ret[N] = val; + return ret; + } + /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + public static @NonNull int[] appendInt(@Nullable int[] cur, int val) { + return appendInt(cur, val, false); + } + /** + * Removes value from given array if present, providing set-like behavior. + */ + public static @Nullable int[] removeInt(@Nullable int[] cur, int val) { + if (cur == null) { + return null; + } + final int N = cur.length; + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + int[] ret = new int[N - 1]; + if (i > 0) { + System.arraycopy(cur, 0, ret, 0, i); + } + if (i < (N - 1)) { + System.arraycopy(cur, i + 1, ret, i, N - i - 1); + } + return ret; + } + } + return cur; + } + /** + * Removes value from given array if present, providing set-like behavior. + */ + public static @Nullable String[] removeString(@Nullable String[] cur, String val) { + if (cur == null) { + return null; + } + final int N = cur.length; + for (int i = 0; i < N; i++) { + if (Objects.equals(cur[i], val)) { + String[] ret = new String[N - 1]; + if (i > 0) { + System.arraycopy(cur, 0, ret, 0, i); + } + if (i < (N - 1)) { + System.arraycopy(cur, i + 1, ret, i, N - i - 1); + } + return ret; + } + } + return cur; + } + /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + public static @NonNull long[] appendLong(@Nullable long[] cur, long val, + boolean allowDuplicates) { + if (cur == null) { + return new long[] { val }; + } + final int N = cur.length; + if (!allowDuplicates) { + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + return cur; + } + } + } + long[] ret = new long[N + 1]; + System.arraycopy(cur, 0, ret, 0, N); + ret[N] = val; + return ret; + } + /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + public static @NonNull long[] appendLong(@Nullable long[] cur, long val) { + return appendLong(cur, val, false); + } + /** + * Removes value from given array if present, providing set-like behavior. + */ + public static @Nullable long[] removeLong(@Nullable long[] cur, long val) { + if (cur == null) { + return null; + } + final int N = cur.length; + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + long[] ret = new long[N - 1]; + if (i > 0) { + System.arraycopy(cur, 0, ret, 0, i); + } + if (i < (N - 1)) { + System.arraycopy(cur, i + 1, ret, i, N - i - 1); + } + return ret; + } + } + return cur; + } + public static @Nullable long[] cloneOrNull(@Nullable long[] array) { + return (array != null) ? array.clone() : null; + } + /** + * Clones an array or returns null if the array is null. + */ + public static @Nullable T[] cloneOrNull(@Nullable T[] array) { + return (array != null) ? array.clone() : null; + } + public static @Nullable ArraySet cloneOrNull(@Nullable ArraySet array) { + return (array != null) ? new ArraySet(array) : null; + } + public static @NonNull ArraySet add(@Nullable ArraySet cur, T val) { + if (cur == null) { + cur = new ArraySet<>(); + } + cur.add(val); + return cur; + } + /** + * Similar to {@link Set#addAll(Collection)}}, but with support for set values of {@code null}. + */ + public static @NonNull ArraySet addAll(@Nullable ArraySet cur, + @Nullable Collection val) { + if (cur == null) { + cur = new ArraySet<>(); + } + if (val != null) { + cur.addAll(val); + } + return cur; + } + public static @Nullable ArraySet remove(@Nullable ArraySet cur, T val) { + if (cur == null) { + return null; + } + cur.remove(val); + if (cur.isEmpty()) { + return null; + } else { + return cur; + } + } + public static @NonNull ArrayList add(@Nullable ArrayList cur, T val) { + if (cur == null) { + cur = new ArrayList<>(); + } + cur.add(val); + return cur; + } + public static @NonNull ArrayList add(@Nullable ArrayList cur, int index, T val) { + if (cur == null) { + cur = new ArrayList<>(); + } + cur.add(index, val); + return cur; + } + public static @Nullable ArrayList remove(@Nullable ArrayList cur, T val) { + if (cur == null) { + return null; + } + cur.remove(val); + if (cur.isEmpty()) { + return null; + } else { + return cur; + } + } + public static boolean contains(@Nullable Collection cur, T val) { + return (cur != null) ? cur.contains(val) : false; + } + public static @Nullable T[] trimToSize(@Nullable T[] array, int size) { + if (array == null || size == 0) { + return null; + } else if (array.length == size) { + return array; + } else { + return Arrays.copyOf(array, size); + } + } + /** + * Returns true if the two ArrayLists are equal with respect to the objects they contain. + * The objects must be in the same order and be reference equal (== not .equals()). + */ + public static boolean referenceEquals(ArrayList a, ArrayList b) { + if (a == b) { + return true; + } + final int sizeA = a.size(); + final int sizeB = b.size(); + if (a == null || b == null || sizeA != sizeB) { + return false; + } + boolean diff = false; + for (int i = 0; i < sizeA && !diff; i++) { + diff |= a.get(i) != b.get(i); + } + return !diff; + } + /** + * Removes elements that match the predicate in an efficient way that alters the order of + * elements in the collection. This should only be used if order is not important. + * @param collection The ArrayList from which to remove elements. + * @param predicate The predicate that each element is tested against. + * @return the number of elements removed. + */ + public static int unstableRemoveIf(@Nullable ArrayList collection, + @NonNull java.util.function.Predicate predicate) { + if (collection == null) { + return 0; + } + final int size = collection.size(); + int leftIdx = 0; + int rightIdx = size - 1; + while (leftIdx <= rightIdx) { + // Find the next element to remove moving left to right. + while (leftIdx < size && !predicate.test(collection.get(leftIdx))) { + leftIdx++; + } + // Find the next element to keep moving right to left. + while (rightIdx > leftIdx && predicate.test(collection.get(rightIdx))) { + rightIdx--; + } + if (leftIdx >= rightIdx) { + // Done. + break; + } + Collections.swap(collection, leftIdx, rightIdx); + leftIdx++; + rightIdx--; + } + // leftIdx is now at the end. + for (int i = size - 1; i >= leftIdx; i--) { + collection.remove(i); + } + return size - leftIdx; + } + public static @NonNull int[] defeatNullable(@Nullable int[] val) { + return (val != null) ? val : EmptyArray.INT; + } + public static @NonNull String[] defeatNullable(@Nullable String[] val) { + return (val != null) ? val : EmptyArray.STRING; + } + public static @NonNull File[] defeatNullable(@Nullable File[] val) { + return (val != null) ? val : EMPTY_FILE; + } + /** + * Throws {@link ArrayIndexOutOfBoundsException} if the index is out of bounds. + * + * @param len length of the array. Must be non-negative + * @param index the index to check + * @throws ArrayIndexOutOfBoundsException if the {@code index} is out of bounds of the array + */ + public static void checkBounds(int len, int index) { + if (index < 0 || len <= index) { + throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index); + } + } + /** + * Throws {@link ArrayIndexOutOfBoundsException} if the range is out of bounds. + * @param len length of the array. Must be non-negative + * @param offset start index of the range. Must be non-negative + * @param count length of the range. Must be non-negative + * @throws ArrayIndexOutOfBoundsException if the range from {@code offset} with length + * {@code count} is out of bounds of the array + */ + public static void throwsIfOutOfBounds(int len, int offset, int count) { + if (len < 0) { + throw new ArrayIndexOutOfBoundsException("Negative length: " + len); + } + if ((offset | count) < 0 || offset > len - count) { + throw new ArrayIndexOutOfBoundsException( + "length=" + len + "; regionStart=" + offset + "; regionLength=" + count); + } + } + /** + * Returns an array with values from {@code val} minus {@code null} values + * + * @param arrayConstructor typically {@code T[]::new} e.g. {@code String[]::new} + */ + public static T[] filterNotNull(T[] val, IntFunction arrayConstructor) { + int nullCount = 0; + int size = size(val); + for (int i = 0; i < size; i++) { + if (val[i] == null) { + nullCount++; + } + } + if (nullCount == 0) { + return val; + } + T[] result = arrayConstructor.apply(size - nullCount); + int outIdx = 0; + for (int i = 0; i < size; i++) { + if (val[i] != null) { + result[outIdx++] = val[i]; + } + } + return result; + } + /** + * Returns an array containing elements from the given one that match the given predicate. + * The returned array may, in some cases, be the reference to the input array. + */ + public static @Nullable T[] filter(@Nullable T[] items, + @NonNull IntFunction arrayConstructor, + @NonNull java.util.function.Predicate predicate) { + if (isEmpty(items)) { + return items; + } + int matchesCount = 0; + int size = size(items); + final boolean[] tests = new boolean[size]; + for (int i = 0; i < size; i++) { + tests[i] = predicate.test(items[i]); + if (tests[i]) { + matchesCount++; + } + } + if (matchesCount == items.length) { + return items; + } + T[] result = arrayConstructor.apply(matchesCount); + if (matchesCount == 0) { + return result; + } + int outIdx = 0; + for (int i = 0; i < size; i++) { + if (tests[i]) { + result[outIdx++] = items[i]; + } + } + return result; + } + public static boolean startsWith(byte[] cur, byte[] val) { + if (cur == null || val == null) return false; + if (cur.length < val.length) return false; + for (int i = 0; i < val.length; i++) { + if (cur[i] != val[i]) return false; + } + return true; + } + /** + * Returns the first element from the array for which + * condition {@code predicate} is true, or null if there is no such element + */ + public static @Nullable T find(@Nullable T[] items, + @NonNull java.util.function.Predicate predicate) { + if (isEmpty(items)) return null; + for (final T item : items) { + if (predicate.test(item)) return item; + } + return null; + } + public static String deepToString(Object value) { + if (value != null && value.getClass().isArray()) { + if (value.getClass() == boolean[].class) { + return Arrays.toString((boolean[]) value); + } else if (value.getClass() == byte[].class) { + return Arrays.toString((byte[]) value); + } else if (value.getClass() == char[].class) { + return Arrays.toString((char[]) value); + } else if (value.getClass() == double[].class) { + return Arrays.toString((double[]) value); + } else if (value.getClass() == float[].class) { + return Arrays.toString((float[]) value); + } else if (value.getClass() == int[].class) { + return Arrays.toString((int[]) value); + } else if (value.getClass() == long[].class) { + return Arrays.toString((long[]) value); + } else if (value.getClass() == short[].class) { + return Arrays.toString((short[]) value); + } else { + return Arrays.deepToString((Object[]) value); + } + } else { + return String.valueOf(value); + } + } + /** + * Returns the {@code i}-th item in {@code items}, if it exists and {@code items} is not {@code + * null}, otherwise returns {@code null}. + */ + @Nullable + public static T getOrNull(@Nullable T[] items, int i) { + return (items != null && items.length > i) ? items[i] : null; + } + public static @Nullable T firstOrNull(T[] items) { + return items.length > 0 ? items[0] : null; + } + /** + * Creates a {@link List} from an array. Different from {@link Arrays#asList(Object[])} as that + * will use the parameter as the backing array, meaning changes are not isolated. + */ + public static List toList(T[] array) { + List list = new ArrayList<>(array.length); + //noinspection ManualArrayToCollectionCopy + for (T item : array) { + //noinspection UseBulkOperation + list.add(item); + } + return list; + } +} \ No newline at end of file diff --git a/Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java b/Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java new file mode 100644 index 00000000..9dc53800 --- /dev/null +++ b/Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2014 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 com.android.internal.util; + +/** + * A helper class that aims to provide comparable growth performance to ArrayList, but on primitive + * arrays. Common array operations are implemented for efficient use in dynamic containers. + * + * All methods in this class assume that the length of an array is equivalent to its capacity and + * NOT the number of elements in the array. The current size of the array is always passed in as a + * parameter. + * + * @hide + */ +public final class GrowingArrayUtils { + /** + * Appends an element to the end of the array, growing the array if there is no more room. + * @param array The array to which to append the element. This must NOT be null. + * @param currentSize The number of elements in the array. Must be less than or equal to + * array.length. + * @param element The element to append. + * @return the array to which the element was appended. This may be different than the given + * array. + */ + public static T[] append(T[] array, int currentSize, T element) { + assert currentSize <= array.length; + if (currentSize + 1 > array.length) { + @SuppressWarnings("unchecked") + T[] newArray = ArrayUtils.newUnpaddedArray( + (Class) array.getClass().getComponentType(), growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + /** + * Primitive int version of {@link #append(Object[], int, Object)}. + */ + public static int[] append(int[] array, int currentSize, int element) { + assert currentSize <= array.length; + if (currentSize + 1 > array.length) { + int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + /** + * Primitive long version of {@link #append(Object[], int, Object)}. + */ + public static long[] append(long[] array, int currentSize, long element) { + assert currentSize <= array.length; + if (currentSize + 1 > array.length) { + long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + /** + * Primitive boolean version of {@link #append(Object[], int, Object)}. + */ + public static boolean[] append(boolean[] array, int currentSize, boolean element) { + assert currentSize <= array.length; + if (currentSize + 1 > array.length) { + boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + /** + * Primitive float version of {@link #append(Object[], int, Object)}. + */ + public static float[] append(float[] array, int currentSize, float element) { + assert currentSize <= array.length; + if (currentSize + 1 > array.length) { + float[] newArray = ArrayUtils.newUnpaddedFloatArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + /** + * Inserts an element into the array at the specified index, growing the array if there is no + * more room. + * + * @param array The array to which to append the element. Must NOT be null. + * @param currentSize The number of elements in the array. Must be less than or equal to + * array.length. + * @param element The element to insert. + * @return the array to which the element was appended. This may be different than the given + * array. + */ + public static T[] insert(T[] array, int currentSize, int index, T element) { + assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + @SuppressWarnings("unchecked") + T[] newArray = ArrayUtils.newUnpaddedArray((Class)array.getClass().getComponentType(), + growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + /** + * Primitive int version of {@link #insert(Object[], int, int, Object)}. + */ + public static int[] insert(int[] array, int currentSize, int index, int element) { + assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + /** + * Primitive long version of {@link #insert(Object[], int, int, Object)}. + */ + public static long[] insert(long[] array, int currentSize, int index, long element) { + assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + /** + * Primitive boolean version of {@link #insert(Object[], int, int, Object)}. + */ + public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) { + assert currentSize <= array.length; + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + /** + * Given the current size of an array, returns an ideal size to which the array should grow. + * This is typically double the given size, but should not be relied upon to do so in the + * future. + */ + public static int growSize(int currentSize) { + return currentSize <= 4 ? 8 : currentSize * 2; + } + // Uninstantiable + private GrowingArrayUtils() {} +} \ No newline at end of file diff --git a/Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java b/Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java new file mode 100644 index 00000000..cfbe3c70 --- /dev/null +++ b/Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014, 2015 Qualcomm Technologies Inc + * + * 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 Qualcomm Technologies Inc 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 com.qualcomm.robotcore.exception; + +/* + * RobotCoreException + * + * An exception used commonly by the RobotCore library + */ +public class RobotCoreException extends Exception { + + public RobotCoreException(String message) { + super(message); + } + public RobotCoreException(String message, Throwable cause) { + super(message, cause); + } + + public RobotCoreException(String format, Object... args) { + super(String.format(format, args)); + } + + public static RobotCoreException createChained(Exception e, String format, Object... args) { + return new RobotCoreException(String.format(format, args), e); + } +} \ No newline at end of file diff --git a/Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java b/Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java new file mode 100644 index 00000000..2f5b78c0 --- /dev/null +++ b/Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java @@ -0,0 +1,17 @@ +package com.qualcomm.robotcore.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RobotLog { + + private RobotLog() { } + + public static void ee(String tag, Throwable throwable, String message) { + LoggerFactory.getLogger(tag).error(message, throwable); + } + + public static void ee(String tag, String message) { + LoggerFactory.getLogger(tag).error(message); + } +} diff --git a/Common/src/main/java/dalvik/system/VMRuntime.java b/Common/src/main/java/dalvik/system/VMRuntime.java new file mode 100644 index 00000000..a8b450cc --- /dev/null +++ b/Common/src/main/java/dalvik/system/VMRuntime.java @@ -0,0 +1,22 @@ +package dalvik.system; + +import java.lang.reflect.Array; + +public class VMRuntime { + + // singleton class + + private static VMRuntime runtime = new VMRuntime(); + + private VMRuntime() { + } + + public static VMRuntime getRuntime() { + return runtime; + } + + public Object newUnpaddedArray(Class componentType, int length) { + return Array.newInstance(componentType, length); // we do a little bit of trolling -SEM + } + +} diff --git a/Common/src/main/java/libcore/util/EmptyArray.java b/Common/src/main/java/libcore/util/EmptyArray.java new file mode 100644 index 00000000..c1184d27 --- /dev/null +++ b/Common/src/main/java/libcore/util/EmptyArray.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 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 libcore.util; +import android.annotation.NonNull; +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import android.annotation.SystemApi; + +import java.lang.annotation.Annotation; +/** + * Empty array is immutable. Use a shared empty array to avoid allocation. + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class EmptyArray { + private EmptyArray() {} + /** @hide */ + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull boolean[] BOOLEAN = new boolean[0]; + /** @hide */ + + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull byte[] BYTE = new byte[0]; + /** @hide */ + public static final char[] CHAR = new char[0]; + /** @hide */ + public static final double[] DOUBLE = new double[0]; + /** @hide */ + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull float[] FLOAT = new float[0]; + /** @hide */ + + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull int[] INT = new int[0]; + /** @hide */ + + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull long[] LONG = new long[0]; + /** @hide */ + public static final Class[] CLASS = new Class[0]; + /** @hide */ + + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull Object[] OBJECT = new Object[0]; + /** @hide */ + @SystemApi(client = MODULE_LIBRARIES) + public static final @NonNull String[] STRING = new String[0]; + /** @hide */ + public static final Throwable[] THROWABLE = new Throwable[0]; + /** @hide */ + public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0]; + /** @hide */ + public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0]; + /** @hide */ + public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE = + new java.lang.reflect.TypeVariable[0]; + /** @hide */ + public static final Annotation[] ANNOTATION = new Annotation[0]; +} \ No newline at end of file diff --git a/Common/src/main/java/libcore/util/FP16.java b/Common/src/main/java/libcore/util/FP16.java new file mode 100644 index 00000000..e6864370 --- /dev/null +++ b/Common/src/main/java/libcore/util/FP16.java @@ -0,0 +1,794 @@ +/* + * Copyright (C) 2019 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 libcore.util; +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import android.annotation.SystemApi; +/** + *

The {@code FP16} class is a wrapper and a utility class to manipulate half-precision 16-bit + * IEEE 754 + * floating point data types (also called fp16 or binary16). A half-precision float can be + * created from or converted to single-precision floats, and is stored in a short data type. + * + *

The IEEE 754 standard specifies an fp16 as having the following format:

+ *
    + *
  • Sign bit: 1 bit
  • + *
  • Exponent width: 5 bits
  • + *
  • Significand: 10 bits
  • + *
+ * + *

The format is laid out as follows:

+ *
+ * 1   11111   1111111111
+ * ^   --^--   -----^----
+ * sign  |          |_______ significand
+ *       |
+ *       -- exponent
+ * 
+ * + *

Half-precision floating points can be useful to save memory and/or + * bandwidth at the expense of range and precision when compared to single-precision + * floating points (fp32).

+ *

To help you decide whether fp16 is the right storage type for you need, please + * refer to the table below that shows the available precision throughout the range of + * possible values. The precision column indicates the step size between two + * consecutive numbers in a specific part of the range.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Range startPrecision
01 ⁄ 16,777,216
1 ⁄ 16,3841 ⁄ 16,777,216
1 ⁄ 8,1921 ⁄ 8,388,608
1 ⁄ 4,0961 ⁄ 4,194,304
1 ⁄ 2,0481 ⁄ 2,097,152
1 ⁄ 1,0241 ⁄ 1,048,576
1 ⁄ 5121 ⁄ 524,288
1 ⁄ 2561 ⁄ 262,144
1 ⁄ 1281 ⁄ 131,072
1 ⁄ 641 ⁄ 65,536
1 ⁄ 321 ⁄ 32,768
1 ⁄ 161 ⁄ 16,384
1 ⁄ 81 ⁄ 8,192
1 ⁄ 41 ⁄ 4,096
1 ⁄ 21 ⁄ 2,048
11 ⁄ 1,024
21 ⁄ 512
41 ⁄ 256
81 ⁄ 128
161 ⁄ 64
321 ⁄ 32
641 ⁄ 16
1281 ⁄ 8
2561 ⁄ 4
5121 ⁄ 2
1,0241
2,0482
4,0964
8,1928
16,38416
32,76832
+ * + *

This table shows that numbers higher than 1024 lose all fractional precision.

+ * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class FP16 { + /** + * The number of bits used to represent a half-precision float value. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int SIZE = 16; + /** + * Epsilon is the difference between 1.0 and the next value representable + * by a half-precision floating-point. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short EPSILON = (short) 0x1400; + /** + * Maximum exponent a finite half-precision float may have. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int MAX_EXPONENT = 15; + /** + * Minimum exponent a normalized half-precision float may have. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int MIN_EXPONENT = -14; + /** + * Smallest negative value a half-precision float may have. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short LOWEST_VALUE = (short) 0xfbff; + /** + * Maximum positive finite value a half-precision float may have. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short MAX_VALUE = (short) 0x7bff; + /** + * Smallest positive normal value a half-precision float may have. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short MIN_NORMAL = (short) 0x0400; + /** + * Smallest positive non-zero value a half-precision float may have. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short MIN_VALUE = (short) 0x0001; + /** + * A Not-a-Number representation of a half-precision float. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short NaN = (short) 0x7e00; + /** + * Negative infinity of type half-precision float. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short NEGATIVE_INFINITY = (short) 0xfc00; + /** + * Negative 0 of type half-precision float. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short NEGATIVE_ZERO = (short) 0x8000; + /** + * Positive infinity of type half-precision float. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short POSITIVE_INFINITY = (short) 0x7c00; + /** + * Positive 0 of type half-precision float. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final short POSITIVE_ZERO = (short) 0x0000; + /** + * The offset to shift by to obtain the sign bit. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int SIGN_SHIFT = 15; + /** + * The offset to shift by to obtain the exponent bits. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int EXPONENT_SHIFT = 10; + /** + * The bitmask to AND a number with to obtain the sign bit. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int SIGN_MASK = 0x8000; + /** + * The bitmask to AND a number shifted by {@link #EXPONENT_SHIFT} right, to obtain exponent bits. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int SHIFTED_EXPONENT_MASK = 0x1f; + /** + * The bitmask to AND a number with to obtain significand bits. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int SIGNIFICAND_MASK = 0x3ff; + /** + * The bitmask to AND with to obtain exponent and significand bits. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int EXPONENT_SIGNIFICAND_MASK = 0x7fff; + /** + * The offset of the exponent from the actual value. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int EXPONENT_BIAS = 15; + private static final int FP32_SIGN_SHIFT = 31; + private static final int FP32_EXPONENT_SHIFT = 23; + private static final int FP32_SHIFTED_EXPONENT_MASK = 0xff; + private static final int FP32_SIGNIFICAND_MASK = 0x7fffff; + private static final int FP32_EXPONENT_BIAS = 127; + private static final int FP32_QNAN_MASK = 0x400000; + private static final int FP32_DENORMAL_MAGIC = 126 << 23; + private static final float FP32_DENORMAL_FLOAT = Float.intBitsToFloat(FP32_DENORMAL_MAGIC); + /** Hidden constructor to prevent instantiation. */ + private FP16() {} + /** + *

Compares the two specified half-precision float values. The following + * conditions apply during the comparison:

+ * + *
    + *
  • {@link #NaN} is considered by this method to be equal to itself and greater + * than all other half-precision float values (including {@code #POSITIVE_INFINITY})
  • + *
  • {@link #POSITIVE_ZERO} is considered by this method to be greater than + * {@link #NEGATIVE_ZERO}.
  • + *
+ * + * @param x The first half-precision float value to compare. + * @param y The second half-precision float value to compare + * + * @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a + * value less than {@code 0} if {@code x} is numerically less than {@code y}, + * and a value greater than {@code 0} if {@code x} is numerically greater + * than {@code y} + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static int compare(short x, short y) { + if (less(x, y)) return -1; + if (greater(x, y)) return 1; + // Collapse NaNs, akin to halfToIntBits(), but we want to keep + // (signed) short value types to preserve the ordering of -0.0 + // and +0.0 + short xBits = isNaN(x) ? NaN : x; + short yBits = isNaN(y) ? NaN : y; + return (xBits == yBits ? 0 : (xBits < yBits ? -1 : 1)); + } + /** + * Returns the closest integral half-precision float value to the specified + * half-precision float value. Special values are handled in the + * following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The value of the specified half-precision float rounded to the nearest + * half-precision float value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short rint(short h) { + int bits = h & 0xffff; + int abs = bits & EXPONENT_SIGNIFICAND_MASK; + int result = bits; + if (abs < 0x3c00) { + result &= SIGN_MASK; + if (abs > 0x3800){ + result |= 0x3c00; + } + } else if (abs < 0x6400) { + int exp = 25 - (abs >> 10); + int mask = (1 << exp) - 1; + result += ((1 << (exp - 1)) - (~(abs >> exp) & 1)); + result &= ~mask; + } + if (isNaN((short) result)) { + // if result is NaN mask with qNaN + // (i.e. mask the most significant mantissa bit with 1) + // to comply with hardware implementations (ARM64, Intel, etc). + result |= NaN; + } + return (short) result; + } + /** + * Returns the smallest half-precision float value toward negative infinity + * greater than or equal to the specified half-precision float value. + * Special values are handled in the following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The smallest half-precision float value toward negative infinity + * greater than or equal to the specified half-precision float value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short ceil(short h) { + int bits = h & 0xffff; + int abs = bits & EXPONENT_SIGNIFICAND_MASK; + int result = bits; + if (abs < 0x3c00) { + result &= SIGN_MASK; + result |= 0x3c00 & -(~(bits >> 15) & (abs != 0 ? 1 : 0)); + } else if (abs < 0x6400) { + abs = 25 - (abs >> 10); + int mask = (1 << abs) - 1; + result += mask & ((bits >> 15) - 1); + result &= ~mask; + } + if (isNaN((short) result)) { + // if result is NaN mask with qNaN + // (i.e. mask the most significant mantissa bit with 1) + // to comply with hardware implementations (ARM64, Intel, etc). + result |= NaN; + } + return (short) result; + } + /** + * Returns the largest half-precision float value toward positive infinity + * less than or equal to the specified half-precision float value. + * Special values are handled in the following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The largest half-precision float value toward positive infinity + * less than or equal to the specified half-precision float value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short floor(short h) { + int bits = h & 0xffff; + int abs = bits & EXPONENT_SIGNIFICAND_MASK; + int result = bits; + if (abs < 0x3c00) { + result &= SIGN_MASK; + result |= 0x3c00 & (bits > 0x8000 ? 0xffff : 0x0); + } else if (abs < 0x6400) { + abs = 25 - (abs >> 10); + int mask = (1 << abs) - 1; + result += mask & -(bits >> 15); + result &= ~mask; + } + if (isNaN((short) result)) { + // if result is NaN mask with qNaN + // i.e. (Mask the most significant mantissa bit with 1) + result |= NaN; + } + return (short) result; + } + /** + * Returns the truncated half-precision float value of the specified + * half-precision float value. Special values are handled in the following ways: + *
    + *
  • If the specified half-precision float is NaN, the result is NaN
  • + *
  • If the specified half-precision float is infinity (negative or positive), + * the result is infinity (with the same sign)
  • + *
  • If the specified half-precision float is zero (negative or positive), + * the result is zero (with the same sign)
  • + *
+ * + * @param h A half-precision float value + * @return The truncated half-precision float value of the specified + * half-precision float value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short trunc(short h) { + int bits = h & 0xffff; + int abs = bits & EXPONENT_SIGNIFICAND_MASK; + int result = bits; + if (abs < 0x3c00) { + result &= SIGN_MASK; + } else if (abs < 0x6400) { + abs = 25 - (abs >> 10); + int mask = (1 << abs) - 1; + result &= ~mask; + } + return (short) result; + } + /** + * Returns the smaller of two half-precision float values (the value closest + * to negative infinity). Special values are handled in the following ways: + *
    + *
  • If either value is NaN, the result is NaN
  • + *
  • {@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}
  • + *
+ * + * @param x The first half-precision value + * @param y The second half-precision value + * @return The smaller of the two specified half-precision values + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short min(short x, short y) { + if (isNaN(x)) return NaN; + if (isNaN(y)) return NaN; + if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) { + return (x & SIGN_MASK) != 0 ? x : y; + } + return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) < + ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y; + } + /** + * Returns the larger of two half-precision float values (the value closest + * to positive infinity). Special values are handled in the following ways: + *
    + *
  • If either value is NaN, the result is NaN
  • + *
  • {@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}
  • + *
+ * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return The larger of the two specified half-precision values + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short max(short x, short y) { + if (isNaN(x)) return NaN; + if (isNaN(y)) return NaN; + if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) { + return (x & SIGN_MASK) != 0 ? y : x; + } + return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) > + ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y; + } + /** + * Returns true if the first half-precision float value is less (smaller + * toward negative infinity) than the second half-precision float value. + * If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is less than y, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean less(short x, short y) { + if (isNaN(x)) return false; + if (isNaN(y)) return false; + return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) < + ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff); + } + /** + * Returns true if the first half-precision float value is less (smaller + * toward negative infinity) than or equal to the second half-precision + * float value. If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is less than or equal to y, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean lessEquals(short x, short y) { + if (isNaN(x)) return false; + if (isNaN(y)) return false; + return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <= + ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff); + } + /** + * Returns true if the first half-precision float value is greater (larger + * toward positive infinity) than the second half-precision float value. + * If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is greater than y, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean greater(short x, short y) { + if (isNaN(x)) return false; + if (isNaN(y)) return false; + return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) > + ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff); + } + /** + * Returns true if the first half-precision float value is greater (larger + * toward positive infinity) than or equal to the second half-precision float + * value. If either of the values is NaN, the result is false. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is greater than y, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean greaterEquals(short x, short y) { + if (isNaN(x)) return false; + if (isNaN(y)) return false; + return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >= + ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff); + } + /** + * Returns true if the two half-precision float values are equal. + * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO} + * and {@link #NEGATIVE_ZERO} are considered equal. + * + * @param x The first half-precision value + * @param y The second half-precision value + * + * @return True if x is equal to y, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean equals(short x, short y) { + if (isNaN(x)) return false; + if (isNaN(y)) return false; + return x == y || ((x | y) & EXPONENT_SIGNIFICAND_MASK) == 0; + } + /** + * Returns true if the specified half-precision float value represents + * infinity, false otherwise. + * + * @param h A half-precision float value + * @return True if the value is positive infinity or negative infinity, + * false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean isInfinite(short h) { + return (h & EXPONENT_SIGNIFICAND_MASK) == POSITIVE_INFINITY; + } + /** + * Returns true if the specified half-precision float value represents + * a Not-a-Number, false otherwise. + * + * @param h A half-precision float value + * @return True if the value is a NaN, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean isNaN(short h) { + return (h & EXPONENT_SIGNIFICAND_MASK) > POSITIVE_INFINITY; + } + /** + * Returns true if the specified half-precision float value is normalized + * (does not have a subnormal representation). If the specified value is + * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY}, + * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal + * number, this method returns false. + * + * @param h A half-precision float value + * @return True if the value is normalized, false otherwise + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static boolean isNormalized(short h) { + return (h & POSITIVE_INFINITY) != 0 && (h & POSITIVE_INFINITY) != POSITIVE_INFINITY; + } + /** + *

Converts the specified half-precision float value into a + * single-precision float value. The following special cases are handled:

+ *
    + *
  • If the input is {@link #NaN}, the returned value is {@link Float#NaN}
  • + *
  • If the input is {@link #POSITIVE_INFINITY} or + * {@link #NEGATIVE_INFINITY}, the returned value is respectively + * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}
  • + *
  • If the input is 0 (positive or negative), the returned value is +/-0.0f
  • + *
  • Otherwise, the returned value is a normalized single-precision float value
  • + *
+ * + * @param h The half-precision float value to convert to single-precision + * @return A normalized single-precision float value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static float toFloat(short h) { + int bits = h & 0xffff; + int s = bits & SIGN_MASK; + int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK; + int m = (bits ) & SIGNIFICAND_MASK; + int outE = 0; + int outM = 0; + if (e == 0) { // Denormal or 0 + if (m != 0) { + // Convert denorm fp16 into normalized fp32 + float o = Float.intBitsToFloat(FP32_DENORMAL_MAGIC + m); + o -= FP32_DENORMAL_FLOAT; + return s == 0 ? o : -o; + } + } else { + outM = m << 13; + if (e == 0x1f) { // Infinite or NaN + outE = 0xff; + if (outM != 0) { // SNaNs are quieted + outM |= FP32_QNAN_MASK; + } + } else { + outE = e - EXPONENT_BIAS + FP32_EXPONENT_BIAS; + } + } + int out = (s << 16) | (outE << FP32_EXPONENT_SHIFT) | outM; + return Float.intBitsToFloat(out); + } + /** + *

Converts the specified single-precision float value into a + * half-precision float value. The following special cases are handled:

+ *
    + *
  • If the input is NaN (see {@link Float#isNaN(float)}), the returned + * value is {@link #NaN}
  • + *
  • If the input is {@link Float#POSITIVE_INFINITY} or + * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively + * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}
  • + *
  • If the input is 0 (positive or negative), the returned value is + * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
  • + *
  • If the input is a less than {@link #MIN_VALUE}, the returned value + * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
  • + *
  • If the input is a less than {@link #MIN_NORMAL}, the returned value + * is a denorm half-precision float
  • + *
  • Otherwise, the returned value is rounded to the nearest + * representable half-precision float value
  • + *
+ * + * @param f The single-precision float value to convert to half-precision + * @return A half-precision float value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static short toHalf(float f) { + int bits = Float.floatToRawIntBits(f); + int s = (bits >>> FP32_SIGN_SHIFT ); + int e = (bits >>> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK; + int m = (bits ) & FP32_SIGNIFICAND_MASK; + int outE = 0; + int outM = 0; + if (e == 0xff) { // Infinite or NaN + outE = 0x1f; + outM = m != 0 ? 0x200 : 0; + } else { + e = e - FP32_EXPONENT_BIAS + EXPONENT_BIAS; + if (e >= 0x1f) { // Overflow + outE = 0x1f; + } else if (e <= 0) { // Underflow + if (e < -10) { + // The absolute fp32 value is less than MIN_VALUE, flush to +/-0 + } else { + // The fp32 value is a normalized float less than MIN_NORMAL, + // we convert to a denorm fp16 + m = m | 0x800000; + int shift = 14 - e; + outM = m >> shift; + int lowm = m & ((1 << shift) - 1); + int hway = 1 << (shift - 1); + // if above halfway or exactly halfway and outM is odd + if (lowm + (outM & 1) > hway){ + // Round to nearest even + // Can overflow into exponent bit, which surprisingly is OK. + // This increment relies on the +outM in the return statement below + outM++; + } + } + } else { + outE = e; + outM = m >> 13; + // if above halfway or exactly halfway and outM is odd + if ((m & 0x1fff) + (outM & 0x1) > 0x1000) { + // Round to nearest even + // Can overflow into exponent bit, which surprisingly is OK. + // This increment relies on the +outM in the return statement below + outM++; + } + } + } + // The outM is added here as the +1 increments for outM above can + // cause an overflow in the exponent bit which is OK. + return (short) ((s << SIGN_SHIFT) | (outE << EXPONENT_SHIFT) + outM); + } + /** + *

Returns a hexadecimal string representation of the specified half-precision + * float value. If the value is a NaN, the result is "NaN", + * otherwise the result follows this format:

+ *
    + *
  • If the sign is positive, no sign character appears in the result
  • + *
  • If the sign is negative, the first character is '-'
  • + *
  • If the value is inifinity, the string is "Infinity"
  • + *
  • If the value is 0, the string is "0x0.0p0"
  • + *
  • If the value has a normalized representation, the exponent and + * significand are represented in the string in two fields. The significand + * starts with "0x1." followed by its lowercase hexadecimal + * representation. Trailing zeroes are removed unless all digits are 0, then + * a single zero is used. The significand representation is followed by the + * exponent, represented by "p", itself followed by a decimal + * string of the unbiased exponent
  • + *
  • If the value has a subnormal representation, the significand starts + * with "0x0." followed by its lowercase hexadecimal + * representation. Trailing zeroes are removed unless all digits are 0, then + * a single zero is used. The significand representation is followed by the + * exponent, represented by "p-14"
  • + *
+ * + * @param h A half-precision float value + * @return A hexadecimal string representation of the specified value + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static String toHexString(short h) { + StringBuilder o = new StringBuilder(); + int bits = h & 0xffff; + int s = (bits >>> SIGN_SHIFT ); + int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK; + int m = (bits ) & SIGNIFICAND_MASK; + if (e == 0x1f) { // Infinite or NaN + if (m == 0) { + if (s != 0) o.append('-'); + o.append("Infinity"); + } else { + o.append("NaN"); + } + } else { + if (s == 1) o.append('-'); + if (e == 0) { + if (m == 0) { + o.append("0x0.0p0"); + } else { + o.append("0x0."); + String significand = Integer.toHexString(m); + o.append(significand.replaceFirst("0{2,}$", "")); + o.append("p-14"); + } + } else { + o.append("0x1."); + String significand = Integer.toHexString(m); + o.append(significand.replaceFirst("0{2,}$", "")); + o.append('p'); + o.append(Integer.toString(e - EXPONENT_BIAS)); + } + } + return o.toString(); + } +} \ No newline at end of file diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java new file mode 100644 index 00000000..6e20d544 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2013 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 org.firstinspires.ftc.robotcore.external.android.util; + +/** + * Immutable class for describing width and height dimensions in integer valued units. + * + * Backported from API21, where it was introduced, to here, where we need + * to support API19. + */ +public final class Size { + /** + * Create a new immutable Size instance. + * + * @param width The width of the size, in pixels + * @param height The height of the size, in pixels + */ + public Size(int width, int height) { + mWidth = width; + mHeight = height; + } + + /** + * Get the width of the size (in pixels). + * @return width + */ + public int getWidth() { + return mWidth; + } + + /** + * Get the height of the size (in pixels). + * @return height + */ + public int getHeight() { + return mHeight; + } + + /** + * Check if this size is equal to another size. + *

+ * Two sizes are equal if and only if both their widths and heights are + * equal. + *

+ *

+ * A size object is never equal to any other type of object. + *

+ * + * @return {@code true} if the objects were equal, {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof Size) { + Size other = (Size) obj; + return mWidth == other.mWidth && mHeight == other.mHeight; + } + return false; + } + + /** + * Return the size represented as a string with the format {@code "WxH"} + * + * @return string representation of the size + */ + @Override + public String toString() { + return mWidth + "x" + mHeight; + } + + private static NumberFormatException invalidSize(String s) { + throw new NumberFormatException("Invalid Size: \"" + s + "\""); + } + + /** + * Parses the specified string as a size value. + *

+ * The ASCII characters {@code \}{@code u002a} ('*') and + * {@code \}{@code u0078} ('x') are recognized as separators between + * the width and height.

+ *

+ * For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}. + * However, the method also handles sizes expressed in the + * following forms:

+ *

+ * "width{@code x}height" or + * "width{@code *}height" {@code => new Size(width, height)}, + * where width and height are string integers potentially + * containing a sign, such as "-10", "+7" or "5".

+ * + *
{@code
+     * Size.parseSize("3*+6").equals(new Size(3, 6)) == true
+     * Size.parseSize("-3x-6").equals(new Size(-3, -6)) == true
+     * Size.parseSize("4 by 3") => throws NumberFormatException
+     * }
+ * + * @param string the string representation of a size value. + * @return the size value represented by {@code string}. + * + * @throws NumberFormatException if {@code string} cannot be parsed + * as a size value. + * @throws NullPointerException if {@code string} was {@code null} + */ + public static Size parseSize(String string) + throws NumberFormatException { + if (null==string) throw new IllegalArgumentException("string must not be null"); + + int sep_ix = string.indexOf('*'); + if (sep_ix < 0) { + sep_ix = string.indexOf('x'); + } + if (sep_ix < 0) { + throw invalidSize(string); + } + try { + return new Size(Integer.parseInt(string.substring(0, sep_ix)), + Integer.parseInt(string.substring(sep_ix + 1))); + } catch (NumberFormatException e) { + throw invalidSize(string); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); + } + + private final int mWidth; + private final int mHeight; +} \ No newline at end of file diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java new file mode 100644 index 00000000..9e85951b --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java @@ -0,0 +1,172 @@ +/* +Copyright (c) 2018 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.internal.camera.calibration; + +import androidx.annotation.NonNull; + +import org.firstinspires.ftc.robotcore.external.android.util.Size; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.system.Assert; +import org.firstinspires.ftc.robotcore.internal.system.Misc; + +/** + * An augmentation to {@link CameraIntrinsics} that helps support debugging + * and parsing from XML. + */ +@SuppressWarnings("WeakerAccess") +public class CameraCalibration extends CameraIntrinsics implements Cloneable +{ + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + /** These are not passed to native code */ + protected CameraCalibrationIdentity identity; + protected Size size; + protected boolean remove; + protected final boolean isFake; + + @Override public String toString() + { + return Misc.formatInvariant("CameraCalibration(%s %dx%d f=%.3f,%.3f)", identity, size.getWidth(), size.getHeight(), focalLengthX, focalLengthY); + } + + public CameraCalibrationIdentity getIdentity() + { + return identity; + } + public Size getSize() + { + return size; + } + public boolean getRemove() + { + return remove; + } + public boolean isFake() + { + return isFake; + } + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public CameraCalibration(@NonNull CameraCalibrationIdentity identity, Size size, float focalLengthX, float focalLengthY, float principalPointX, float principalPointY, float[] distortionCoefficients, boolean remove, boolean isFake) throws RuntimeException + { + super(focalLengthX, focalLengthY, principalPointX, principalPointY, distortionCoefficients); + this.identity = identity; + this.size = size; + this.remove = remove; + this.isFake = isFake; + } + + public CameraCalibration(@NonNull CameraCalibrationIdentity identity, int[] size, float[] focalLength, float[] principalPoint, float[] distortionCoefficients, boolean remove, boolean isFake) throws RuntimeException + { + this(identity, new Size(size[0], size[1]), focalLength[0], focalLength[1], principalPoint[0], principalPoint[1], distortionCoefficients, remove, isFake); + if (size.length != 2) throw Misc.illegalArgumentException("frame size must be 2"); + if (principalPoint.length != 2) throw Misc.illegalArgumentException("principal point size must be 2"); + if (focalLength.length != 2) throw Misc.illegalArgumentException("focal length size must be 2"); + if (distortionCoefficients.length != 8) throw Misc.illegalArgumentException("distortion coefficients size must be 8"); + } + + public static CameraCalibration forUnavailable(CameraCalibrationIdentity calibrationIdentity, Size size) + { + if (calibrationIdentity==null) + { + calibrationIdentity = new VendorProductCalibrationIdentity(0, 0); + } + return new CameraCalibration(calibrationIdentity, size, 0, 0, 0, 0, new float[8], false, true); + } + + @SuppressWarnings({"unchecked"}) + protected CameraCalibration memberwiseClone() + { + try { + return (CameraCalibration)super.clone(); + } + catch (CloneNotSupportedException e) + { + throw AppUtil.getInstance().unreachable(); + } + } + + //---------------------------------------------------------------------------------------------- + // Access + //---------------------------------------------------------------------------------------------- + + /* + * From: Dobrev, Niki + * Sent: Wednesday, May 23, 2018 4:57 AM + * + * Other option (in case the aspect ratio stays the same) is to just scale the principal point + * and focal length values to match the resolution currently used before providing them to + * Vuforia in the camera frame callback. Please keep in mind, that this is not optimal and it + * is possible that it doesn't provide optimal results always, but from calibration effort point + * of view is probably easier than doing calibrations for each supported resolution. Scaling + * should work reasonably well in general case, if the camera is not doing anything strange when + * switching capture resolutions. + */ + public CameraCalibration scaledTo(Size newSize) + { + Assert.assertTrue(Misc.approximatelyEquals(getAspectRatio(newSize), getAspectRatio(size))); + double factor = (double)(newSize.getWidth()) / (double)(size.getWidth()); + + CameraCalibration result = memberwiseClone(); + result.size = newSize; + result.focalLengthX *= factor; + result.focalLengthY *= factor; + result.principalPointX *= factor; + result.principalPointY *= factor; + return result; + } + + public double getAspectRatio() + { + return getAspectRatio(size); + } + public double getDiagonal() + { + return getDiagonal(size); + } + + public static double getDiagonal(Size size) + { + return Math.sqrt(size.getWidth() * size.getWidth() + size.getHeight() * size.getHeight()); + } + protected static double getAspectRatio(Size size) + { + return (double)size.getWidth() / (double)size.getHeight(); + } + +} \ No newline at end of file diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java new file mode 100644 index 00000000..883db001 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java @@ -0,0 +1,38 @@ +/* +Copyright (c) 2018 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.internal.camera.calibration; + +public interface CameraCalibrationIdentity +{ + boolean isDegenerate(); +} \ No newline at end of file diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java new file mode 100644 index 00000000..2b609d45 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java @@ -0,0 +1,117 @@ +/* +Copyright (c) 2018 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.internal.camera.calibration; + +import androidx.annotation.NonNull; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; + +import java.util.Arrays; + +/** + * Provides basic information regarding some characteristics which are built-in to / intrinsic + * to a particular camera model. + * + * Note that this information is passed to native code. This class is a Java manifestation of + * of Vuforia::ExternalProvider::CameraIntrinsics found in ExternalProvider.h. + * + * https://docs.opencv.org/2.4/doc/tutorials/calib3d/camera_calibration/camera_calibration.html + * https://docs.opencv.org/3.0-beta/doc/tutorials/calib3d/camera_calibration/camera_calibration.html + * https://www.mathworks.com/help/vision/camera-calibration.html + * + * @see CameraCalibration + */ +@SuppressWarnings("WeakerAccess") +public class CameraIntrinsics +{ + /** Focal length x-component. 0.f if not available. */ + public float focalLengthX; + + /** Focal length y-component. 0.f if not available. */ + public float focalLengthY; + + /** Principal point x-component. 0.f if not available. */ + public float principalPointX; + + /** Principal point y-component. 0.f if not available. */ + public float principalPointY; + + /** + * An 8 element array of distortion coefficients. + * Array should be filled in the following order (r: radial, t:tangential): + * [r0, r1, t0, t1, r2, r3, r4, r5] + * Values that are not available should be set to 0.f. + * + * Yes, the parameter order seems odd, but it is correct. + */ + @NonNull public final float[] distortionCoefficients; + + public CameraIntrinsics(float focalLengthX, float focalLengthY, float principalPointX, float principalPointY, float[] distortionCoefficients) throws RuntimeException + { + if (distortionCoefficients != null && distortionCoefficients.length == 8) + { + this.focalLengthX = focalLengthX; + this.focalLengthY = focalLengthY; + this.principalPointX = principalPointX; + this.principalPointY = principalPointY; + this.distortionCoefficients = Arrays.copyOf(distortionCoefficients, distortionCoefficients.length); + } + else + throw Misc.illegalArgumentException("distortionCoefficients must have length 8"); + } + + public float[] toArray() + { + float[] result = new float[12]; + result[0] = focalLengthX; + result[1] = focalLengthY; + result[2] = principalPointX; + result[3] = principalPointY; + System.arraycopy(distortionCoefficients, 0, result, 4, 8); + return result; + } + + public boolean isDegenerate() + { + return focalLengthX==0 && focalLengthY==0 && principalPointX==0 && principalPointY==0 + && distortionCoefficients[0]==0 + && distortionCoefficients[1]==0 + && distortionCoefficients[2]==0 + && distortionCoefficients[3]==0 + && distortionCoefficients[4]==0 + && distortionCoefficients[5]==0 + && distortionCoefficients[6]==0 + && distortionCoefficients[7]==0; + } + +} \ No newline at end of file diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java new file mode 100644 index 00000000..4d323b4c --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java @@ -0,0 +1,84 @@ +/* +Copyright (c) 2018 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.internal.camera.calibration; + +import org.firstinspires.ftc.robotcore.internal.system.Misc; + +public class VendorProductCalibrationIdentity implements CameraCalibrationIdentity +{ + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + public final int vid; + public final int pid; + + @Override public String toString() + { + return Misc.formatInvariant("%s(vid=0x%04x,pid=0x%04x)", getClass().getSimpleName(), vid, pid); + } + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public VendorProductCalibrationIdentity(int vid, int pid) + { + this.vid = vid; + this.pid = pid; + } + + //---------------------------------------------------------------------------------------------- + // Comparison + //---------------------------------------------------------------------------------------------- + + @Override public boolean isDegenerate() + { + return vid==0 || pid==0; + } + + @Override public boolean equals(Object o) + { + if (o instanceof VendorProductCalibrationIdentity) + { + VendorProductCalibrationIdentity them = (VendorProductCalibrationIdentity)o; + return vid==them.vid && pid==them.pid; + } + return super.equals(o); + } + + @Override public int hashCode() + { + return Integer.valueOf(vid).hashCode() ^ Integer.valueOf(pid).hashCode() ^ 738187; + } +} diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java new file mode 100644 index 00000000..62eda152 --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java @@ -0,0 +1,70 @@ +package org.firstinspires.ftc.robotcore.internal.system; + +import com.qualcomm.robotcore.util.RobotLog; + +public class AppUtil { + + public static final String TAG= "AppUtil"; + + protected AppUtil() { } + + private static AppUtil instance = null; + + public static AppUtil getInstance() { + if(instance == null) { + instance = new AppUtil(); + } + + return instance; + } + + public RuntimeException unreachable() + { + return unreachable(TAG); + } + + public RuntimeException unreachable(Throwable throwable) + { + return unreachable(TAG, throwable); + } + + public RuntimeException unreachable(String tag) + { + return failFast(tag, "internal error: this code is unreachable"); + } + + public RuntimeException unreachable(String tag, Throwable throwable) + { + return failFast(tag, throwable, "internal error: this code is unreachable"); + } + + public RuntimeException failFast(String tag, String format, Object... args) + { + String message = String.format(format, args); + return failFast(tag, message); + } + + public RuntimeException failFast(String tag, String message) + { + RobotLog.ee(tag, message); + exitApplication(-1); + return new RuntimeException("keep compiler happy"); + } + + public RuntimeException failFast(String tag, Throwable throwable, String format, Object... args) + { + String message = String.format(format, args); + return failFast(tag, throwable, message); + } + + public RuntimeException failFast(String tag, Throwable throwable, String message) + { + RobotLog.ee(tag, throwable, message); + exitApplication(-1); + return new RuntimeException("keep compiler happy", throwable); + } + + public void exitApplication(int code) { + System.exit(code); + } +} diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java new file mode 100644 index 00000000..a720625b --- /dev/null +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java @@ -0,0 +1,475 @@ +/* +Copyright (c) 2017 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.internal.system; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +/** + * A collection of misfit utilities. They all need to go somewhere, and we can't + * seem to find a better fit. + */ +@SuppressWarnings("WeakerAccess") +public class Misc +{ + public static final String TAG = "Misc"; + + //---------------------------------------------------------------------------------------------- + // Strings + //---------------------------------------------------------------------------------------------- + + /** Formats the string using what in C# is called the 'invariant' culture */ + public static String formatInvariant(@NonNull String format, Object...args) + { + return String.format(Locale.ROOT, format, args); + } + public static String formatInvariant(@NonNull String format) + { + return format; + } + + public static String formatForUser(@NonNull String format, Object...args) + { + return String.format(Locale.getDefault(), format, args); + } + public static String formatForUser(@NonNull String format) + { + return format; + } + + public static String formatForUser(@StringRes int resId, Object...args) + { + // return AppUtil.getDefContext().getString(resId, args); + throw new RuntimeException("Stub!"); + } + public static String formatForUser(@StringRes int resId) + { + // return AppUtil.getDefContext().getString(resId); + throw new RuntimeException("Stub!"); + } + + public static String encodeEntity(String string) + { + return encodeEntity(string, ""); + } + public static String encodeEntity(String string, String rgchEscape) + { + StringBuilder builder = new StringBuilder(); + for (char ch : string.toCharArray()) + { + switch (ch) + { + case '&': + builder.append("&"); + break; + case '<': + builder.append("<"); + break; + case '>': + builder.append(">"); + break; + case '"': + builder.append("""); + break; + case '\'': + builder.append("'"); + break; + default: + if (rgchEscape.indexOf(ch) >= 0) + builder.append(Misc.formatInvariant("&#x%x;", ch)); + else + builder.append(ch); + break; + } + } + return builder.toString(); + } + public static String decodeEntity(String string) + { + StringBuilder builder = new StringBuilder(); + for (int ich = 0; ich < string.length(); ich++) + { + char ch = string.charAt(ich); + if (ch == '&') + { + ich++; + int ichFirst = ich; + while (string.charAt(ich) != ';') + { + ich++; + } + String payload = string.substring(ichFirst, ich-1); + switch (payload) + { + case "amp": + builder.append('&'); + break; + case "lt": + builder.append('<'); + break; + case "gt": + builder.append('>'); + break; + case "quot": + builder.append('"'); + break; + case "apos": + builder.append('\''); + break; + default: + if (payload.length() > 2 && payload.charAt(0) == '#' && payload.charAt(1) == 'x') + { + payload = "0x" + payload.substring(2); + int i = Integer.decode(payload); + builder.append((char)i); + } + else + throw illegalArgumentException("illegal entity reference"); + } + } + else + builder.append(ch); + } + return builder.toString(); + } + //---------------------------------------------------------------------------------------------- + // Math + //---------------------------------------------------------------------------------------------- + + public static long saturatingAdd(long x, long y) + { + if (x == 0 || y == 0 || (x > 0 ^ y > 0)) + { + //zero+N or one pos, another neg = no problems + return x + y; + } + else if (x > 0) + { + //both pos, can only overflow + return Long.MAX_VALUE - x < y ? Long.MAX_VALUE : x + y; + } + else + { + //both neg, can only underflow + return Long.MIN_VALUE - x > y ? Long.MIN_VALUE : x + y; + } + } + public static int saturatingAdd(int x, int y) + { + if (x == 0 || y == 0 || (x > 0 ^ y > 0)) + { + //zero+N or one pos, another neg = no problems + return x + y; + } + else if (x > 0) + { + //both pos, can only overflow + return Integer.MAX_VALUE - x < y ? Integer.MAX_VALUE : x + y; + } + else + { + //both neg, can only underflow + return Integer.MIN_VALUE - x > y ? Integer.MIN_VALUE : x + y; + } + } + + public static boolean isEven(byte value) + { + return (value & 1) == 0; + } + public static boolean isEven(short value) + { + return (value & 1) == 0; + } + public static boolean isEven(int value) + { + return (value & 1) == 0; + } + public static boolean isEven(long value) + { + return (value & 1) == 0; + } + + public static boolean isOdd(byte value) + { + return !isEven(value); + } + public static boolean isOdd(short value) + { + return !isEven(value); + } + public static boolean isOdd(int value) + { + return !isEven(value); + } + public static boolean isOdd(long value) + { + return !isEven(value); + } + + public static boolean isFinite(double d) + { + return !Double.isNaN(d) && !Double.isInfinite(d); + } + + public static boolean approximatelyEquals(double a, double b) + { + return approximatelyEquals(a, b, 1e-9); + } + + public static boolean approximatelyEquals(double a, double b, double tolerance) + { + if (a==b) return true; // zero and infinity are the important cases + double error = b==0 ? Math.abs(a) : Math.abs(a/b-1.0); // pretty arbitrary + return error < tolerance; + } + + //---------------------------------------------------------------------------------------------- + // UUIDs + //---------------------------------------------------------------------------------------------- + + /** @see UUID Spec */ + public static UUID uuidFromBytes(byte[] rgb, ByteOrder byteOrder) + { + Assert.assertTrue(rgb.length == 16); + + ByteBuffer readBuffer = ByteBuffer.wrap(rgb).order(byteOrder); + ByteBuffer writeBuffer = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); + + // There's funky byte ordering in the first eight bytes + writeBuffer.putInt(readBuffer.getInt()); + writeBuffer.putShort(readBuffer.getShort()); + writeBuffer.putShort(readBuffer.getShort()); + writeBuffer.rewind(); + long mostSignificant = writeBuffer.getLong(); + + // The remaining eight bytes are unordered + writeBuffer.rewind(); + writeBuffer.put(readBuffer); + writeBuffer.rewind(); + long leastSignificant = writeBuffer.getLong(); + + return new UUID(mostSignificant, leastSignificant); + } + + //---------------------------------------------------------------------------------------------- + // Arrays + //---------------------------------------------------------------------------------------------- + + public static boolean contains(byte[] array, byte value) + { + for (byte i : array) + { + if (i == value) return true; + } + return false; + } + + public static boolean contains(short[] array, short value) + { + for (short i : array) + { + if (i == value) return true; + } + return false; + } + + public static boolean contains(int[] array, int value) + { + for (int i : array) + { + if (i == value) return true; + } + return false; + } + + public static boolean contains(long[] array, long value) + { + for (long i : array) + { + if (i == value) return true; + } + return false; + } + + public static T[] toArray(T[] contents, Collection collection) + { + int s = collection.size(); + if (contents.length < s) + { + @SuppressWarnings("unchecked") T[] newArray = (T[]) Array.newInstance(contents.getClass().getComponentType(), s); + contents = newArray; + } + int i = 0; + for (T t : collection) + { + contents[i++] = t; + } + if (contents.length > s) + { + contents[s] = null; + } + return contents; + } + + public static T[] toArray(T[] contents, ArrayList collection) + { + return collection.toArray(contents); + } + + public static long[] toLongArray(Collection collection) + { + long[] result = new long[collection.size()]; + int i = 0; + for (Long value: collection) + { + result[i++] = value; + } + return result; + } + + public static int[] toIntArray(Collection collection) + { + int[] result = new int[collection.size()]; + int i = 0; + for (Integer value : collection) + { + result[i++] = value; + } + return result; + } + + public static short[] toShortArray(Collection collection) + { + short[] result = new short[collection.size()]; + int i = 0; + for (Short value : collection) + { + result[i++] = value; + } + return result; + } + + public static byte[] toByteArray(Collection collection) + { + byte[] result = new byte[collection.size()]; + int i = 0; + for (Byte value: collection) + { + result[i++] = value; + } + return result; + } + + //---------------------------------------------------------------------------------------------- + // Collections + //---------------------------------------------------------------------------------------------- + + public static Set intersect(Set left, Set right) + { + Set result = new HashSet<>(); + for (E element : left) + { + if (right.contains(element)) + { + result.add(element); + } + } + return result; + } + + //---------------------------------------------------------------------------------------------- + // Exceptions + //---------------------------------------------------------------------------------------------- + + public static IllegalArgumentException illegalArgumentException(String message) + { + return new IllegalArgumentException(message); + } + public static IllegalArgumentException illegalArgumentException(String format, Object...args) + { + return new IllegalArgumentException(formatInvariant(format, args)); + } + public static IllegalArgumentException illegalArgumentException(Throwable throwable, String format, Object...args) + { + return new IllegalArgumentException(formatInvariant(format, args), throwable); + } + public static IllegalArgumentException illegalArgumentException(Throwable throwable, String message) + { + return new IllegalArgumentException(message, throwable); + } + + public static IllegalStateException illegalStateException(String message) + { + return new IllegalStateException(message); + } + public static IllegalStateException illegalStateException(String format, Object...args) + { + return new IllegalStateException(formatInvariant(format, args)); + } + public static IllegalStateException illegalStateException(Throwable throwable, String format, Object...args) + { + return new IllegalStateException(formatInvariant(format, args), throwable); + } + public static IllegalStateException illegalStateException(Throwable throwable, String message) + { + return new IllegalStateException(message, throwable); + } + + public static RuntimeException internalError(String message) + { + return new RuntimeException("internal error:" + message); + } + public static RuntimeException internalError(String format, Object...args) + { + return new RuntimeException("internal error:" + formatInvariant(format, args)); + } + public static RuntimeException internalError(Throwable throwable, String format, Object...args) + { + return new RuntimeException("internal error:" + formatInvariant(format, args), throwable); + } + public static RuntimeException internalError(Throwable throwable, String message) + { + return new RuntimeException("internal error:" + message, throwable); + } +} \ No newline at end of file diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index edc856b2..522c1081 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -34,7 +34,8 @@ test { apply from: '../test-logging.gradle' dependencies { - implementation project(':Common') + api project(':Common') + api project(':Vision') implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 8ef34278..d2dfbbac 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -9,7 +9,6 @@ apply from: '../build.common.gradle' dependencies { implementation project(':EOCV-Sim') - implementation project(':Common') implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" diff --git a/Vision/build.gradle b/Vision/build.gradle index ca0abd24..9e5a815f 100644 --- a/Vision/build.gradle +++ b/Vision/build.gradle @@ -20,12 +20,29 @@ publishing { } } +configurations.all { + resolutionStrategy { + cacheChangingModulesFor 0, 'seconds' + } +} + dependencies { implementation project(':Common') - - implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" + + implementation("com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version") { + if(env == 'dev') { changing = true } + } api "org.openpnp:opencv:$opencv_version" + implementation "org.slf4j:slf4j-api:$slf4j_version" implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + + // Compatibility: Skija supports many platforms but we will only be adding + // those that are supported by AprilTagDesktop as well + + implementation("io.github.humbleui:skija-windows-x64:$skija_version") + implementation("io.github.humbleui:skija-linux-x64:$skija_version") + implementation("io.github.humbleui:skija-macos-x64:$skija_version") + implementation("io.github.humbleui:skija-macos-arm64:$skija_version") } \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Bitmap.java b/Vision/src/main/java/android/graphics/Bitmap.java new file mode 100644 index 00000000..973e762a --- /dev/null +++ b/Vision/src/main/java/android/graphics/Bitmap.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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 android.graphics; + +public class Bitmap { + + public final io.github.humbleui.skija.Bitmap theBitmap; + + public Bitmap() { + theBitmap = new io.github.humbleui.skija.Bitmap(); + } + +} diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java new file mode 100644 index 00000000..17404bb1 --- /dev/null +++ b/Vision/src/main/java/android/graphics/Canvas.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 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 android.graphics; + +import io.github.humbleui.skija.Font; +import io.github.humbleui.types.RRect; +import org.firstinspires.ftc.vision.apriltag.AprilTagCanvasAnnotator; + +public class Canvas { + + public final io.github.humbleui.skija.Canvas theCanvas; + + public Canvas(Bitmap bitmap) { + theCanvas = new io.github.humbleui.skija.Canvas(bitmap.theBitmap); + } + + public Canvas drawLine(float x, float y, float x1, float y1, Paint paint) { + theCanvas.drawLine(x, y, x1, y1, paint.thePaint); + return this; + } + + + public Canvas 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); + + return this; + } + + + public Canvas drawText(String text, float x, float y, Paint paint) { + Font font = FontCache.makeFont(paint.getTypeface().theTypeface, paint.getTextSize()); + theCanvas.drawString(text, x, y, font, paint.thePaint); + + return this; + } + + public Canvas rotate(float degrees, float xCenter, float yCenter) { + theCanvas.rotate(degrees); + return this; + } + + public Canvas rotate(float degrees) { + theCanvas.rotate(degrees); + return this; + } + + public int save() { + return theCanvas.save(); + } + + public Canvas restore() { + theCanvas.restore(); + return this; + } + + public Canvas drawLines(float[] points, Paint paint) { + theCanvas.drawLines(points, paint.thePaint); + return this; + } +} diff --git a/Vision/src/main/java/android/graphics/Color.java b/Vision/src/main/java/android/graphics/Color.java new file mode 100644 index 00000000..7460ec3e --- /dev/null +++ b/Vision/src/main/java/android/graphics/Color.java @@ -0,0 +1,1415 @@ +/* + * Copyright (C) 2006 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 android.graphics; +import android.annotation.AnyThread; +import android.annotation.ColorInt; +import android.annotation.ColorLong; +import android.annotation.HalfFloat; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Size; +import android.annotation.SuppressAutoDoc; +import android.util.Half; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.function.DoubleUnaryOperator; +/** + * {@usesMathJax} + * + *

The Color class provides methods for creating, converting and + * manipulating colors. Colors have three different representations:

+ *
    + *
  • Color ints, the most common representation
  • + *
  • Color longs
  • + *
  • Color instances
  • + *
+ *

The section below describe each representation in detail.

+ * + *

Color ints

+ *

Color ints are the most common representation of colors on Android and + * have been used since {link android.os.Build.VERSION_CODES#BASE API level 1}.

+ * + *

A color int always defines a color in the {link ColorSpace.Named#SRGB sRGB} + * color space using 4 components packed in a single 32 bit integer value:

+ * + * + * + * + * + * + * + * + * + *
ComponentNameSizeRange
AAlpha8 bits\([0..255]\)
RRed8 bits\([0..255]\)
GGreen8 bits\([0..255]\)
BBlue8 bits\([0..255]\)
+ * + *

The components in this table are listed in encoding order (see below), + * which is why color ints are called ARGB colors.

+ * + *

Usage in code

+ *

To avoid confusing color ints with arbitrary integer values, it is a + * good practice to annotate them with the @ColorInt annotation + * found in the Android Support Library.

+ * + *

Encoding

+ *

The four components of a color int are encoded in the following way:

+ *
+ * int color = (A & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);
+ * 
+ * + *

Because of this encoding, color ints can easily be described as an integer + * constant in source. For instance, opaque blue is 0xff0000ff + * and yellow is 0xffffff00.

+ * + *

To easily encode color ints, it is recommended to use the static methods + * {@link #argb(int, int, int, int)} and {@link #rgb(int, int, int)}. The second + * method omits the alpha component and assumes the color is opaque (alpha is 255). + * As a convenience this class also offers methods to encode color ints from components + * defined in the \([0..1]\) range: {@link #argb(float, float, float, float)} and + * {@link #rgb(float, float, float)}.

+ * + *

Color longs (defined below) can be easily converted to color ints by invoking + * the {@link #toArgb(long)} method. This method performs a color space conversion + * if needed.

+ * + *

It is also possible to create a color int by invoking the method {@link #toArgb()} + * on a color instance.

+ * + *

Decoding

+ *

The four ARGB components can be individually extracted from a color int + * using the following expressions:

+ *
+ * int A = (color >> 24) & 0xff; // or color >>> 24
+ * int R = (color >> 16) & 0xff;
+ * int G = (color >>  8) & 0xff;
+ * int B = (color      ) & 0xff;
+ * 
+ * + *

This class offers convenience methods to easily extract these components:

+ *
    + *
  • {@link #alpha(int)} to extract the alpha component
  • + *
  • {@link #red(int)} to extract the red component
  • + *
  • {@link #green(int)} to extract the green component
  • + *
  • {@link #blue(int)} to extract the blue component
  • + *
+ * + *

Color longs

+ *

Color longs are a representation introduced in + * {link android.os.Build.VERSION_CODES#O Android O} to store colors in different + * {@link ColorSpace color spaces}, with more precision than color ints.

+ * + *

A color long always defines a color using 4 components packed in a single + * 64 bit long value. One of these components is always alpha while the other + * three components depend on the color space's {@link ColorSpace.Model color model}. + * The most common color model is the {@link ColorSpace.Model#RGB RGB} model in + * which the components represent red, green and blue values.

+ * + *

Component ranges: the ranges defined in the tables + * below indicate the ranges that can be encoded in a color long. They do not + * represent the actual ranges as they may differ per color space. For instance, + * the RGB components of a color in the {@link ColorSpace.Named#DISPLAY_P3 Display P3} + * color space use the \([0..1]\) range. Please refer to the documentation of the + * various {@link ColorSpace.Named color spaces} to find their respective ranges.

+ * + *

Alpha range: while alpha is encoded in a color long using + * a 10 bit integer (thus using a range of \([0..1023]\)), it is converted to and + * from \([0..1]\) float values when decoding and encoding color longs.

+ * + *

sRGB color space: for compatibility reasons and ease of + * use, color longs encoding {@link ColorSpace.Named#SRGB sRGB} colors do not + * use the same encoding as other color longs.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ComponentNameSizeRange
{@link ColorSpace.Model#RGB RGB} color model
RRed16 bits\([-65504.0, 65504.0]\)
GGreen16 bits\([-65504.0, 65504.0]\)
BBlue16 bits\([-65504.0, 65504.0]\)
AAlpha10 bits\([0..1023]\)
Color space6 bits\([0..63]\)
{@link ColorSpace.Named#SRGB sRGB} color space
AAlpha8 bits\([0..255]\)
RRed8 bits\([0..255]\)
GGreen8 bits\([0..255]\)
BBlue8 bits\([0..255]\)
XUnused32 bits\(0\)
{@link ColorSpace.Model#XYZ XYZ} color model
XX16 bits\([-65504.0, 65504.0]\)
YY16 bits\([-65504.0, 65504.0]\)
ZZ16 bits\([-65504.0, 65504.0]\)
AAlpha10 bits\([0..1023]\)
Color space6 bits\([0..63]\)
{@link ColorSpace.Model#XYZ Lab} color model
LL16 bits\([-65504.0, 65504.0]\)
aa16 bits\([-65504.0, 65504.0]\)
bb16 bits\([-65504.0, 65504.0]\)
AAlpha10 bits\([0..1023]\)
Color space6 bits\([0..63]\)
{@link ColorSpace.Model#CMYK CMYK} color model
Unsupported
+ * + *

The components in this table are listed in encoding order (see below), + * which is why color longs in the RGB model are called RGBA colors (even if + * this doesn't quite hold for the special case of sRGB colors).

+ * + *

The color long encoding relies on half-precision float values (fp16). If you + * wish to know more about the limitations of half-precision float values, please + * refer to the documentation of the {@link Half} class.

+ * + *

Usage in code

+ *

To avoid confusing color longs with arbitrary long values, it is a + * good practice to annotate them with the @ColorLong annotation + * found in the Android Support Library.

+ * + *

Encoding

+ * + *

Given the complex nature of color longs, it is strongly encouraged to use + * the various methods provided by this class to encode them.

+ * + *

The most flexible way to encode a color long is to use the method + * {@link #pack(float, float, float, float, ColorSpace)}. This method allows you + * to specify three color components (typically RGB), an alpha component and a + * color space. To encode sRGB colors, use {@link #pack(float, float, float)} + * and {@link #pack(float, float, float, float)} which are the + * equivalent of {@link #rgb(int, int, int)} and {@link #argb(int, int, int, int)} + * for color ints. If you simply need to convert a color int into a color long, + * use {@link #pack(int)}.

+ * + *

It is also possible to create a color long value by invoking the method + * {@link #pack()} on a color instance.

+ * + *

Decoding

+ * + *

This class offers convenience methods to easily extract the components + * of a color long:

+ *
    + *
  • {@link #alpha(long)} to extract the alpha component
  • + *
  • {@link #red(long)} to extract the red/X/L component
  • + *
  • {@link #green(long)} to extract the green/Y/a component
  • + *
  • {@link #blue(long)} to extract the blue/Z/b component
  • + *
+ * + *

The values returned by these methods depend on the color space encoded + * in the color long. The values are however typically in the \([0..1]\) range + * for RGB colors. Please refer to the documentation of the various + * {@link ColorSpace.Named color spaces} for the exact ranges.

+ * + *

Color instances

+ *

Color instances are a representation introduced in + * {link android.os.Build.VERSION_CODES#O Android O} to store colors in different + * {@link ColorSpace color spaces}, with more precision than both color ints and + * color longs. Color instances also offer the ability to store more than 4 + * components if necessary.

+ * + *

Colors instances are immutable and can be created using one of the various + * valueOf methods. For instance:

+ *
+ * // sRGB
+ * Color opaqueRed = Color.valueOf(0xffff0000); // from a color int
+ * Color translucentRed = Color.valueOf(1.0f, 0.0f, 0.0f, 0.5f);
+ *
+ * // Wide gamut color
+ * {@literal @}ColorLong long p3 = pack(1.0f, 1.0f, 0.0f, 1.0f, colorSpaceP3);
+ * Color opaqueYellow = Color.valueOf(p3); // from a color long
+ *
+ * // CIE L*a*b* color space
+ * ColorSpace lab = ColorSpace.get(ColorSpace.Named.LAB);
+ * Color green = Color.valueOf(100.0f, -128.0f, 128.0f, 1.0f, lab);
+ * 
+ * + *

Color instances can be converted to color ints ({@link #toArgb()}) or + * color longs ({@link #pack()}). They also offer easy access to their various + * components using the following methods:

+ *
    + *
  • {@link #alpha()}, returns the alpha component value
  • + *
  • {@link #red()}, returns the red component value (or first + * component value in non-RGB models)
  • + *
  • {@link #green()}, returns the green component value (or second + * component value in non-RGB models)
  • + *
  • {@link #blue()}, returns the blue component value (or third + * component value in non-RGB models)
  • + *
  • {@link #getComponent(int)}, returns a specific component value
  • + *
  • {@link #getComponents()}, returns all component values as an array
  • + *
+ * + *

Color space conversions

+ *

You can convert colors from one color space to another using + * {@link ColorSpace#connect(ColorSpace, ColorSpace)} and its variants. However, + * the Color class provides a few convenience methods to simplify + * the process. Here is a brief description of some of them:

+ *
    + *
  • {@link #convert(ColorSpace)} to convert a color instance in a color + * space to a new color instance in a different color space
  • + *
  • {@link #convert(float, float, float, float, ColorSpace, ColorSpace)} to + * convert a color from a source color space to a destination color space
  • + *
  • {@link #convert(long, ColorSpace)} to convert a color long from its + * built-in color space to a destination color space
  • + *
  • {@link #convert(int, ColorSpace)} to convert a color int from sRGB + * to a destination color space
  • + *
+ * + *

Please refere to the {@link ColorSpace} documentation for more + * information.

+ * + *

Alpha and transparency

+ *

The alpha component of a color defines the level of transparency of a + * color. When the alpha component is 0, the color is completely transparent. + * When the alpha is component is 1 (in the \([0..1]\) range) or 255 (in the + * \([0..255]\) range), the color is completely opaque.

+ * + *

The color representations described above do not use pre-multiplied + * color components (a pre-multiplied color component is a color component + * that has been multiplied by the value of the alpha component). + * For instance, the color int representation of opaque red is + * 0xffff0000. For semi-transparent (50%) red, the + * representation becomes 0x80ff0000. The equivalent color + * instance representations would be (1.0, 0.0, 0.0, 1.0) + * and (1.0, 0.0, 0.0, 0.5).

+ */ +@AnyThread +@SuppressAutoDoc +public class Color { + @ColorInt public static final int BLACK = 0xFF000000; + @ColorInt public static final int DKGRAY = 0xFF444444; + @ColorInt public static final int GRAY = 0xFF888888; + @ColorInt public static final int LTGRAY = 0xFFCCCCCC; + @ColorInt public static final int WHITE = 0xFFFFFFFF; + @ColorInt public static final int RED = 0xFFFF0000; + @ColorInt public static final int GREEN = 0xFF00FF00; + @ColorInt public static final int BLUE = 0xFF0000FF; + @ColorInt public static final int YELLOW = 0xFFFFFF00; + @ColorInt public static final int CYAN = 0xFF00FFFF; + @ColorInt public static final int MAGENTA = 0xFFFF00FF; + @ColorInt public static final int TRANSPARENT = 0; + @NonNull + @Size(min = 4, max = 5) + private final float[] mComponents; + @NonNull + private final ColorSpace mColorSpace; + /** + * Creates a new color instance set to opaque black in the + * {@link ColorSpace.Named#SRGB sRGB} color space. + * + * @see #valueOf(float, float, float) + * @see #valueOf(float, float, float, float) + * @see #valueOf(float, float, float, float, ColorSpace) + * @see #valueOf(float[], ColorSpace) + * @see #valueOf(int) + * @see #valueOf(long) + */ + public Color() { + // This constructor is required for compatibility with previous APIs + mComponents = new float[] { 0.0f, 0.0f, 0.0f, 1.0f }; + mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + } + /** + * Creates a new color instance in the {@link ColorSpace.Named#SRGB sRGB} + * color space. + * + * @param r The value of the red channel, must be in [0..1] range + * @param g The value of the green channel, must be in [0..1] range + * @param b The value of the blue channel, must be in [0..1] range + * @param a The value of the alpha channel, must be in [0..1] range + */ + private Color(float r, float g, float b, float a) { + this(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB)); + } + /** + * Creates a new color instance in the specified color space. The color space + * must have a 3 components model. + * + * @param r The value of the red channel, must be in the color space defined range + * @param g The value of the green channel, must be in the color space defined range + * @param b The value of the blue channel, must be in the color space defined range + * @param a The value of the alpha channel, must be in [0..1] range + * @param colorSpace This color's color space, cannot be null + */ + private Color(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) { + mComponents = new float[] { r, g, b, a }; + mColorSpace = colorSpace; + } + /** + * Creates a new color instance in the specified color space. + * + * @param components An array of color components, plus alpha + * @param colorSpace This color's color space, cannot be null + */ + private Color(@Size(min = 4, max = 5) float[] components, @NonNull ColorSpace colorSpace) { + mComponents = components; + mColorSpace = colorSpace; + } + /** + * Returns this color's color space. + * + * @return A non-null instance of {@link ColorSpace} + */ + @NonNull + public ColorSpace getColorSpace() { + return mColorSpace; + } + /** + * Returns the color model of this color. + * + * @return A non-null {@link ColorSpace.Model} + */ + public ColorSpace.Model getModel() { + return mColorSpace.getModel(); + } + /** + * Indicates whether this color color is in a wide-gamut color space. + * See {@link ColorSpace#isWideGamut()} for a definition of a wide-gamut + * color space. + * + * @return True if this color is in a wide-gamut color space, false otherwise + * + * @see #isSrgb() + * @see ColorSpace#isWideGamut() + */ + public boolean isWideGamut() { + return getColorSpace().isWideGamut(); + } + /** + * Indicates whether this color is in the {@link ColorSpace.Named#SRGB sRGB} + * color space. + * + * @return True if this color is in the sRGB color space, false otherwise + * + * @see #isWideGamut() + */ + public boolean isSrgb() { + return getColorSpace().isSrgb(); + } + /** + * Returns the number of components that form a color value according + * to this color space's color model, plus one extra component for + * alpha. + * + * @return The integer 4 or 5 + */ + @IntRange(from = 4, to = 5) + public int getComponentCount() { + return mColorSpace.getComponentCount() + 1; + } + /** + * Packs this color into a color long. See the documentation of this class + * for a description of the color long format. + * + * @return A color long + * + * @throws IllegalArgumentException If this color's color space has the id + * {@link ColorSpace#MIN_ID} or if this color has more than 4 components + */ + @ColorLong + public long pack() { + return pack(mComponents[0], mComponents[1], mComponents[2], mComponents[3], mColorSpace); + } + /** + * Converts this color from its color space to the specified color space. + * The conversion is done using the default rendering intent as specified + * by {@link ColorSpace#connect(ColorSpace, ColorSpace)}. + * + * @param colorSpace The destination color space, cannot be null + * + * @return A non-null color instance in the specified color space + */ + @NonNull + public Color convert(@NonNull ColorSpace colorSpace) { + ColorSpace.Connector connector = ColorSpace.connect(mColorSpace, colorSpace); + float[] color = new float[] { + mComponents[0], mComponents[1], mComponents[2], mComponents[3] + }; + connector.transform(color); + return new Color(color, colorSpace); + } + /** + * Converts this color to an ARGB color int. A color int is always in + * the {@link ColorSpace.Named#SRGB sRGB} color space. This implies + * a color space conversion is applied if needed. + * + * @return An ARGB color in the sRGB color space + */ + @ColorInt + public int toArgb() { + if (mColorSpace.isSrgb()) { + return ((int) (mComponents[3] * 255.0f + 0.5f) << 24) | + ((int) (mComponents[0] * 255.0f + 0.5f) << 16) | + ((int) (mComponents[1] * 255.0f + 0.5f) << 8) | + (int) (mComponents[2] * 255.0f + 0.5f); + } + float[] color = new float[] { + mComponents[0], mComponents[1], mComponents[2], mComponents[3] + }; + // The transformation saturates the output + ColorSpace.connect(mColorSpace).transform(color); + return ((int) (color[3] * 255.0f + 0.5f) << 24) | + ((int) (color[0] * 255.0f + 0.5f) << 16) | + ((int) (color[1] * 255.0f + 0.5f) << 8) | + (int) (color[2] * 255.0f + 0.5f); + } + /** + *

Returns the value of the red component in the range defined by this + * color's color space (see {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}).

+ * + *

If this color's color model is not {@link ColorSpace.Model#RGB RGB}, + * calling this method is equivalent to getComponent(0).

+ * + * @see #alpha() + * @see #red() + * @see #green + * @see #getComponents() + */ + public float red() { + return mComponents[0]; + } + /** + *

Returns the value of the green component in the range defined by this + * color's color space (see {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}).

+ * + *

If this color's color model is not {@link ColorSpace.Model#RGB RGB}, + * calling this method is equivalent to getComponent(1).

+ * + * @see #alpha() + * @see #red() + * @see #green + * @see #getComponents() + */ + public float green() { + return mComponents[1]; + } + /** + *

Returns the value of the blue component in the range defined by this + * color's color space (see {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}).

+ * + *

If this color's color model is not {@link ColorSpace.Model#RGB RGB}, + * calling this method is equivalent to getComponent(2).

+ * + * @see #alpha() + * @see #red() + * @see #green + * @see #getComponents() + */ + public float blue() { + return mComponents[2]; + } + /** + * Returns the value of the alpha component in the range \([0..1]\). + * Calling this method is equivalent to + * getComponent(getComponentCount() - 1). + * + * @see #red() + * @see #green() + * @see #blue() + * @see #getComponents() + * @see #getComponent(int) + */ + public float alpha() { + return mComponents[mComponents.length - 1]; + } + /** + * Returns this color's components as a new array. The last element of the + * array is always the alpha component. + * + * @return A new, non-null array whose size is equal to {@link #getComponentCount()} + * + * @see #getComponent(int) + */ + @NonNull + @Size(min = 4, max = 5) + public float[] getComponents() { + return Arrays.copyOf(mComponents, mComponents.length); + } + /** + * Copies this color's components in the supplied array. The last element of the + * array is always the alpha component. + * + * @param components An array of floats whose size must be at least + * {@link #getComponentCount()}, can be null + * @return The array passed as a parameter if not null, or a new array of length + * {@link #getComponentCount()} + * + * @see #getComponent(int) + * + * @throws IllegalArgumentException If the specified array's length is less than + * {@link #getComponentCount()} + */ + @NonNull + @Size(min = 4) + public float[] getComponents(@Nullable @Size(min = 4) float[] components) { + if (components == null) { + return Arrays.copyOf(mComponents, mComponents.length); + } + if (components.length < mComponents.length) { + throw new IllegalArgumentException("The specified array's length must be at " + + "least " + mComponents.length); + } + System.arraycopy(mComponents, 0, components, 0, mComponents.length); + return components; + } + /** + *

Returns the value of the specified component in the range defined by + * this color's color space (see {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}).

+ * + *

If the requested component index is {@link #getComponentCount()}, + * this method returns the alpha component, always in the range + * \([0..1]\).

+ * + * @see #getComponents() + * + * @throws ArrayIndexOutOfBoundsException If the specified component index + * is < 0 or >= {@link #getComponentCount()} + */ + public float getComponent(@IntRange(from = 0, to = 4) int component) { + return mComponents[component]; + } + /** + *

Returns the relative luminance of this color.

+ * + *

Based on the formula for relative luminance defined in WCAG 2.0, + * W3C Recommendation 11 December 2008.

+ * + * @return A value between 0 (darkest black) and 1 (lightest white) + * + * @throws IllegalArgumentException If the this color's color space + * does not use the {@link ColorSpace.Model#RGB RGB} color model + */ + public float luminance() { + if (mColorSpace.getModel() != ColorSpace.Model.RGB) { + throw new IllegalArgumentException("The specified color must be encoded in an RGB " + + "color space. The supplied color space is " + mColorSpace.getModel()); + } + DoubleUnaryOperator eotf = ((ColorSpace.Rgb) mColorSpace).getEotf(); + double r = eotf.applyAsDouble(mComponents[0]); + double g = eotf.applyAsDouble(mComponents[1]); + double b = eotf.applyAsDouble(mComponents[2]); + return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b))); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Color color = (Color) o; + //noinspection SimplifiableIfStatement + if (!Arrays.equals(mComponents, color.mComponents)) return false; + return mColorSpace.equals(color.mColorSpace); + } + @Override + public int hashCode() { + int result = Arrays.hashCode(mComponents); + result = 31 * result + mColorSpace.hashCode(); + return result; + } + /** + *

Returns a string representation of the object. This method returns + * a string equal to the value of:

+ * + *
+     * "Color(" + r + ", " + g + ", " + b + ", " + a +
+     *         ", " + getColorSpace().getName + ')'
+     * 
+ * + *

For instance, the string representation of opaque black in the sRGB + * color space is equal to the following value:

+ * + *
+     * Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1)
+     * 
+ * + * @return A non-null string representation of the object + */ + @Override + @NonNull + public String toString() { + StringBuilder b = new StringBuilder("Color("); + for (float c : mComponents) { + b.append(c).append(", "); + } + b.append(mColorSpace.getName()); + b.append(')'); + return b.toString(); + } + /** + * Returns the color space encoded in the specified color long. + * + * @param color The color long whose color space to extract + * @return A non-null color space instance + * @throws IllegalArgumentException If the encoded color space is invalid or unknown + * + * @see #red(long) + * @see #green(long) + * @see #blue(long) + * @see #alpha(long) + */ + @NonNull + public static ColorSpace colorSpace(@ColorLong long color) { + return ColorSpace.get((int) (color & 0x3fL)); + } + /** + * Returns the red component encoded in the specified color long. + * The range of the returned value depends on the color space + * associated with the specified color. The color space can be + * queried by calling {@link #colorSpace(long)}. + * + * @param color The color long whose red channel to extract + * @return A float value with a range defined by the specified color's + * color space + * + * @see #colorSpace(long) + * @see #green(long) + * @see #blue(long) + * @see #alpha(long) + */ + public static float red(@ColorLong long color) { + if ((color & 0x3fL) == 0L) return ((color >> 48) & 0xff) / 255.0f; + return Half.toFloat((short) ((color >> 48) & 0xffff)); + } + /** + * Returns the green component encoded in the specified color long. + * The range of the returned value depends on the color space + * associated with the specified color. The color space can be + * queried by calling {@link #colorSpace(long)}. + * + * @param color The color long whose green channel to extract + * @return A float value with a range defined by the specified color's + * color space + * + * @see #colorSpace(long) + * @see #red(long) + * @see #blue(long) + * @see #alpha(long) + */ + public static float green(@ColorLong long color) { + if ((color & 0x3fL) == 0L) return ((color >> 40) & 0xff) / 255.0f; + return Half.toFloat((short) ((color >> 32) & 0xffff)); + } + /** + * Returns the blue component encoded in the specified color long. + * The range of the returned value depends on the color space + * associated with the specified color. The color space can be + * queried by calling {@link #colorSpace(long)}. + * + * @param color The color long whose blue channel to extract + * @return A float value with a range defined by the specified color's + * color space + * + * @see #colorSpace(long) + * @see #red(long) + * @see #green(long) + * @see #alpha(long) + */ + public static float blue(@ColorLong long color) { + if ((color & 0x3fL) == 0L) return ((color >> 32) & 0xff) / 255.0f; + return Half.toFloat((short) ((color >> 16) & 0xffff)); + } + /** + * Returns the alpha component encoded in the specified color long. + * The returned value is always in the range \([0..1]\). + * + * @param color The color long whose alpha channel to extract + * @return A float value in the range \([0..1]\) + * + * @see #colorSpace(long) + * @see #red(long) + * @see #green(long) + * @see #blue(long) + */ + public static float alpha(@ColorLong long color) { + if ((color & 0x3fL) == 0L) return ((color >> 56) & 0xff) / 255.0f; + return ((color >> 6) & 0x3ff) / 1023.0f; + } + /** + * Indicates whether the specified color is in the + * {@link ColorSpace.Named#SRGB sRGB} color space. + * + * @param color The color to test + * @return True if the color is in the sRGB color space, false otherwise + * @throws IllegalArgumentException If the encoded color space is invalid or unknown + * + * @see #isInColorSpace(long, ColorSpace) + * @see #isWideGamut(long) + */ + public static boolean isSrgb(@ColorLong long color) { + return colorSpace(color).isSrgb(); + } + /** + * Indicates whether the specified color is in a wide-gamut color space. + * See {@link ColorSpace#isWideGamut()} for a definition of a wide-gamut + * color space. + * + * @param color The color to test + * @return True if the color is in a wide-gamut color space, false otherwise + * @throws IllegalArgumentException If the encoded color space is invalid or unknown + * + * @see #isInColorSpace(long, ColorSpace) + * @see #isSrgb(long) + * @see ColorSpace#isWideGamut() + */ + public static boolean isWideGamut(@ColorLong long color) { + return colorSpace(color).isWideGamut(); + } + /** + * Indicates whether the specified color is in the specified color space. + * + * @param color The color to test + * @param colorSpace The color space to test against + * @return True if the color is in the specified color space, false otherwise + * + * @see #isSrgb(long) + * @see #isWideGamut(long) + */ + public static boolean isInColorSpace(@ColorLong long color, @NonNull ColorSpace colorSpace) { + return (int) (color & 0x3fL) == colorSpace.getId(); + } + /** + * Converts the specified color long to an ARGB color int. A color int is + * always in the {@link ColorSpace.Named#SRGB sRGB} color space. This implies + * a color space conversion is applied if needed. + * + * @return An ARGB color in the sRGB color space + * @throws IllegalArgumentException If the encoded color space is invalid or unknown + */ + @ColorInt + public static int toArgb(@ColorLong long color) { + if ((color & 0x3fL) == 0L) return (int) (color >> 32); + float r = red(color); + float g = green(color); + float b = blue(color); + float a = alpha(color); + // The transformation saturates the output + float[] c = ColorSpace.connect(colorSpace(color)).transform(r, g, b); + return ((int) (a * 255.0f + 0.5f) << 24) | + ((int) (c[0] * 255.0f + 0.5f) << 16) | + ((int) (c[1] * 255.0f + 0.5f) << 8) | + (int) (c[2] * 255.0f + 0.5f); + } + /** + * Creates a new Color instance from an ARGB color int. + * The resulting color is in the {@link ColorSpace.Named#SRGB sRGB} + * color space. + * + * @param color The ARGB color int to create a Color from + * @return A non-null instance of {@link Color} + */ + @NonNull + public static Color valueOf(@ColorInt int color) { + float r = ((color >> 16) & 0xff) / 255.0f; + float g = ((color >> 8) & 0xff) / 255.0f; + float b = ((color ) & 0xff) / 255.0f; + float a = ((color >> 24) & 0xff) / 255.0f; + return new Color(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB)); + } + /** + * Creates a new Color instance from a color long. + * The resulting color is in the same color space as the specified color long. + * + * @param color The color long to create a Color from + * @return A non-null instance of {@link Color} + * @throws IllegalArgumentException If the encoded color space is invalid or unknown + */ + @NonNull + public static Color valueOf(@ColorLong long color) { + return new Color(red(color), green(color), blue(color), alpha(color), colorSpace(color)); + } + /** + * Creates a new opaque Color in the {@link ColorSpace.Named#SRGB sRGB} + * color space with the specified red, green and blue component values. The component + * values must be in the range \([0..1]\). + * + * @param r The red component of the opaque sRGB color to create, in \([0..1]\) + * @param g The green component of the opaque sRGB color to create, in \([0..1]\) + * @param b The blue component of the opaque sRGB color to create, in \([0..1]\) + * @return A non-null instance of {@link Color} + */ + @NonNull + public static Color valueOf(float r, float g, float b) { + return new Color(r, g, b, 1.0f); + } + /** + * Creates a new Color in the {@link ColorSpace.Named#SRGB sRGB} + * color space with the specified red, green, blue and alpha component values. + * The component values must be in the range \([0..1]\). + * + * @param r The red component of the sRGB color to create, in \([0..1]\) + * @param g The green component of the sRGB color to create, in \([0..1]\) + * @param b The blue component of the sRGB color to create, in \([0..1]\) + * @param a The alpha component of the sRGB color to create, in \([0..1]\) + * @return A non-null instance of {@link Color} + */ + @NonNull + public static Color valueOf(float r, float g, float b, float a) { + return new Color(saturate(r), saturate(g), saturate(b), saturate(a)); + } + /** + * Creates a new Color in the specified color space with the + * specified red, green, blue and alpha component values. The range of the + * components is defined by {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}. The values passed to this method + * must be in the proper range. + * + * @param r The red component of the color to create + * @param g The green component of the color to create + * @param b The blue component of the color to create + * @param a The alpha component of the color to create, in \([0..1]\) + * @param colorSpace The color space of the color to create + * @return A non-null instance of {@link Color} + * + * @throws IllegalArgumentException If the specified color space uses a + * color model with more than 3 components + */ + @NonNull + public static Color valueOf(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) { + if (colorSpace.getComponentCount() > 3) { + throw new IllegalArgumentException("The specified color space must use a color model " + + "with at most 3 color components"); + } + return new Color(r, g, b, a, colorSpace); + } + /** + *

Creates a new Color in the specified color space with the + * specified component values. The range of the components is defined by + * {@link ColorSpace#getMinValue(int)} and {@link ColorSpace#getMaxValue(int)}. + * The values passed to this method must be in the proper range. The alpha + * component is always in the range \([0..1]\).

+ * + *

The length of the array of components must be at least + * {@link ColorSpace#getComponentCount()} + 1. The component at index + * {@link ColorSpace#getComponentCount()} is always alpha.

+ * + * @param components The components of the color to create, with alpha as the last component + * @param colorSpace The color space of the color to create + * @return A non-null instance of {@link Color} + * + * @throws IllegalArgumentException If the array of components is smaller than + * required by the color space + */ + @NonNull + public static Color valueOf(@NonNull @Size(min = 4, max = 5) float[] components, + @NonNull ColorSpace colorSpace) { + if (components.length < colorSpace.getComponentCount() + 1) { + throw new IllegalArgumentException("Received a component array of length " + + components.length + " but the color model requires " + + (colorSpace.getComponentCount() + 1) + " (including alpha)"); + } + return new Color(Arrays.copyOf(components, colorSpace.getComponentCount() + 1), colorSpace); + } + /** + * Converts the specified ARGB color int to an RGBA color long in the sRGB + * color space. See the documentation of this class for a description of + * the color long format. + * + * @param color The ARGB color int to convert to an RGBA color long in sRGB + * + * @return A color long + */ + @ColorLong + public static long pack(@ColorInt int color) { + return (color & 0xffffffffL) << 32; + } + /** + * Packs the sRGB color defined by the specified red, green and blue component + * values into an RGBA color long in the sRGB color space. The alpha component + * is set to 1.0. See the documentation of this class for a description of the + * color long format. + * + * @param red The red component of the sRGB color to create, in \([0..1]\) + * @param green The green component of the sRGB color to create, in \([0..1]\) + * @param blue The blue component of the sRGB color to create, in \([0..1]\) + * + * @return A color long + */ + @ColorLong + public static long pack(float red, float green, float blue) { + return pack(red, green, blue, 1.0f, ColorSpace.get(ColorSpace.Named.SRGB)); + } + /** + * Packs the sRGB color defined by the specified red, green, blue and alpha + * component values into an RGBA color long in the sRGB color space. See the + * documentation of this class for a description of the color long format. + * + * @param red The red component of the sRGB color to create, in \([0..1]\) + * @param green The green component of the sRGB color to create, in \([0..1]\) + * @param blue The blue component of the sRGB color to create, in \([0..1]\) + * @param alpha The alpha component of the sRGB color to create, in \([0..1]\) + * + * @return A color long + */ + @ColorLong + public static long pack(float red, float green, float blue, float alpha) { + return pack(red, green, blue, alpha, ColorSpace.get(ColorSpace.Named.SRGB)); + } + /** + *

Packs the 3 component color defined by the specified red, green, blue and + * alpha component values into a color long in the specified color space. See the + * documentation of this class for a description of the color long format.

+ * + *

The red, green and blue components must be in the range defined by the + * specified color space. See {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}.

+ * + * @param red The red component of the color to create + * @param green The green component of the color to create + * @param blue The blue component of the color to create + * @param alpha The alpha component of the color to create, in \([0..1]\) + * + * @return A color long + * + * @throws IllegalArgumentException If the color space's id is {@link ColorSpace#MIN_ID} + * or if the color space's color model has more than 3 components + */ + @ColorLong + public static long pack(float red, float green, float blue, float alpha, + @NonNull ColorSpace colorSpace) { + if (colorSpace.isSrgb()) { + int argb = + ((int) (alpha * 255.0f + 0.5f) << 24) | + ((int) (red * 255.0f + 0.5f) << 16) | + ((int) (green * 255.0f + 0.5f) << 8) | + (int) (blue * 255.0f + 0.5f); + return (argb & 0xffffffffL) << 32; + } + int id = colorSpace.getId(); + if (id == ColorSpace.MIN_ID) { + throw new IllegalArgumentException( + "Unknown color space, please use a color space returned by ColorSpace.get()"); + } + if (colorSpace.getComponentCount() > 3) { + throw new IllegalArgumentException( + "The color space must use a color model with at most 3 components"); + } + @HalfFloat short r = Half.toHalf(red); + @HalfFloat short g = Half.toHalf(green); + @HalfFloat short b = Half.toHalf(blue); + int a = (int) (Math.max(0.0f, Math.min(alpha, 1.0f)) * 1023.0f + 0.5f); + // Suppress sign extension + return (r & 0xffffL) << 48 | + (g & 0xffffL) << 32 | + (b & 0xffffL) << 16 | + (a & 0x3ffL ) << 6 | + id & 0x3fL; + } + /** + * Converts the specified ARGB color int from the {@link ColorSpace.Named#SRGB sRGB} + * color space into the specified destination color space. The resulting color is + * returned as a color long. See the documentation of this class for a description + * of the color long format. + * + * @param color The sRGB color int to convert + * @param colorSpace The destination color space + * @return A color long in the destination color space + */ + @ColorLong + public static long convert(@ColorInt int color, @NonNull ColorSpace colorSpace) { + float r = ((color >> 16) & 0xff) / 255.0f; + float g = ((color >> 8) & 0xff) / 255.0f; + float b = ((color ) & 0xff) / 255.0f; + float a = ((color >> 24) & 0xff) / 255.0f; + ColorSpace source = ColorSpace.get(ColorSpace.Named.SRGB); + return convert(r, g, b, a, source, colorSpace); + } + /** + *

Converts the specified color long from its color space into the specified + * destination color space. The resulting color is returned as a color long. See + * the documentation of this class for a description of the color long format.

+ * + *

When converting several colors in a row, it is recommended to use + * {@link #convert(long, ColorSpace.Connector)} instead to + * avoid the creation of a {@link ColorSpace.Connector} on every invocation.

+ * + * @param color The color long to convert + * @param colorSpace The destination color space + * @return A color long in the destination color space + * @throws IllegalArgumentException If the encoded color space is invalid or unknown + */ + @ColorLong + public static long convert(@ColorLong long color, @NonNull ColorSpace colorSpace) { + float r = red(color); + float g = green(color); + float b = blue(color); + float a = alpha(color); + ColorSpace source = colorSpace(color); + return convert(r, g, b, a, source, colorSpace); + } + /** + *

Converts the specified 3 component color from the source color space to the + * destination color space. The resulting color is returned as a color long. See + * the documentation of this class for a description of the color long format.

+ * + *

When converting multiple colors in a row, it is recommended to use + * {@link #convert(float, float, float, float, ColorSpace.Connector)} instead to + * avoid the creation of a {@link ColorSpace.Connector} on every invocation.

+ * + *

The red, green and blue components must be in the range defined by the + * specified color space. See {@link ColorSpace#getMinValue(int)} and + * {@link ColorSpace#getMaxValue(int)}.

+ * + * @param r The red component of the color to convert + * @param g The green component of the color to convert + * @param b The blue component of the color to convert + * @param a The alpha component of the color to convert, in \([0..1]\) + * @param source The source color space, cannot be null + * @param destination The destination color space, cannot be null + * @return A color long in the destination color space + * + * @see #convert(float, float, float, float, ColorSpace.Connector) + */ + @ColorLong + public static long convert(float r, float g, float b, float a, + @NonNull ColorSpace source, @NonNull ColorSpace destination) { + float[] c = ColorSpace.connect(source, destination).transform(r, g, b); + return pack(c[0], c[1], c[2], a, destination); + } + /** + *

Converts the specified color long from a color space to another using the + * specified color space {@link ColorSpace.Connector connector}. The resulting + * color is returned as a color long. See the documentation of this class for a + * description of the color long format.

+ * + *

When converting several colors in a row, this method is preferable to + * {@link #convert(long, ColorSpace)} as it prevents a new connector from being + * created on every invocation.

+ * + *

The connector's source color space should match the color long's + * color space.

+ * + * @param color The color long to convert + * @param connector A color space connector, cannot be null + * @return A color long in the destination color space of the connector + */ + @ColorLong + public static long convert(@ColorLong long color, @NonNull ColorSpace.Connector connector) { + float r = red(color); + float g = green(color); + float b = blue(color); + float a = alpha(color); + return convert(r, g, b, a, connector); + } + /** + *

Converts the specified 3 component color from a color space to another using + * the specified color space {@link ColorSpace.Connector connector}. The resulting + * color is returned as a color long. See the documentation of this class for a + * description of the color long format.

+ * + *

When converting several colors in a row, this method is preferable to + * {@link #convert(float, float, float, float, ColorSpace, ColorSpace)} as + * it prevents a new connector from being created on every invocation.

+ * + *

The red, green and blue components must be in the range defined by the + * source color space of the connector. See {@link ColorSpace#getMinValue(int)} + * and {@link ColorSpace#getMaxValue(int)}.

+ * + * @param r The red component of the color to convert + * @param g The green component of the color to convert + * @param b The blue component of the color to convert + * @param a The alpha component of the color to convert, in \([0..1]\) + * @param connector A color space connector, cannot be null + * @return A color long in the destination color space of the connector + * + * @see #convert(float, float, float, float, ColorSpace, ColorSpace) + */ + @ColorLong + public static long convert(float r, float g, float b, float a, + @NonNull ColorSpace.Connector connector) { + float[] c = connector.transform(r, g, b); + return pack(c[0], c[1], c[2], a, connector.getDestination()); + } + /** + *

Returns the relative luminance of a color.

+ * + *

Based on the formula for relative luminance defined in WCAG 2.0, + * W3C Recommendation 11 December 2008.

+ * + * @return A value between 0 (darkest black) and 1 (lightest white) + * + * @throws IllegalArgumentException If the specified color's color space + * is unknown or does not use the {@link ColorSpace.Model#RGB RGB} color model + */ + public static float luminance(@ColorLong long color) { + ColorSpace colorSpace = colorSpace(color); + if (colorSpace.getModel() != ColorSpace.Model.RGB) { + throw new IllegalArgumentException("The specified color must be encoded in an RGB " + + "color space. The supplied color space is " + colorSpace.getModel()); + } + DoubleUnaryOperator eotf = ((ColorSpace.Rgb) colorSpace).getEotf(); + double r = eotf.applyAsDouble(red(color)); + double g = eotf.applyAsDouble(green(color)); + double b = eotf.applyAsDouble(blue(color)); + return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b))); + } + private static float saturate(float v) { + return v <= 0.0f ? 0.0f : (v >= 1.0f ? 1.0f : v); + } + /** + * Return the alpha component of a color int. This is the same as saying + * color >>> 24 + */ + @IntRange(from = 0, to = 255) + public static int alpha(int color) { + return color >>> 24; + } + /** + * Return the red component of a color int. This is the same as saying + * (color >> 16) & 0xFF + */ + @IntRange(from = 0, to = 255) + public static int red(int color) { + return (color >> 16) & 0xFF; + } + /** + * Return the green component of a color int. This is the same as saying + * (color >> 8) & 0xFF + */ + @IntRange(from = 0, to = 255) + public static int green(int color) { + return (color >> 8) & 0xFF; + } + /** + * Return the blue component of a color int. This is the same as saying + * color & 0xFF + */ + @IntRange(from = 0, to = 255) + public static int blue(int color) { + return color & 0xFF; + } + /** + * Return a color-int from red, green, blue components. + * The alpha component is implicitly 255 (fully opaque). + * These component values should be \([0..255]\), but there is no + * range check performed, so if they are out of range, the + * returned color is undefined. + * + * @param red Red component \([0..255]\) of the color + * @param green Green component \([0..255]\) of the color + * @param blue Blue component \([0..255]\) of the color + */ + @ColorInt + public static int rgb( + @IntRange(from = 0, to = 255) int red, + @IntRange(from = 0, to = 255) int green, + @IntRange(from = 0, to = 255) int blue) { + return 0xff000000 | (red << 16) | (green << 8) | blue; + } + /** + * Return a color-int from red, green, blue float components + * in the range \([0..1]\). The alpha component is implicitly + * 1.0 (fully opaque). If the components are out of range, the + * returned color is undefined. + * + * @param red Red component \([0..1]\) of the color + * @param green Green component \([0..1]\) of the color + * @param blue Blue component \([0..1]\) of the color + */ + @ColorInt + public static int rgb(float red, float green, float blue) { + return 0xff000000 | + ((int) (red * 255.0f + 0.5f) << 16) | + ((int) (green * 255.0f + 0.5f) << 8) | + (int) (blue * 255.0f + 0.5f); + } + /** + * Return a color-int from alpha, red, green, blue components. + * These component values should be \([0..255]\), but there is no + * range check performed, so if they are out of range, the + * returned color is undefined. + * @param alpha Alpha component \([0..255]\) of the color + * @param red Red component \([0..255]\) of the color + * @param green Green component \([0..255]\) of the color + * @param blue Blue component \([0..255]\) of the color + */ + @ColorInt + public static int argb( + @IntRange(from = 0, to = 255) int alpha, + @IntRange(from = 0, to = 255) int red, + @IntRange(from = 0, to = 255) int green, + @IntRange(from = 0, to = 255) int blue) { + return (alpha << 24) | (red << 16) | (green << 8) | blue; + } + /** + * Return a color-int from alpha, red, green, blue float components + * in the range \([0..1]\). If the components are out of range, the + * returned color is undefined. + * + * @param alpha Alpha component \([0..1]\) of the color + * @param red Red component \([0..1]\) of the color + * @param green Green component \([0..1]\) of the color + * @param blue Blue component \([0..1]\) of the color + */ + @ColorInt + public static int argb(float alpha, float red, float green, float blue) { + return ((int) (alpha * 255.0f + 0.5f) << 24) | + ((int) (red * 255.0f + 0.5f) << 16) | + ((int) (green * 255.0f + 0.5f) << 8) | + (int) (blue * 255.0f + 0.5f); + } + /** + * Returns the relative luminance of a color. + *

+ * Assumes sRGB encoding. Based on the formula for relative luminance + * defined in WCAG 2.0, W3C Recommendation 11 December 2008. + * + * @return a value between 0 (darkest black) and 1 (lightest white) + */ + public static float luminance(@ColorInt int color) { + ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB); + DoubleUnaryOperator eotf = cs.getEotf(); + double r = eotf.applyAsDouble(red(color) / 255.0); + double g = eotf.applyAsDouble(green(color) / 255.0); + double b = eotf.applyAsDouble(blue(color) / 255.0); + return (float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)); + } + /** + *

Parse the color string, and return the corresponding color-int. + * If the string cannot be parsed, throws an IllegalArgumentException + * exception. Supported formats are:

+ * + *
    + *
  • #RRGGBB
  • + *
  • #AARRGGBB
  • + *
+ * + *

The following names are also accepted: red, blue, + * green, black, white, gray, + * cyan, magenta, yellow, lightgray, + * darkgray, grey, lightgrey, darkgrey, + * aqua, fuchsia, lime, maroon, + * navy, olive, purple, silver, + * and teal.

+ */ + @ColorInt + public static int parseColor(@Size(min=1) String colorString) { + if (colorString.charAt(0) == '#') { + // Use a long to avoid rollovers on #ffXXXXXX + long color = Long.parseLong(colorString.substring(1), 16); + if (colorString.length() == 7) { + // Set the alpha value + color |= 0x00000000ff000000; + } else if (colorString.length() != 9) { + throw new IllegalArgumentException("Unknown color"); + } + return (int)color; + } else { + Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT)); + if (color != null) { + return color; + } + } + throw new IllegalArgumentException("Unknown color"); + } + /** + * Convert RGB components to HSV. + *
    + *
  • hsv[0] is Hue \([0..360[\)
  • + *
  • hsv[1] is Saturation \([0...1]\)
  • + *
  • hsv[2] is Value \([0...1]\)
  • + *
+ * @param red red component value \([0..255]\) + * @param green green component value \([0..255]\) + * @param blue blue component value \([0..255]\) + * @param hsv 3 element array which holds the resulting HSV components. + */ + public static void RGBToHSV( + @IntRange(from = 0, to = 255) int red, + @IntRange(from = 0, to = 255) int green, + @IntRange(from = 0, to = 255) int blue, @Size(3) float hsv[]) { + if (hsv.length < 3) { + throw new RuntimeException("3 components required for hsv"); + } + nativeRGBToHSV(red, green, blue, hsv); + } + /** + * Convert the ARGB color to its HSV components. + *
    + *
  • hsv[0] is Hue \([0..360[\)
  • + *
  • hsv[1] is Saturation \([0...1]\)
  • + *
  • hsv[2] is Value \([0...1]\)
  • + *
+ * @param color the argb color to convert. The alpha component is ignored. + * @param hsv 3 element array which holds the resulting HSV components. + */ + public static void colorToHSV(@ColorInt int color, @Size(3) float hsv[]) { + RGBToHSV((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, hsv); + } + /** + * Convert HSV components to an ARGB color. Alpha set to 0xFF. + *
    + *
  • hsv[0] is Hue \([0..360[\)
  • + *
  • hsv[1] is Saturation \([0...1]\)
  • + *
  • hsv[2] is Value \([0...1]\)
  • + *
+ * If hsv values are out of range, they are pinned. + * @param hsv 3 element array which holds the input HSV components. + * @return the resulting argb color + */ + @ColorInt + public static int HSVToColor(@Size(3) float hsv[]) { + return HSVToColor(0xFF, hsv); + } + /** + * Convert HSV components to an ARGB color. The alpha component is passed + * through unchanged. + *
    + *
  • hsv[0] is Hue \([0..360[\)
  • + *
  • hsv[1] is Saturation \([0...1]\)
  • + *
  • hsv[2] is Value \([0...1]\)
  • + *
+ * If hsv values are out of range, they are pinned. + * @param alpha the alpha component of the returned argb color. + * @param hsv 3 element array which holds the input HSV components. + * @return the resulting argb color + */ + @ColorInt + public static int HSVToColor(@IntRange(from = 0, to = 255) int alpha, @Size(3) float hsv[]) { + if (hsv.length < 3) { + throw new RuntimeException("3 components required for hsv"); + } + return nativeHSVToColor(alpha, hsv); + } + private static native void nativeRGBToHSV(int red, int greed, int blue, float hsv[]); + private static native int nativeHSVToColor(int alpha, float hsv[]); + private static final HashMap sColorNameMap; + static { + sColorNameMap = new HashMap<>(); + sColorNameMap.put("black", BLACK); + sColorNameMap.put("darkgray", DKGRAY); + sColorNameMap.put("gray", GRAY); + sColorNameMap.put("lightgray", LTGRAY); + sColorNameMap.put("white", WHITE); + sColorNameMap.put("red", RED); + sColorNameMap.put("green", GREEN); + sColorNameMap.put("blue", BLUE); + sColorNameMap.put("yellow", YELLOW); + sColorNameMap.put("cyan", CYAN); + sColorNameMap.put("magenta", MAGENTA); + sColorNameMap.put("aqua", 0xFF00FFFF); + sColorNameMap.put("fuchsia", 0xFFFF00FF); + sColorNameMap.put("darkgrey", DKGRAY); + sColorNameMap.put("grey", GRAY); + sColorNameMap.put("lightgrey", LTGRAY); + sColorNameMap.put("lime", 0xFF00FF00); + sColorNameMap.put("maroon", 0xFF800000); + sColorNameMap.put("navy", 0xFF000080); + sColorNameMap.put("olive", 0xFF808000); + sColorNameMap.put("purple", 0xFF800080); + sColorNameMap.put("silver", 0xFFC0C0C0); + sColorNameMap.put("teal", 0xFF008080); + } +} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/ColorSpace.java b/Vision/src/main/java/android/graphics/ColorSpace.java new file mode 100644 index 00000000..15ead4d0 --- /dev/null +++ b/Vision/src/main/java/android/graphics/ColorSpace.java @@ -0,0 +1,3642 @@ +/* + * Copyright (C) 2016 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 android.graphics; + +import android.annotation.*; +import android.hardware.DataSpace; +import android.hardware.DataSpace.NamedDataSpace; +import android.util.SparseIntArray; + +import java.util.Arrays; +import java.util.function.DoubleUnaryOperator; +/** + * {@usesMathJax} + * + *

A {@link ColorSpace} is used to identify a specific organization of colors. + * Each color space is characterized by a {@link Model color model} that defines + * how a color value is represented (for instance the {@link Model#RGB RGB} color + * model defines a color value as a triplet of numbers).

+ * + *

Each component of a color must fall within a valid range, specific to each + * color space, defined by {@link #getMinValue(int)} and {@link #getMaxValue(int)} + * This range is commonly \([0..1]\). While it is recommended to use values in the + * valid range, a color space always clamps input and output values when performing + * operations such as converting to a different color space.

+ * + *

Using color spaces

+ * + *

This implementation provides a pre-defined set of common color spaces + * described in the {@link Named} enum. To obtain an instance of one of the + * pre-defined color spaces, simply invoke {@link #get(Named)}:

+ * + *
+ * ColorSpace sRgb = ColorSpace.get(ColorSpace.Named.SRGB);
+ * 
+ * + *

The {@link #get(Named)} method always returns the same instance for a given + * name. Color spaces with an {@link Model#RGB RGB} color model can be safely + * cast to {@link Rgb}. Doing so gives you access to more APIs to query various + * properties of RGB color models: color gamut primaries, transfer functions, + * conversions to and from linear space, etc. Please refer to {@link Rgb} for + * more information.

+ * + *

The documentation of {@link Named} provides a detailed description of the + * various characteristics of each available color space.

+ * + *

Color space conversions

+ *

To allow conversion between color spaces, this implementation uses the CIE + * XYZ profile connection space (PCS). Color values can be converted to and from + * this PCS using {@link #toXyz(float[])} and {@link #fromXyz(float[])}.

+ * + *

For color space with a non-RGB color model, the white point of the PCS + * must be the CIE standard illuminant D50. RGB color spaces use their + * native white point (D65 for {@link Named#SRGB sRGB} for instance and must + * undergo {@link Adaptation chromatic adaptation} as necessary.

+ * + *

Since the white point of the PCS is not defined for RGB color space, it is + * highly recommended to use the variants of the {@link #connect(ColorSpace, ColorSpace)} + * method to perform conversions between color spaces. A color space can be + * manually adapted to a specific white point using {@link #adapt(ColorSpace, float[])}. + * Please refer to the documentation of {@link Rgb RGB color spaces} for more + * information. Several common CIE standard illuminants are provided in this + * class as reference (see {@link #ILLUMINANT_D65} or {@link #ILLUMINANT_D50} + * for instance).

+ * + *

Here is an example of how to convert from a color space to another:

+ * + *
+ * // Convert from DCI-P3 to Rec.2020
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ *         ColorSpace.get(ColorSpace.Named.DCI_P3),
+ *         ColorSpace.get(ColorSpace.Named.BT2020));
+ *
+ * float[] bt2020 = connector.transform(p3r, p3g, p3b);
+ * 
+ * + *

You can easily convert to {@link Named#SRGB sRGB} by omitting the second + * parameter:

+ * + *
+ * // Convert from DCI-P3 to sRGB
+ * ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3));
+ *
+ * float[] sRGB = connector.transform(p3r, p3g, p3b);
+ * 
+ * + *

Conversions also work between color spaces with different color models:

+ * + *
+ * // Convert from CIE L*a*b* (color model Lab) to Rec.709 (color model RGB)
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ *         ColorSpace.get(ColorSpace.Named.CIE_LAB),
+ *         ColorSpace.get(ColorSpace.Named.BT709));
+ * 
+ * + *

Color spaces and multi-threading

+ * + *

Color spaces and other related classes ({@link Connector} for instance) + * are immutable and stateless. They can be safely used from multiple concurrent + * threads.

+ * + *

Public static methods provided by this class, such as {@link #get(Named)} + * and {@link #connect(ColorSpace, ColorSpace)}, are also guaranteed to be + * thread-safe.

+ * + * @see #get(Named) + * @see Named + * @see Model + * @see Connector + * @see Adaptation + */ +@AnyThread +@SuppressWarnings("StaticInitializerReferencesSubClass") +@SuppressAutoDoc +public abstract class ColorSpace { + /** + * Standard CIE 1931 2° illuminant A, encoded in xyY. + * This illuminant has a color temperature of 2856K. + */ + public static final float[] ILLUMINANT_A = { 0.44757f, 0.40745f }; + /** + * Standard CIE 1931 2° illuminant B, encoded in xyY. + * This illuminant has a color temperature of 4874K. + */ + public static final float[] ILLUMINANT_B = { 0.34842f, 0.35161f }; + /** + * Standard CIE 1931 2° illuminant C, encoded in xyY. + * This illuminant has a color temperature of 6774K. + */ + public static final float[] ILLUMINANT_C = { 0.31006f, 0.31616f }; + /** + * Standard CIE 1931 2° illuminant D50, encoded in xyY. + * This illuminant has a color temperature of 5003K. This illuminant + * is used by the profile connection space in ICC profiles. + */ + public static final float[] ILLUMINANT_D50 = { 0.34567f, 0.35850f }; + /** + * Standard CIE 1931 2° illuminant D55, encoded in xyY. + * This illuminant has a color temperature of 5503K. + */ + public static final float[] ILLUMINANT_D55 = { 0.33242f, 0.34743f }; + /** + * Standard CIE 1931 2° illuminant D60, encoded in xyY. + * This illuminant has a color temperature of 6004K. + */ + public static final float[] ILLUMINANT_D60 = { 0.32168f, 0.33767f }; + /** + * Standard CIE 1931 2° illuminant D65, encoded in xyY. + * This illuminant has a color temperature of 6504K. This illuminant + * is commonly used in RGB color spaces such as sRGB, BT.709, etc. + */ + public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f }; + /** + * Standard CIE 1931 2° illuminant D75, encoded in xyY. + * This illuminant has a color temperature of 7504K. + */ + public static final float[] ILLUMINANT_D75 = { 0.29902f, 0.31485f }; + /** + * Standard CIE 1931 2° illuminant E, encoded in xyY. + * This illuminant has a color temperature of 5454K. + */ + public static final float[] ILLUMINANT_E = { 0.33333f, 0.33333f }; + /** + * The minimum ID value a color space can have. + * + * @see #getId() + */ + public static final int MIN_ID = -1; // Do not change + /** + * The maximum ID value a color space can have. + * + * @see #getId() + */ + public static final int MAX_ID = 63; // Do not change, used to encode in longs + private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }; + private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f }; + /** + * A gray color space does not have meaningful primaries, so we use this arbitrary set. + */ + private static final float[] GRAY_PRIMARIES = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f }; + private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS = + new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4); + // See static initialization block next to #get(Named) + private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length]; + private static final SparseIntArray sDataToColorSpaces = new SparseIntArray(); + @NonNull private final String mName; + @NonNull private final Model mModel; + @IntRange(from = MIN_ID, to = MAX_ID) private final int mId; + /** + * {@usesMathJax} + * + *

List of common, named color spaces. A corresponding instance of + * {@link ColorSpace} can be obtained by calling {@link ColorSpace#get(Named)}:

+ * + *
+     * ColorSpace cs = ColorSpace.get(ColorSpace.Named.DCI_P3);
+     * 
+ * + *

The properties of each color space are described below (see {@link #SRGB sRGB} + * for instance). When applicable, the color gamut of each color space is compared + * to the color gamut of sRGB using a CIE 1931 xy chromaticity diagram. This diagram + * shows the location of the color space's primaries and white point.

+ * + * @see ColorSpace#get(Named) + */ + public enum Named { + // NOTE: Do NOT change the order of the enum + /** + *

{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamesRGB IEC61966-2.1
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0031308 \\\ + * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0031308 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \lt 0.04045 \\\ + * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

sRGB
+ *

+ */ + SRGB, + /** + *

{@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamesRGB IEC61966-2.1 (Linear)
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(C_{sRGB} = C_{linear}\)
Electro-optical transfer function (EOTF)\(C_{linear} = C_{sRGB}\)
Range\([0..1]\)
+ *

+ * + *

sRGB
+ *

+ */ + LINEAR_SRGB, + /** + *

{@link ColorSpace.Rgb RGB} color space scRGB-nl standardized as IEC 61966-2-2:2003.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamescRGB-nl IEC 61966-2-2:2003
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{scRGB} = \begin{cases} sign(C_{linear}) 12.92 \times \left| C_{linear} \right| & + * \left| C_{linear} \right| \lt 0.0031308 \\\ + * sign(C_{linear}) 1.055 \times \left| C_{linear} \right| ^{\frac{1}{2.4}} - 0.055 & + * \left| C_{linear} \right| \ge 0.0031308 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}sign(C_{scRGB}) \frac{\left| C_{scRGB} \right|}{12.92} & + * \left| C_{scRGB} \right| \lt 0.04045 \\\ + * sign(C_{scRGB}) \left( \frac{\left| C_{scRGB} \right| + 0.055}{1.055} \right) ^{2.4} & + * \left| C_{scRGB} \right| \ge 0.04045 \end{cases} + * \end{equation}\) + *
Range\([-0.799..2.399[\)
+ *

+ * + *

Extended sRGB (orange) vs sRGB (white)
+ *

+ */ + EXTENDED_SRGB, + /** + *

{@link ColorSpace.Rgb RGB} color space scRGB standardized as IEC 61966-2-2:2003.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamescRGB IEC 61966-2-2:2003
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(C_{scRGB} = C_{linear}\)
Electro-optical transfer function (EOTF)\(C_{linear} = C_{scRGB}\)
Range\([-0.5..7.499[\)
+ *

+ * + *

Extended sRGB (orange) vs sRGB (white)
+ *

+ */ + LINEAR_EXTENDED_SRGB, + /** + *

{@link ColorSpace.Rgb RGB} color space BT.709 standardized as Rec. ITU-R BT.709-5.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NameRec. ITU-R BT.709-5
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\\ + * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\\ + * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

BT.709
+ *

+ */ + BT709, + /** + *

{@link ColorSpace.Rgb RGB} color space BT.2020 standardized as Rec. ITU-R BT.2020-1.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.7080.1700.1310.3127
y0.2920.7970.0460.3290
PropertyValue
NameRec. ITU-R BT.2020-1
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{BT2020} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.0181 \\\ + * 1.0993 \times C_{linear}^{\frac{1}{2.2}} - 0.0993 & C_{linear} \ge 0.0181 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT2020}}{4.5} & C_{BT2020} \lt 0.08145 \\\ + * \left( \frac{C_{BT2020} + 0.0993}{1.0993} \right) ^{2.2} & C_{BT2020} \ge 0.08145 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

BT.2020 (orange) vs sRGB (white)
+ *

+ */ + BT2020, + /** + *

{@link ColorSpace.Rgb RGB} color space DCI-P3 standardized as SMPTE RP 431-2-2007.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6800.2650.1500.314
y0.3200.6900.0600.351
PropertyValue
NameSMPTE RP 431-2-2007 DCI (P3)
CIE standard illuminantN/A
Opto-electronic transfer function (OETF)\(C_{P3} = C_{linear}^{\frac{1}{2.6}}\)
Electro-optical transfer function (EOTF)\(C_{linear} = C_{P3}^{2.6}\)
Range\([0..1]\)
+ *

+ * + *

DCI-P3 (orange) vs sRGB (white)
+ *

+ */ + DCI_P3, + /** + *

{@link ColorSpace.Rgb RGB} color space Display P3 based on SMPTE RP 431-2-2007 and IEC 61966-2.1:1999.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6800.2650.1500.3127
y0.3200.6900.0600.3290
PropertyValue
NameDisplay P3
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{DisplayP3} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0030186 \\\ + * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0030186 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{DisplayP3}}{12.92} & C_{sRGB} \lt 0.04045 \\\ + * \left( \frac{C_{DisplayP3} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

Display P3 (orange) vs sRGB (white)
+ *

+ */ + DISPLAY_P3, + /** + *

{@link ColorSpace.Rgb RGB} color space NTSC, 1953 standard.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.670.210.140.310
y0.330.710.080.316
PropertyValue
NameNTSC (1953)
CIE standard illuminantC
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\\ + * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\\ + * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

NTSC 1953 (orange) vs sRGB (white)
+ *

+ */ + NTSC_1953, + /** + *

{@link ColorSpace.Rgb RGB} color space SMPTE C.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6300.3100.1550.3127
y0.3400.5950.0700.3290
PropertyValue
NameSMPTE-C RGB
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\\ + * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\\ + * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

SMPTE-C (orange) vs sRGB (white)
+ *

+ */ + SMPTE_C, + /** + *

{@link ColorSpace.Rgb RGB} color space Adobe RGB (1998).

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.640.210.150.3127
y0.330.710.060.3290
PropertyValue
NameAdobe RGB (1998)
CIE standard illuminantD65
Opto-electronic transfer function (OETF)\(C_{RGB} = C_{linear}^{\frac{1}{2.2}}\)
Electro-optical transfer function (EOTF)\(C_{linear} = C_{RGB}^{2.2}\)
Range\([0..1]\)
+ *

+ * + *

Adobe RGB (orange) vs sRGB (white)
+ *

+ */ + ADOBE_RGB, + /** + *

{@link ColorSpace.Rgb RGB} color space ProPhoto RGB standardized as ROMM RGB ISO 22028-2:2013.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.73470.15960.03660.3457
y0.26530.84040.00010.3585
PropertyValue
NameROMM RGB ISO 22028-2:2013
CIE standard illuminantD50
Opto-electronic transfer function (OETF)\(\begin{equation} + * C_{ROMM} = \begin{cases} 16 \times C_{linear} & C_{linear} \lt 0.001953 \\\ + * C_{linear}^{\frac{1}{1.8}} & C_{linear} \ge 0.001953 \end{cases} + * \end{equation}\) + *
Electro-optical transfer function (EOTF)\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{ROMM}}{16} & C_{ROMM} \lt 0.031248 \\\ + * C_{ROMM}^{1.8} & C_{ROMM} \ge 0.031248 \end{cases} + * \end{equation}\) + *
Range\([0..1]\)
+ *

+ * + *

ProPhoto RGB (orange) vs sRGB (white)
+ *

+ */ + PRO_PHOTO_RGB, + /** + *

{@link ColorSpace.Rgb RGB} color space ACES standardized as SMPTE ST 2065-1:2012.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.734700.000000.000100.32168
y0.265301.00000-0.077000.33767
PropertyValue
NameSMPTE ST 2065-1:2012 ACES
CIE standard illuminantD60
Opto-electronic transfer function (OETF)\(C_{ACES} = C_{linear}\)
Electro-optical transfer function (EOTF)\(C_{linear} = C_{ACES}\)
Range\([-65504.0, 65504.0]\)
+ *

+ * + *

ACES (orange) vs sRGB (white)
+ *

+ */ + ACES, + /** + *

{@link ColorSpace.Rgb RGB} color space ACEScg standardized as Academy S-2014-004.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.7130.1650.1280.32168
y0.2930.8300.0440.33767
PropertyValue
NameAcademy S-2014-004 ACEScg
CIE standard illuminantD60
Opto-electronic transfer function (OETF)\(C_{ACEScg} = C_{linear}\)
Electro-optical transfer function (EOTF)\(C_{linear} = C_{ACEScg}\)
Range\([-65504.0, 65504.0]\)
+ *

+ * + *

ACEScg (orange) vs sRGB (white)
+ *

+ */ + ACESCG, + /** + *

{@link Model#XYZ XYZ} color space CIE XYZ. This color space assumes standard + * illuminant D50 as its white point.

+ * + * + * + * + * + *
PropertyValue
NameGeneric XYZ
CIE standard illuminantD50
Range\([-2.0, 2.0]\)
+ */ + CIE_XYZ, + /** + *

{@link Model#LAB Lab} color space CIE L*a*b*. This color space uses CIE XYZ D50 + * as a profile conversion space.

+ * + * + * + * + * + *
PropertyValue
NameGeneric L*a*b*
CIE standard illuminantD50
Range\(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\)
+ */ + CIE_LAB + // Update the initialization block next to #get(Named) when adding new values + } + /** + *

A render intent determines how a {@link ColorSpace.Connector connector} + * maps colors from one color space to another. The choice of mapping is + * important when the source color space has a larger color gamut than the + * destination color space.

+ * + * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent) + */ + public enum RenderIntent { + /** + *

Compresses the source gamut into the destination gamut. + * This render intent affects all colors, inside and outside + * of destination gamut. The goal of this render intent is + * to preserve the visual relationship between colors.

+ * + *

This render intent is currently not + * implemented and behaves like {@link #RELATIVE}.

+ */ + PERCEPTUAL, + /** + * Similar to the {@link #ABSOLUTE} render intent, this render + * intent matches the closest color in the destination gamut + * but makes adjustments for the destination white point. + */ + RELATIVE, + /** + *

Attempts to maintain the relative saturation of colors + * from the source gamut to the destination gamut, to keep + * highly saturated colors as saturated as possible.

+ * + *

This render intent is currently not + * implemented and behaves like {@link #RELATIVE}.

+ */ + SATURATION, + /** + * Colors that are in the destination gamut are left unchanged. + * Colors that fall outside of the destination gamut are mapped + * to the closest possible color within the gamut of the destination + * color space (they are clipped). + */ + ABSOLUTE + } + /** + * {@usesMathJax} + * + *

List of adaptation matrices that can be used for chromatic adaptation + * using the von Kries transform. These matrices are used to convert values + * in the CIE XYZ space to values in the LMS space (Long Medium Short).

+ * + *

Given an adaptation matrix \(A\), the conversion from XYZ to + * LMS is straightforward:

+ * + * $$\left[ \begin{array}{c} L\\ M\\ S \end{array} \right] = + * A \left[ \begin{array}{c} X\\ Y\\ Z \end{array} \right]$$ + * + *

The complete von Kries transform \(T\) uses a diagonal matrix + * noted \(D\) to perform the adaptation in LMS space. In addition + * to \(A\) and \(D\), the source white point \(W1\) and the destination + * white point \(W2\) must be specified:

+ * + * $$\begin{align*} + * \left[ \begin{array}{c} L_1\\ M_1\\ S_1 \end{array} \right] &= + * A \left[ \begin{array}{c} W1_X\\ W1_Y\\ W1_Z \end{array} \right] \\\ + * \left[ \begin{array}{c} L_2\\ M_2\\ S_2 \end{array} \right] &= + * A \left[ \begin{array}{c} W2_X\\ W2_Y\\ W2_Z \end{array} \right] \\\ + * D &= \left[ \begin{matrix} \frac{L_2}{L_1} & 0 & 0 \\\ + * 0 & \frac{M_2}{M_1} & 0 \\\ + * 0 & 0 & \frac{S_2}{S_1} \end{matrix} \right] \\\ + * T &= A^{-1}.D.A + * \end{align*}$$ + * + *

As an example, the resulting matrix \(T\) can then be used to + * perform the chromatic adaptation of sRGB XYZ transform from D65 + * to D50:

+ * + * $$sRGB_{D50} = T.sRGB_{D65}$$ + * + * @see ColorSpace.Connector + * @see ColorSpace#connect(ColorSpace, ColorSpace) + */ + public enum Adaptation { + /** + * Bradford chromatic adaptation transform, as defined in the + * CIECAM97s color appearance model. + */ + BRADFORD(new float[] { + 0.8951f, -0.7502f, 0.0389f, + 0.2664f, 1.7135f, -0.0685f, + -0.1614f, 0.0367f, 1.0296f + }), + /** + * von Kries chromatic adaptation transform. + */ + VON_KRIES(new float[] { + 0.40024f, -0.22630f, 0.00000f, + 0.70760f, 1.16532f, 0.00000f, + -0.08081f, 0.04570f, 0.91822f + }), + /** + * CIECAT02 chromatic adaption transform, as defined in the + * CIECAM02 color appearance model. + */ + CIECAT02(new float[] { + 0.7328f, -0.7036f, 0.0030f, + 0.4296f, 1.6975f, 0.0136f, + -0.1624f, 0.0061f, 0.9834f + }); + final float[] mTransform; + Adaptation(@NonNull @Size(9) float[] transform) { + mTransform = transform; + } + } + /** + * A color model is required by a {@link ColorSpace} to describe the + * way colors can be represented as tuples of numbers. A common color + * model is the {@link #RGB RGB} color model which defines a color + * as represented by a tuple of 3 numbers (red, green and blue). + */ + public enum Model { + /** + * The RGB model is a color model with 3 components that + * refer to the three additive primaries: red, green + * and blue. + */ + RGB(3), + /** + * The XYZ model is a color model with 3 components that + * are used to model human color vision on a basic sensory + * level. + */ + XYZ(3), + /** + * The Lab model is a color model with 3 components used + * to describe a color space that is more perceptually + * uniform than XYZ. + */ + LAB(3), + /** + * The CMYK model is a color model with 4 components that + * refer to four inks used in color printing: cyan, magenta, + * yellow and black (or key). CMYK is a subtractive color + * model. + */ + CMYK(4); + private final int mComponentCount; + Model(@IntRange(from = 1, to = 4) int componentCount) { + mComponentCount = componentCount; + } + /** + * Returns the number of components for this color model. + * + * @return An integer between 1 and 4 + */ + @IntRange(from = 1, to = 4) + public int getComponentCount() { + return mComponentCount; + } + } + /*package*/ ColorSpace( + @NonNull String name, + @NonNull Model model, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + if (name == null || name.length() < 1) { + throw new IllegalArgumentException("The name of a color space cannot be null and " + + "must contain at least 1 character"); + } + if (model == null) { + throw new IllegalArgumentException("A color space must have a model"); + } + if (id < MIN_ID || id > MAX_ID) { + throw new IllegalArgumentException("The id must be between " + + MIN_ID + " and " + MAX_ID); + } + mName = name; + mModel = model; + mId = id; + } + /** + *

Returns the name of this color space. The name is never null + * and contains always at least 1 character.

+ * + *

Color space names are recommended to be unique but are not + * guaranteed to be. There is no defined format but the name usually + * falls in one of the following categories:

+ *
    + *
  • Generic names used to identify color spaces in non-RGB + * color models. For instance: {@link Named#CIE_LAB Generic L*a*b*}.
  • + *
  • Names tied to a particular specification. For instance: + * {@link Named#SRGB sRGB IEC61966-2.1} or + * {@link Named#ACES SMPTE ST 2065-1:2012 ACES}.
  • + *
  • Ad-hoc names, often generated procedurally or by the user + * during a calibration workflow. These names often contain the + * make and model of the display.
  • + *
+ * + *

Because the format of color space names is not defined, it is + * not recommended to programmatically identify a color space by its + * name alone. Names can be used as a first approximation.

+ * + *

It is however perfectly acceptable to display color space names to + * users in a UI, or in debuggers and logs. When displaying a color space + * name to the user, it is recommended to add extra information to avoid + * ambiguities: color model, a representation of the color space's gamut, + * white point, etc.

+ * + * @return A non-null String of length >= 1 + */ + @NonNull + public String getName() { + return mName; + } + /** + * Returns the ID of this color space. Positive IDs match the color + * spaces enumerated in {@link Named}. A negative ID indicates a + * color space created by calling one of the public constructors. + * + * @return An integer between {@link #MIN_ID} and {@link #MAX_ID} + */ + @IntRange(from = MIN_ID, to = MAX_ID) + public int getId() { + return mId; + } + /** + * Return the color model of this color space. + * + * @return A non-null {@link Model} + * + * @see Model + * @see #getComponentCount() + */ + @NonNull + public Model getModel() { + return mModel; + } + /** + * Returns the number of components that form a color value according + * to this color space's color model. + * + * @return An integer between 1 and 4 + * + * @see Model + * @see #getModel() + */ + @IntRange(from = 1, to = 4) + public int getComponentCount() { + return mModel.getComponentCount(); + } + /** + * Returns whether this color space is a wide-gamut color space. + * An RGB color space is wide-gamut if its gamut entirely contains + * the {@link Named#SRGB sRGB} gamut and if the area of its gamut is + * 90% of greater than the area of the {@link Named#NTSC_1953 NTSC} + * gamut. + * + * @return True if this color space is a wide-gamut color space, + * false otherwise + */ + public abstract boolean isWideGamut(); + /** + *

Indicates whether this color space is the sRGB color space or + * equivalent to the sRGB color space.

+ *

A color space is considered sRGB if it meets all the following + * conditions:

+ *
    + *
  • Its color model is {@link Model#RGB}.
  • + *
  • + * Its primaries are within 1e-3 of the true + * {@link Named#SRGB sRGB} primaries. + *
  • + *
  • + * Its white point is within 1e-3 of the CIE standard + * illuminant {@link #ILLUMINANT_D65 D65}. + *
  • + *
  • Its opto-electronic transfer function is not linear.
  • + *
  • Its electro-optical transfer function is not linear.
  • + *
  • Its transfer functions yield values within 1e-3 of {@link Named#SRGB}.
  • + *
  • Its range is \([0..1]\).
  • + *
+ *

This method always returns true for {@link Named#SRGB}.

+ * + * @return True if this color space is the sRGB color space (or a + * close approximation), false otherwise + */ + public boolean isSrgb() { + return false; + } + /** + * Returns the minimum valid value for the specified component of this + * color space's color model. + * + * @param component The index of the component + * @return A floating point value less than {@link #getMaxValue(int)} + * + * @see #getMaxValue(int) + * @see Model#getComponentCount() + */ + public abstract float getMinValue(@IntRange(from = 0, to = 3) int component); + /** + * Returns the maximum valid value for the specified component of this + * color space's color model. + * + * @param component The index of the component + * @return A floating point value greater than {@link #getMinValue(int)} + * + * @see #getMinValue(int) + * @see Model#getComponentCount() + */ + public abstract float getMaxValue(@IntRange(from = 0, to = 3) int component); + /** + *

Converts a color value from this color space's model to + * tristimulus CIE XYZ values. If the color model of this color + * space is not {@link Model#RGB RGB}, it is assumed that the + * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50} + * standard illuminant.

+ * + *

This method is a convenience for color spaces with a model + * of 3 components ({@link Model#RGB RGB} or {@link Model#LAB} + * for instance). With color spaces using fewer or more components, + * use {@link #toXyz(float[])} instead

. + * + * @param r The first component of the value to convert from (typically R in RGB) + * @param g The second component of the value to convert from (typically G in RGB) + * @param b The third component of the value to convert from (typically B in RGB) + * @return A new array of 3 floats, containing tristimulus XYZ values + * + * @see #toXyz(float[]) + * @see #fromXyz(float, float, float) + */ + @NonNull + @Size(3) + public float[] toXyz(float r, float g, float b) { + return toXyz(new float[] { r, g, b }); + } + /** + *

Converts a color value from this color space's model to + * tristimulus CIE XYZ values. If the color model of this color + * space is not {@link Model#RGB RGB}, it is assumed that the + * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50} + * standard illuminant.

+ * + *

The specified array's length must be at least + * equal to to the number of color components as returned by + * {@link Model#getComponentCount()}.

+ * + * @param v An array of color components containing the color space's + * color value to convert to XYZ, and large enough to hold + * the resulting tristimulus XYZ values + * @return The array passed in parameter + * + * @see #toXyz(float, float, float) + * @see #fromXyz(float[]) + */ + @NonNull + @Size(min = 3) + public abstract float[] toXyz(@NonNull @Size(min = 3) float[] v); + /** + *

Converts tristimulus values from the CIE XYZ space to this + * color space's color model.

+ * + * @param x The X component of the color value + * @param y The Y component of the color value + * @param z The Z component of the color value + * @return A new array whose size is equal to the number of color + * components as returned by {@link Model#getComponentCount()} + * + * @see #fromXyz(float[]) + * @see #toXyz(float, float, float) + */ + @NonNull + @Size(min = 3) + public float[] fromXyz(float x, float y, float z) { + float[] xyz = new float[mModel.getComponentCount()]; + xyz[0] = x; + xyz[1] = y; + xyz[2] = z; + return fromXyz(xyz); + } + /** + *

Converts tristimulus values from the CIE XYZ space to this color + * space's color model. The resulting value is passed back in the specified + * array.

+ * + *

The specified array's length must be at least equal to + * to the number of color components as returned by + * {@link Model#getComponentCount()}, and its first 3 values must + * be the XYZ components to convert from.

+ * + * @param v An array of color components containing the XYZ values + * to convert from, and large enough to hold the number + * of components of this color space's model + * @return The array passed in parameter + * + * @see #fromXyz(float, float, float) + * @see #toXyz(float[]) + */ + @NonNull + @Size(min = 3) + public abstract float[] fromXyz(@NonNull @Size(min = 3) float[] v); + /** + *

Returns a string representation of the object. This method returns + * a string equal to the value of:

+ * + *
+     * getName() + "(id=" + getId() + ", model=" + getModel() + ")"
+     * 
+ * + *

For instance, the string representation of the {@link Named#SRGB sRGB} + * color space is equal to the following value:

+ * + *
+     * sRGB IEC61966-2.1 (id=0, model=RGB)
+     * 
+ * + * @return A string representation of the object + */ + @Override + @NonNull + public String toString() { + return mName + " (id=" + mId + ", model=" + mModel + ")"; + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ColorSpace that = (ColorSpace) o; + if (mId != that.mId) return false; + //noinspection SimplifiableIfStatement + if (!mName.equals(that.mName)) return false; + return mModel == that.mModel; + } + @Override + public int hashCode() { + int result = mName.hashCode(); + result = 31 * result + mModel.hashCode(); + result = 31 * result + mId; + return result; + } + /** + *

Connects two color spaces to allow conversion from the source color + * space to the destination color space. If the source and destination + * color spaces do not have the same profile connection space (CIE XYZ + * with the same white point), they are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.

+ * + *

If the source and destination are the same, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.

+ * + *

Colors are mapped from the source color space to the destination color + * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.

+ * + * @param source The color space to convert colors from + * @param destination The color space to convert colors to + * @return A non-null connector between the two specified color spaces + * + * @see #connect(ColorSpace) + * @see #connect(ColorSpace, RenderIntent) + * @see #connect(ColorSpace, ColorSpace, RenderIntent) + */ + @NonNull + public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination) { + return connect(source, destination, RenderIntent.PERCEPTUAL); + } + /** + *

Connects two color spaces to allow conversion from the source color + * space to the destination color space. If the source and destination + * color spaces do not have the same profile connection space (CIE XYZ + * with the same white point), they are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.

+ * + *

If the source and destination are the same, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.

+ * + * @param source The color space to convert colors from + * @param destination The color space to convert colors to + * @param intent The render intent to map colors from the source to the destination + * @return A non-null connector between the two specified color spaces + * + * @see #connect(ColorSpace) + * @see #connect(ColorSpace, RenderIntent) + * @see #connect(ColorSpace, ColorSpace) + */ + @NonNull + @SuppressWarnings("ConstantConditions") + public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination, + @NonNull RenderIntent intent) { + if (source.equals(destination)) return Connector.identity(source); + if (source.getModel() == Model.RGB && destination.getModel() == Model.RGB) { + return new Connector.Rgb((Rgb) source, (Rgb) destination, intent); + } + return new Connector(source, destination, intent); + } + /** + *

Connects the specified color spaces to sRGB. + * If the source color space does not use CIE XYZ D65 as its profile + * connection space, the two spaces are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.

+ * + *

If the source is the sRGB color space, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.

+ * + *

Colors are mapped from the source color space to the destination color + * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.

+ * + * @param source The color space to convert colors from + * @return A non-null connector between the specified color space and sRGB + * + * @see #connect(ColorSpace, RenderIntent) + * @see #connect(ColorSpace, ColorSpace) + * @see #connect(ColorSpace, ColorSpace, RenderIntent) + */ + @NonNull + public static Connector connect(@NonNull ColorSpace source) { + return connect(source, RenderIntent.PERCEPTUAL); + } + /** + *

Connects the specified color spaces to sRGB. + * If the source color space does not use CIE XYZ D65 as its profile + * connection space, the two spaces are chromatically adapted to use the + * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.

+ * + *

If the source is the sRGB color space, an optimized connector + * is returned to avoid unnecessary computations and loss of precision.

+ * + * @param source The color space to convert colors from + * @param intent The render intent to map colors from the source to the destination + * @return A non-null connector between the specified color space and sRGB + * + * @see #connect(ColorSpace) + * @see #connect(ColorSpace, ColorSpace) + * @see #connect(ColorSpace, ColorSpace, RenderIntent) + */ + @NonNull + public static Connector connect(@NonNull ColorSpace source, @NonNull RenderIntent intent) { + if (source.isSrgb()) return Connector.identity(source); + if (source.getModel() == Model.RGB) { + return new Connector.Rgb((Rgb) source, (Rgb) get(Named.SRGB), intent); + } + return new Connector(source, get(Named.SRGB), intent); + } + /** + *

Performs the chromatic adaptation of a color space from its native + * white point to the specified white point.

+ * + *

The chromatic adaptation is performed using the + * {@link Adaptation#BRADFORD} matrix.

+ * + *

The color space returned by this method always has + * an ID of {@link #MIN_ID}.

+ * + * @param colorSpace The color space to chromatically adapt + * @param whitePoint The new white point + * @return A {@link ColorSpace} instance with the same name, primaries, + * transfer functions and range as the specified color space + * + * @see Adaptation + * @see #adapt(ColorSpace, float[], Adaptation) + */ + @NonNull + public static ColorSpace adapt(@NonNull ColorSpace colorSpace, + @NonNull @Size(min = 2, max = 3) float[] whitePoint) { + return adapt(colorSpace, whitePoint, Adaptation.BRADFORD); + } + /** + *

Performs the chromatic adaptation of a color space from its native + * white point to the specified white point. If the specified color space + * does not have an {@link Model#RGB RGB} color model, or if the color + * space already has the target white point, the color space is returned + * unmodified.

+ * + *

The chromatic adaptation is performed using the von Kries method + * described in the documentation of {@link Adaptation}.

+ * + *

The color space returned by this method always has + * an ID of {@link #MIN_ID}.

+ * + * @param colorSpace The color space to chromatically adapt + * @param whitePoint The new white point + * @param adaptation The adaptation matrix + * @return A new color space if the specified color space has an RGB + * model and a white point different from the specified white + * point; the specified color space otherwise + * + * @see Adaptation + * @see #adapt(ColorSpace, float[]) + */ + @NonNull + public static ColorSpace adapt(@NonNull ColorSpace colorSpace, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @NonNull Adaptation adaptation) { + if (colorSpace.getModel() == Model.RGB) { + ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace; + if (compare(rgb.mWhitePoint, whitePoint)) return colorSpace; + float[] xyz = whitePoint.length == 3 ? + Arrays.copyOf(whitePoint, 3) : xyYToXyz(whitePoint); + float[] adaptationTransform = chromaticAdaptation(adaptation.mTransform, + xyYToXyz(rgb.getWhitePoint()), xyz); + float[] transform = mul3x3(adaptationTransform, rgb.mTransform); + return new ColorSpace.Rgb(rgb, transform, whitePoint); + } + return colorSpace; + } + /** + * Helper method for creating native SkColorSpace. + * + * This essentially calls adapt on a ColorSpace that has not been fully + * created. It also does not fully create the adapted ColorSpace, but + * just returns the transform. + */ + @NonNull @Size(9) + private static float[] adaptToIlluminantD50( + @NonNull @Size(2) float[] origWhitePoint, + @NonNull @Size(9) float[] origTransform) { + float[] desired = ILLUMINANT_D50; + if (compare(origWhitePoint, desired)) return origTransform; + float[] xyz = xyYToXyz(desired); + float[] adaptationTransform = chromaticAdaptation(Adaptation.BRADFORD.mTransform, + xyYToXyz(origWhitePoint), xyz); + return mul3x3(adaptationTransform, origTransform); + } + /** + *

Returns an instance of {@link ColorSpace} whose ID matches the + * specified ID.

+ * + *

This method always returns the same instance for a given ID.

+ * + *

This method is thread-safe.

+ * + * @param index An integer ID between {@link #MIN_ID} and {@link #MAX_ID} + * @return A non-null {@link ColorSpace} instance + * @throws IllegalArgumentException If the ID does not match the ID of one of the + * {@link Named named color spaces} + */ + @NonNull + static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) { + if (index < 0 || index >= sNamedColorSpaces.length) { + throw new IllegalArgumentException("Invalid ID, must be in the range [0.." + + sNamedColorSpaces.length + ")"); + } + return sNamedColorSpaces[index]; + } + /** + * Create a {@link ColorSpace} object using a {@link android.hardware.DataSpace DataSpace} + * value. + * + *

This function maps from a dataspace to a {@link Named} ColorSpace. + * If no {@link Named} ColorSpace object matching the {@code dataSpace} value can be created, + * {@code null} will return.

+ * + * @param dataSpace The dataspace value + * @return the ColorSpace object or {@code null} if no matching colorspace can be found. + */ + @SuppressLint("MethodNameUnits") + @Nullable + public static ColorSpace getFromDataSpace(@NamedDataSpace int dataSpace) { + int index = sDataToColorSpaces.get(dataSpace, -1); + if (index != -1) { + return ColorSpace.get(index); + } else { + return null; + } + } + /** + * Retrieve the {@link android.hardware.DataSpace DataSpace} value from a {@link ColorSpace} + * object. + * + *

If this {@link ColorSpace} object has no matching {@code dataSpace} value, + * {@link android.hardware.DataSpace#DATASPACE_UNKNOWN DATASPACE_UNKNOWN} will return.

+ * + * @return the dataspace value. + */ + @SuppressLint("MethodNameUnits") + public @NamedDataSpace int getDataSpace() { + int index = sDataToColorSpaces.indexOfValue(getId()); + if (index != -1) { + return sDataToColorSpaces.keyAt(index); + } else { + return DataSpace.DATASPACE_UNKNOWN; + } + } + /** + *

Returns an instance of {@link ColorSpace} identified by the specified + * name. The list of names provided in the {@link Named} enum gives access + * to a variety of common RGB color spaces.

+ * + *

This method always returns the same instance for a given name.

+ * + *

This method is thread-safe.

+ * + * @param name The name of the color space to get an instance of + * @return A non-null {@link ColorSpace} instance + */ + @NonNull + public static ColorSpace get(@NonNull Named name) { + return sNamedColorSpaces[name.ordinal()]; + } + /** + *

Returns a {@link Named} instance of {@link ColorSpace} that matches + * the specified RGB to CIE XYZ transform and transfer functions. If no + * instance can be found, this method returns null.

+ * + *

The color transform matrix is assumed to target the CIE XYZ space + * a {@link #ILLUMINANT_D50 D50} standard illuminant.

+ * + * @param toXYZD50 3x3 column-major transform matrix from RGB to the profile + * connection space CIE XYZ as an array of 9 floats, cannot be null + * @param function Parameters for the transfer functions + * @return A non-null {@link ColorSpace} if a match is found, null otherwise + */ + @Nullable + public static ColorSpace match( + @NonNull @Size(9) float[] toXYZD50, + @NonNull Rgb.TransferParameters function) { + for (ColorSpace colorSpace : sNamedColorSpaces) { + if (colorSpace.getModel() == Model.RGB) { + ColorSpace.Rgb rgb = (ColorSpace.Rgb) adapt(colorSpace, ILLUMINANT_D50_XYZ); + if (compare(toXYZD50, rgb.mTransform) && + compare(function, rgb.mTransferParameters)) { + return colorSpace; + } + } + } + return null; + } + static { + sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb( + "sRGB IEC61966-2.1", + SRGB_PRIMARIES, + ILLUMINANT_D65, + null, + SRGB_TRANSFER_PARAMETERS, + Named.SRGB.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB, Named.SRGB.ordinal()); + sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb( + "sRGB IEC61966-2.1 (Linear)", + SRGB_PRIMARIES, + ILLUMINANT_D65, + 1.0, + 0.0f, 1.0f, + Named.LINEAR_SRGB.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB_LINEAR, Named.LINEAR_SRGB.ordinal()); + sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( + "scRGB-nl IEC 61966-2-2:2003", + SRGB_PRIMARIES, + ILLUMINANT_D65, + null, + x -> absRcpResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4), + x -> absResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4), + -0.799f, 2.399f, + SRGB_TRANSFER_PARAMETERS, + Named.EXTENDED_SRGB.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_SCRGB, Named.EXTENDED_SRGB.ordinal()); + sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb( + "scRGB IEC 61966-2-2:2003", + SRGB_PRIMARIES, + ILLUMINANT_D65, + 1.0, + -0.5f, 7.499f, + Named.LINEAR_EXTENDED_SRGB.ordinal() + ); + sDataToColorSpaces.put( + DataSpace.DATASPACE_SCRGB_LINEAR, Named.LINEAR_EXTENDED_SRGB.ordinal()); + sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb( + "Rec. ITU-R BT.709-5", + new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }, + ILLUMINANT_D65, + null, + new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), + Named.BT709.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal()); + sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb( + "Rec. ITU-R BT.2020-1", + new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, + ILLUMINANT_D65, + null, + new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45), + Named.BT2020.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020, Named.BT2020.ordinal()); + sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb( + "SMPTE RP 431-2-2007 DCI (P3)", + new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, + new float[] { 0.314f, 0.351f }, + 2.6, + 0.0f, 1.0f, + Named.DCI_P3.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_DCI_P3, Named.DCI_P3.ordinal()); + sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb( + "Display P3", + new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f }, + ILLUMINANT_D65, + null, + SRGB_TRANSFER_PARAMETERS, + Named.DISPLAY_P3.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_DISPLAY_P3, Named.DISPLAY_P3.ordinal()); + sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb( + "NTSC (1953)", + NTSC_1953_PRIMARIES, + ILLUMINANT_C, + null, + new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), + Named.NTSC_1953.ordinal() + ); + sNamedColorSpaces[Named.SMPTE_C.ordinal()] = new ColorSpace.Rgb( + "SMPTE-C RGB", + new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f }, + ILLUMINANT_D65, + null, + new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45), + Named.SMPTE_C.ordinal() + ); + sNamedColorSpaces[Named.ADOBE_RGB.ordinal()] = new ColorSpace.Rgb( + "Adobe RGB (1998)", + new float[] { 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f }, + ILLUMINANT_D65, + 2.2, + 0.0f, 1.0f, + Named.ADOBE_RGB.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_ADOBE_RGB, Named.ADOBE_RGB.ordinal()); + sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb( + "ROMM RGB ISO 22028-2:2013", + new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f }, + ILLUMINANT_D50, + null, + new Rgb.TransferParameters(1.0, 0.0, 1 / 16.0, 0.031248, 1.8), + Named.PRO_PHOTO_RGB.ordinal() + ); + sNamedColorSpaces[Named.ACES.ordinal()] = new ColorSpace.Rgb( + "SMPTE ST 2065-1:2012 ACES", + new float[] { 0.73470f, 0.26530f, 0.0f, 1.0f, 0.00010f, -0.0770f }, + ILLUMINANT_D60, + 1.0, + -65504.0f, 65504.0f, + Named.ACES.ordinal() + ); + sNamedColorSpaces[Named.ACESCG.ordinal()] = new ColorSpace.Rgb( + "Academy S-2014-004 ACEScg", + new float[] { 0.713f, 0.293f, 0.165f, 0.830f, 0.128f, 0.044f }, + ILLUMINANT_D60, + 1.0, + -65504.0f, 65504.0f, + Named.ACESCG.ordinal() + ); + sNamedColorSpaces[Named.CIE_XYZ.ordinal()] = new Xyz( + "Generic XYZ", + Named.CIE_XYZ.ordinal() + ); + sNamedColorSpaces[Named.CIE_LAB.ordinal()] = new ColorSpace.Lab( + "Generic L*a*b*", + Named.CIE_LAB.ordinal() + ); + } + // Reciprocal piecewise gamma response + private static double rcpResponse(double x, double a, double b, double c, double d, double g) { + return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c; + } + // Piecewise gamma response + private static double response(double x, double a, double b, double c, double d, double g) { + return x >= d ? Math.pow(a * x + b, g) : c * x; + } + // Reciprocal piecewise gamma response + private static double rcpResponse(double x, double a, double b, double c, double d, + double e, double f, double g) { + return x >= d * c ? (Math.pow(x - e, 1.0 / g) - b) / a : (x - f) / c; + } + // Piecewise gamma response + private static double response(double x, double a, double b, double c, double d, + double e, double f, double g) { + return x >= d ? Math.pow(a * x + b, g) + e : c * x + f; + } + // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color + // spaces that allow negative values + @SuppressWarnings("SameParameterValue") + private static double absRcpResponse(double x, double a, double b, double c, double d, double g) { + return Math.copySign(rcpResponse(x < 0.0 ? -x : x, a, b, c, d, g), x); + } + // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that + // allow negative values + @SuppressWarnings("SameParameterValue") + private static double absResponse(double x, double a, double b, double c, double d, double g) { + return Math.copySign(response(x < 0.0 ? -x : x, a, b, c, d, g), x); + } + /** + * Compares two sets of parametric transfer functions parameters with a precision of 1e-3. + * + * @param a The first set of parameters to compare + * @param b The second set of parameters to compare + * @return True if the two sets are equal, false otherwise + */ + private static boolean compare( + @Nullable Rgb.TransferParameters a, + @Nullable Rgb.TransferParameters b) { + //noinspection SimplifiableIfStatement + if (a == null && b == null) return true; + return a != null && b != null && + Math.abs(a.a - b.a) < 1e-3 && + Math.abs(a.b - b.b) < 1e-3 && + Math.abs(a.c - b.c) < 1e-3 && + Math.abs(a.d - b.d) < 2e-3 && // Special case for variations in sRGB OETF/EOTF + Math.abs(a.e - b.e) < 1e-3 && + Math.abs(a.f - b.f) < 1e-3 && + Math.abs(a.g - b.g) < 1e-3; + } + /** + * Compares two arrays of float with a precision of 1e-3. + * + * @param a The first array to compare + * @param b The second array to compare + * @return True if the two arrays are equal, false otherwise + */ + private static boolean compare(@NonNull float[] a, @NonNull float[] b) { + if (a == b) return true; + for (int i = 0; i < a.length; i++) { + if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > 1e-3f) return false; + } + return true; + } + /** + * Inverts a 3x3 matrix. This method assumes the matrix is invertible. + * + * @param m A 3x3 matrix as a non-null array of 9 floats + * @return A new array of 9 floats containing the inverse of the input matrix + */ + @NonNull + @Size(9) + private static float[] inverse3x3(@NonNull @Size(9) float[] m) { + float a = m[0]; + float b = m[3]; + float c = m[6]; + float d = m[1]; + float e = m[4]; + float f = m[7]; + float g = m[2]; + float h = m[5]; + float i = m[8]; + float A = e * i - f * h; + float B = f * g - d * i; + float C = d * h - e * g; + float det = a * A + b * B + c * C; + float inverted[] = new float[m.length]; + inverted[0] = A / det; + inverted[1] = B / det; + inverted[2] = C / det; + inverted[3] = (c * h - b * i) / det; + inverted[4] = (a * i - c * g) / det; + inverted[5] = (b * g - a * h) / det; + inverted[6] = (b * f - c * e) / det; + inverted[7] = (c * d - a * f) / det; + inverted[8] = (a * e - b * d) / det; + return inverted; + } + /** + * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats. + * + * @param lhs 3x3 matrix, as a non-null array of 9 floats + * @param rhs 3x3 matrix, as a non-null array of 9 floats + * @return A new array of 9 floats containing the result of the multiplication + * of rhs by lhs + */ + @NonNull + @Size(9) + private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) { + float[] r = new float[9]; + r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2]; + r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2]; + r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2]; + r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5]; + r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5]; + r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5]; + r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8]; + r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8]; + r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8]; + return r; + } + /** + * Multiplies a vector of 3 components by a 3x3 matrix and stores the + * result in the input vector. + * + * @param lhs 3x3 matrix, as a non-null array of 9 floats + * @param rhs Vector of 3 components, as a non-null array of 3 floats + * @return The array of 3 passed as the rhs parameter + */ + @NonNull + @Size(min = 3) + private static float[] mul3x3Float3( + @NonNull @Size(9) float[] lhs, @NonNull @Size(min = 3) float[] rhs) { + float r0 = rhs[0]; + float r1 = rhs[1]; + float r2 = rhs[2]; + rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2; + rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2; + rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2; + return rhs; + } + /** + * Multiplies a diagonal 3x3 matrix lhs, represented as an array of 3 floats, + * by a 3x3 matrix represented as an array of 9 floats. + * + * @param lhs Diagonal 3x3 matrix, as a non-null array of 3 floats + * @param rhs 3x3 matrix, as a non-null array of 9 floats + * @return A new array of 9 floats containing the result of the multiplication + * of rhs by lhs + */ + @NonNull + @Size(9) + private static float[] mul3x3Diag( + @NonNull @Size(3) float[] lhs, @NonNull @Size(9) float[] rhs) { + return new float[] { + lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2], + lhs[0] * rhs[3], lhs[1] * rhs[4], lhs[2] * rhs[5], + lhs[0] * rhs[6], lhs[1] * rhs[7], lhs[2] * rhs[8] + }; + } + /** + * Converts a value from CIE xyY to CIE XYZ. Y is assumed to be 1 so the + * input xyY array only contains the x and y components. + * + * @param xyY The xyY value to convert to XYZ, cannot be null, length must be 2 + * @return A new float array of length 3 containing XYZ values + */ + @NonNull + @Size(3) + private static float[] xyYToXyz(@NonNull @Size(2) float[] xyY) { + return new float[] { xyY[0] / xyY[1], 1.0f, (1 - xyY[0] - xyY[1]) / xyY[1] }; + } + /** + *

Computes the chromatic adaptation transform from the specified + * source white point to the specified destination white point.

+ * + *

The transform is computed using the von Kries method, described + * in more details in the documentation of {@link Adaptation}. The + * {@link Adaptation} enum provides different matrices that can be + * used to perform the adaptation.

+ * + * @param matrix The adaptation matrix + * @param srcWhitePoint The white point to adapt from, *will be modified* + * @param dstWhitePoint The white point to adapt to, *will be modified* + * @return A 3x3 matrix as a non-null array of 9 floats + */ + @NonNull + @Size(9) + private static float[] chromaticAdaptation(@NonNull @Size(9) float[] matrix, + @NonNull @Size(3) float[] srcWhitePoint, @NonNull @Size(3) float[] dstWhitePoint) { + float[] srcLMS = mul3x3Float3(matrix, srcWhitePoint); + float[] dstLMS = mul3x3Float3(matrix, dstWhitePoint); + // LMS is a diagonal matrix stored as a float[3] + float[] LMS = { dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2] }; + return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix)); + } + /** + *

Computes the chromaticity coordinates of a specified correlated color + * temperature (CCT) on the Planckian locus. The specified CCT must be + * greater than 0. A meaningful CCT range is [1667, 25000].

+ * + *

The transform is computed using the methods in Kang et + * al., Design of Advanced Color - Temperature Control System for HDTV + * Applications, Journal of Korean Physical Society 41, 865-871 + * (2002).

+ * + * @param cct The correlated color temperature, in Kelvin + * @return Corresponding XYZ values + * @throws IllegalArgumentException If cct is invalid + */ + @NonNull + @Size(3) + public static float[] cctToXyz(@IntRange(from = 1) int cct) { + if (cct < 1) { + throw new IllegalArgumentException("Temperature must be greater than 0"); + } + final float icct = 1e3f / cct; + final float icct2 = icct * icct; + final float x = cct <= 4000.0f ? + 0.179910f + 0.8776956f * icct - 0.2343589f * icct2 - 0.2661239f * icct2 * icct : + 0.240390f + 0.2226347f * icct + 2.1070379f * icct2 - 3.0258469f * icct2 * icct; + final float x2 = x * x; + final float y = cct <= 2222.0f ? + -0.20219683f + 2.18555832f * x - 1.34811020f * x2 - 1.1063814f * x2 * x : + cct <= 4000.0f ? + -0.16748867f + 2.09137015f * x - 1.37418593f * x2 - 0.9549476f * x2 * x : + -0.37001483f + 3.75112997f * x - 5.8733867f * x2 + 3.0817580f * x2 * x; + return xyYToXyz(new float[] {x, y}); + } + /** + *

Computes the chromatic adaptation transform from the specified + * source white point to the specified destination white point.

+ * + *

The transform is computed using the von Kries method, described + * in more details in the documentation of {@link Adaptation}. The + * {@link Adaptation} enum provides different matrices that can be + * used to perform the adaptation.

+ * + * @param adaptation The adaptation method + * @param srcWhitePoint The white point to adapt from + * @param dstWhitePoint The white point to adapt to + * @return A 3x3 matrix as a non-null array of 9 floats + */ + @NonNull + @Size(9) + public static float[] chromaticAdaptation(@NonNull Adaptation adaptation, + @NonNull @Size(min = 2, max = 3) float[] srcWhitePoint, + @NonNull @Size(min = 2, max = 3) float[] dstWhitePoint) { + if ((srcWhitePoint.length != 2 && srcWhitePoint.length != 3) + || (dstWhitePoint.length != 2 && dstWhitePoint.length != 3)) { + throw new IllegalArgumentException("A white point array must have 2 or 3 floats"); + } + float[] srcXyz = srcWhitePoint.length == 3 ? + Arrays.copyOf(srcWhitePoint, 3) : xyYToXyz(srcWhitePoint); + float[] dstXyz = dstWhitePoint.length == 3 ? + Arrays.copyOf(dstWhitePoint, 3) : xyYToXyz(dstWhitePoint); + if (compare(srcXyz, dstXyz)) { + return new float[] { + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f + }; + } + return chromaticAdaptation(adaptation.mTransform, srcXyz, dstXyz); + } + /** + * Implementation of the CIE XYZ color space. Assumes the white point is D50. + */ + @AnyThread + private static final class Xyz extends ColorSpace { + private Xyz(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) { + super(name, Model.XYZ, id); + } + @Override + public boolean isWideGamut() { + return true; + } + @Override + public float getMinValue(@IntRange(from = 0, to = 3) int component) { + return -2.0f; + } + @Override + public float getMaxValue(@IntRange(from = 0, to = 3) int component) { + return 2.0f; + } + @Override + public float[] toXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = clamp(v[0]); + v[1] = clamp(v[1]); + v[2] = clamp(v[2]); + return v; + } + @Override + public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = clamp(v[0]); + v[1] = clamp(v[1]); + v[2] = clamp(v[2]); + return v; + } + private static float clamp(float x) { + return x < -2.0f ? -2.0f : x > 2.0f ? 2.0f : x; + } + } + /** + * Implementation of the CIE L*a*b* color space. Its PCS is CIE XYZ + * with a white point of D50. + */ + @AnyThread + private static final class Lab extends ColorSpace { + private static final float A = 216.0f / 24389.0f; + private static final float B = 841.0f / 108.0f; + private static final float C = 4.0f / 29.0f; + private static final float D = 6.0f / 29.0f; + private Lab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) { + super(name, Model.LAB, id); + } + @Override + public boolean isWideGamut() { + return true; + } + @Override + public float getMinValue(@IntRange(from = 0, to = 3) int component) { + return component == 0 ? 0.0f : -128.0f; + } + @Override + public float getMaxValue(@IntRange(from = 0, to = 3) int component) { + return component == 0 ? 100.0f : 128.0f; + } + @Override + public float[] toXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = clamp(v[0], 0.0f, 100.0f); + v[1] = clamp(v[1], -128.0f, 128.0f); + v[2] = clamp(v[2], -128.0f, 128.0f); + float fy = (v[0] + 16.0f) / 116.0f; + float fx = fy + (v[1] * 0.002f); + float fz = fy - (v[2] * 0.005f); + float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C); + float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C); + float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C); + v[0] = X * ILLUMINANT_D50_XYZ[0]; + v[1] = Y * ILLUMINANT_D50_XYZ[1]; + v[2] = Z * ILLUMINANT_D50_XYZ[2]; + return v; + } + @Override + public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { + float X = v[0] / ILLUMINANT_D50_XYZ[0]; + float Y = v[1] / ILLUMINANT_D50_XYZ[1]; + float Z = v[2] / ILLUMINANT_D50_XYZ[2]; + float fx = X > A ? (float) Math.pow(X, 1.0 / 3.0) : B * X + C; + float fy = Y > A ? (float) Math.pow(Y, 1.0 / 3.0) : B * Y + C; + float fz = Z > A ? (float) Math.pow(Z, 1.0 / 3.0) : B * Z + C; + float L = 116.0f * fy - 16.0f; + float a = 500.0f * (fx - fy); + float b = 200.0f * (fy - fz); + v[0] = clamp(L, 0.0f, 100.0f); + v[1] = clamp(a, -128.0f, 128.0f); + v[2] = clamp(b, -128.0f, 128.0f); + return v; + } + private static float clamp(float x, float min, float max) { + return x < min ? min : x > max ? max : x; + } + } + /** + * Retrieve the native SkColorSpace object for passing to native. + * + * Only valid on ColorSpace.Rgb. + */ + long getNativeInstance() { + throw new IllegalArgumentException("colorSpace must be an RGB color space"); + } + /** + * {@usesMathJax} + * + *

An RGB color space is an additive color space using the + * {@link Model#RGB RGB} color model (a color is therefore represented + * by a tuple of 3 numbers).

+ * + *

A specific RGB color space is defined by the following properties:

+ *
    + *
  • Three chromaticities of the red, green and blue primaries, which + * define the gamut of the color space.
  • + *
  • A white point chromaticity that defines the stimulus to which + * color space values are normalized (also just called "white").
  • + *
  • An opto-electronic transfer function, also called opto-electronic + * conversion function or often, and approximately, gamma function.
  • + *
  • An electro-optical transfer function, also called electo-optical + * conversion function or often, and approximately, gamma function.
  • + *
  • A range of valid RGB values (most commonly \([0..1]\)).
  • + *
+ * + *

The most commonly used RGB color space is {@link Named#SRGB sRGB}.

+ * + *

Primaries and white point chromaticities

+ *

In this implementation, the chromaticity of the primaries and the white + * point of an RGB color space is defined in the CIE xyY color space. This + * color space separates the chromaticity of a color, the x and y components, + * and its luminance, the Y component. Since the primaries and the white + * point have full brightness, the Y component is assumed to be 1 and only + * the x and y components are needed to encode them.

+ *

For convenience, this implementation also allows to define the + * primaries and white point in the CIE XYZ space. The tristimulus XYZ values + * are internally converted to xyY.

+ * + *

+ * + *

sRGB primaries and white point
+ *

+ * + *

Transfer functions

+ *

A transfer function is a color component conversion function, defined as + * a single variable, monotonic mathematical function. It is applied to each + * individual component of a color. They are used to perform the mapping + * between linear tristimulus values and non-linear electronic signal value.

+ *

The opto-electronic transfer function (OETF or OECF) encodes + * tristimulus values in a scene to a non-linear electronic signal value. + * An OETF is often expressed as a power function with an exponent between + * 0.38 and 0.55 (the reciprocal of 1.8 to 2.6).

+ *

The electro-optical transfer function (EOTF or EOCF) decodes + * a non-linear electronic signal value to a tristimulus value at the display. + * An EOTF is often expressed as a power function with an exponent between + * 1.8 and 2.6.

+ *

Transfer functions are used as a compression scheme. For instance, + * linear sRGB values would normally require 11 to 12 bits of precision to + * store all values that can be perceived by the human eye. When encoding + * sRGB values using the appropriate OETF (see {@link Named#SRGB sRGB} for + * an exact mathematical description of that OETF), the values can be + * compressed to only 8 bits precision.

+ *

When manipulating RGB values, particularly sRGB values, it is safe + * to assume that these values have been encoded with the appropriate + * OETF (unless noted otherwise). Encoded values are often said to be in + * "gamma space". They are therefore defined in a non-linear space. This + * in turns means that any linear operation applied to these values is + * going to yield mathematically incorrect results (any linear interpolation + * such as gradient generation for instance, most image processing functions + * such as blurs, etc.).

+ *

To properly process encoded RGB values you must first apply the + * EOTF to decode the value into linear space. After processing, the RGB + * value must be encoded back to non-linear ("gamma") space. Here is a + * formal description of the process, where \(f\) is the processing + * function to apply:

+ * + * $$RGB_{out} = OETF(f(EOTF(RGB_{in})))$$ + * + *

If the transfer functions of the color space can be expressed as an + * ICC parametric curve as defined in ICC.1:2004-10, the numeric parameters + * can be retrieved by calling {@link #getTransferParameters()}. This can + * be useful to match color spaces for instance.

+ * + *

Some RGB color spaces, such as {@link Named#ACES} and + * {@link Named#LINEAR_EXTENDED_SRGB scRGB}, are said to be linear because + * their transfer functions are the identity function: \(f(x) = x\). + * If the source and/or destination are known to be linear, it is not + * necessary to invoke the transfer functions.

+ * + *

Range

+ *

Most RGB color spaces allow RGB values in the range \([0..1]\). There + * are however a few RGB color spaces that allow much larger ranges. For + * instance, {@link Named#EXTENDED_SRGB scRGB} is used to manipulate the + * range \([-0.5..7.5]\) while {@link Named#ACES ACES} can be used throughout + * the range \([-65504, 65504]\).

+ * + *

+ * + *

Extended sRGB and its large range
+ *

+ * + *

Converting between RGB color spaces

+ *

Conversion between two color spaces is achieved by using an intermediate + * color space called the profile connection space (PCS). The PCS used by + * this implementation is CIE XYZ. The conversion operation is defined + * as such:

+ * + * $$RGB_{out} = OETF(T_{dst}^{-1} \cdot T_{src} \cdot EOTF(RGB_{in}))$$ + * + *

Where \(T_{src}\) is the {@link #getTransform() RGB to XYZ transform} + * of the source color space and \(T_{dst}^{-1}\) the {@link #getInverseTransform() + * XYZ to RGB transform} of the destination color space.

+ *

Many RGB color spaces commonly used with electronic devices use the + * standard illuminant {@link #ILLUMINANT_D65 D65}. Care must be take however + * when converting between two RGB color spaces if their white points do not + * match. This can be achieved by either calling + * {@link #adapt(ColorSpace, float[])} to adapt one or both color spaces to + * a single common white point. This can be achieved automatically by calling + * {@link ColorSpace#connect(ColorSpace, ColorSpace)}, which also handles + * non-RGB color spaces.

+ *

To learn more about the white point adaptation process, refer to the + * documentation of {@link Adaptation}.

+ */ + @AnyThread + public static class Rgb extends ColorSpace { + /** + * {@usesMathJax} + * + *

Defines the parameters for the ICC parametric curve type 4, as + * defined in ICC.1:2004-10, section 10.15.

+ * + *

The EOTF is of the form:

+ * + * \(\begin{equation} + * Y = \begin{cases}c X + f & X \lt d \\\ + * \left( a X + b \right) ^{g} + e & X \ge d \end{cases} + * \end{equation}\) + * + *

The corresponding OETF is simply the inverse function.

+ * + *

The parameters defined by this class form a valid transfer + * function only if all the following conditions are met:

+ *
    + *
  • No parameter is a {@link Double#isNaN(double) Not-a-Number}
  • + *
  • \(d\) is in the range \([0..1]\)
  • + *
  • The function is not constant
  • + *
  • The function is positive and increasing
  • + *
+ */ + public static class TransferParameters { + /** Variable \(a\) in the equation of the EOTF described above. */ + public final double a; + /** Variable \(b\) in the equation of the EOTF described above. */ + public final double b; + /** Variable \(c\) in the equation of the EOTF described above. */ + public final double c; + /** Variable \(d\) in the equation of the EOTF described above. */ + public final double d; + /** Variable \(e\) in the equation of the EOTF described above. */ + public final double e; + /** Variable \(f\) in the equation of the EOTF described above. */ + public final double f; + /** Variable \(g\) in the equation of the EOTF described above. */ + public final double g; + /** + *

Defines the parameters for the ICC parametric curve type 3, as + * defined in ICC.1:2004-10, section 10.15.

+ * + *

The EOTF is of the form:

+ * + * \(\begin{equation} + * Y = \begin{cases}c X & X \lt d \\\ + * \left( a X + b \right) ^{g} & X \ge d \end{cases} + * \end{equation}\) + * + *

This constructor is equivalent to setting \(e\) and \(f\) to 0.

+ * + * @param a The value of \(a\) in the equation of the EOTF described above + * @param b The value of \(b\) in the equation of the EOTF described above + * @param c The value of \(c\) in the equation of the EOTF described above + * @param d The value of \(d\) in the equation of the EOTF described above + * @param g The value of \(g\) in the equation of the EOTF described above + * + * @throws IllegalArgumentException If the parameters form an invalid transfer function + */ + public TransferParameters(double a, double b, double c, double d, double g) { + this(a, b, c, d, 0.0, 0.0, g); + } + /** + *

Defines the parameters for the ICC parametric curve type 4, as + * defined in ICC.1:2004-10, section 10.15.

+ * + * @param a The value of \(a\) in the equation of the EOTF described above + * @param b The value of \(b\) in the equation of the EOTF described above + * @param c The value of \(c\) in the equation of the EOTF described above + * @param d The value of \(d\) in the equation of the EOTF described above + * @param e The value of \(e\) in the equation of the EOTF described above + * @param f The value of \(f\) in the equation of the EOTF described above + * @param g The value of \(g\) in the equation of the EOTF described above + * + * @throws IllegalArgumentException If the parameters form an invalid transfer function + */ + public TransferParameters(double a, double b, double c, double d, double e, + double f, double g) { + if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) || + Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) || + Double.isNaN(g)) { + throw new IllegalArgumentException("Parameters cannot be NaN"); + } + // Next representable float after 1.0 + // We use doubles here but the representation inside our native code is often floats + if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) { + throw new IllegalArgumentException("Parameter d must be in the range [0..1], " + + "was " + d); + } + if (d == 0.0 && (a == 0.0 || g == 0.0)) { + throw new IllegalArgumentException( + "Parameter a or g is zero, the transfer function is constant"); + } + if (d >= 1.0 && c == 0.0) { + throw new IllegalArgumentException( + "Parameter c is zero, the transfer function is constant"); + } + if ((a == 0.0 || g == 0.0) && c == 0.0) { + throw new IllegalArgumentException("Parameter a or g is zero," + + " and c is zero, the transfer function is constant"); + } + if (c < 0.0) { + throw new IllegalArgumentException("The transfer function must be increasing"); + } + if (a < 0.0 || g < 0.0) { + throw new IllegalArgumentException("The transfer function must be " + + "positive or increasing"); + } + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + this.g = g; + } + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransferParameters that = (TransferParameters) o; + if (Double.compare(that.a, a) != 0) return false; + if (Double.compare(that.b, b) != 0) return false; + if (Double.compare(that.c, c) != 0) return false; + if (Double.compare(that.d, d) != 0) return false; + if (Double.compare(that.e, e) != 0) return false; + if (Double.compare(that.f, f) != 0) return false; + return Double.compare(that.g, g) == 0; + } + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(a); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(b); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(c); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(d); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(e); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(f); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(g); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + } + @NonNull private final float[] mWhitePoint; + @NonNull private final float[] mPrimaries; + @NonNull private final float[] mTransform; + @NonNull private final float[] mInverseTransform; + @NonNull private final DoubleUnaryOperator mOetf; + @NonNull private final DoubleUnaryOperator mEotf; + @NonNull private final DoubleUnaryOperator mClampedOetf; + @NonNull private final DoubleUnaryOperator mClampedEotf; + private final float mMin; + private final float mMax; + private final boolean mIsWideGamut; + private final boolean mIsSrgb; + @Nullable private final TransferParameters mTransferParameters; + private final long mNativePtr; + @Override + long getNativeInstance() { + if (mNativePtr == 0) { + // If this object has TransferParameters, it must have a native object. + throw new IllegalArgumentException("ColorSpace must use an ICC " + + "parametric transfer function! used " + this); + } + return mNativePtr; + } + private static native long nativeGetNativeFinalizer(); + private static native long nativeCreate(float a, float b, float c, float d, + float e, float f, float g, float[] xyz); + /** + *

Creates a new RGB color space using a 3x3 column-major transform matrix. + * The transform matrix must convert from the RGB space to the profile connection + * space CIE XYZ.

+ * + *

The range of the color space is imposed to be \([0..1]\).

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param toXYZ 3x3 column-major transform matrix from RGB to the profile + * connection space CIE XYZ as an array of 9 floats, cannot be null + * @param oetf Opto-electronic transfer function, cannot be null + * @param eotf Electro-optical transfer function, cannot be null + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • The OETF is null or the EOTF is null.
  • + *
  • The minimum valid value is >= the maximum valid value.
  • + *
+ * + * @see #get(Named) + */ + public Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(9) float[] toXYZ, + @NonNull DoubleUnaryOperator oetf, + @NonNull DoubleUnaryOperator eotf) { + this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), null, + oetf, eotf, 0.0f, 1.0f, null, MIN_ID); + } + /** + *

Creates a new RGB color space using a specified set of primaries + * and a specified white point.

+ * + *

The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.

+ * + *

The ID, as returned by {@link #getId()}, of an object created by + * this constructor is always {@link #MIN_ID}.

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param oetf Opto-electronic transfer function, cannot be null + * @param eotf Electro-optical transfer function, cannot be null + * @param min The minimum valid value in this color space's RGB range + * @param max The maximum valid value in this color space's RGB range + * + * @throws IllegalArgumentException

If any of the following conditions is met:

+ *
    + *
  • The name is null or has a length of 0.
  • + *
  • The primaries array is null or has a length that is neither 6 or 9.
  • + *
  • The white point array is null or has a length that is neither 2 or 3.
  • + *
  • The OETF is null or the EOTF is null.
  • + *
  • The minimum valid value is >= the maximum valid value.
  • + *
+ * + * @see #get(Named) + */ + public Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(min = 6, max = 9) float[] primaries, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @NonNull DoubleUnaryOperator oetf, + @NonNull DoubleUnaryOperator eotf, + float min, + float max) { + this(name, primaries, whitePoint, null, oetf, eotf, min, max, null, MIN_ID); + } + /** + *

Creates a new RGB color space using a 3x3 column-major transform matrix. + * The transform matrix must convert from the RGB space to the profile connection + * space CIE XYZ.

+ * + *

The range of the color space is imposed to be \([0..1]\).

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param toXYZ 3x3 column-major transform matrix from RGB to the profile + * connection space CIE XYZ as an array of 9 floats, cannot be null + * @param function Parameters for the transfer functions + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • Gamma is negative.
  • + *
+ * + * @see #get(Named) + */ + public Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(9) float[] toXYZ, + @NonNull TransferParameters function) { + // Note: when isGray() returns false, this passes null for the transform for + // consistency with other constructors, which compute the transform from the primaries + // and white point. + this(name, isGray(toXYZ) ? GRAY_PRIMARIES : computePrimaries(toXYZ), + computeWhitePoint(toXYZ), isGray(toXYZ) ? toXYZ : null, function, MIN_ID); + } + /** + *

Creates a new RGB color space using a specified set of primaries + * and a specified white point.

+ * + *

The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param function Parameters for the transfer functions + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • The primaries array is null or has a length that is neither 6 or 9.
  • + *
  • The white point array is null or has a length that is neither 2 or 3.
  • + *
  • The transfer parameters are invalid.
  • + *
+ * + * @see #get(Named) + */ + public Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(min = 6, max = 9) float[] primaries, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @NonNull TransferParameters function) { + this(name, primaries, whitePoint, null, function, MIN_ID); + } + /** + *

Creates a new RGB color space using a specified set of primaries + * and a specified white point.

+ * + *

The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param transform Computed transform matrix that converts from RGB to XYZ, or + * {@code null} to compute it from {@code primaries} and {@code whitePoint}. + * @param function Parameters for the transfer functions + * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID} + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • The primaries array is null or has a length that is neither 6 or 9.
  • + *
  • The white point array is null or has a length that is neither 2 or 3.
  • + *
  • The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
  • + *
  • The transfer parameters are invalid.
  • + *
+ * + * @see #get(Named) + */ + private Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(min = 6, max = 9) float[] primaries, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @Nullable @Size(9) float[] transform, + @NonNull TransferParameters function, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + this(name, primaries, whitePoint, transform, + function.e == 0.0 && function.f == 0.0 ? + x -> rcpResponse(x, function.a, function.b, + function.c, function.d, function.g) : + x -> rcpResponse(x, function.a, function.b, function.c, + function.d, function.e, function.f, function.g), + function.e == 0.0 && function.f == 0.0 ? + x -> response(x, function.a, function.b, + function.c, function.d, function.g) : + x -> response(x, function.a, function.b, function.c, + function.d, function.e, function.f, function.g), + 0.0f, 1.0f, function, id); + } + /** + *

Creates a new RGB color space using a 3x3 column-major transform matrix. + * The transform matrix must convert from the RGB space to the profile connection + * space CIE XYZ.

+ * + *

The range of the color space is imposed to be \([0..1]\).

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param toXYZ 3x3 column-major transform matrix from RGB to the profile + * connection space CIE XYZ as an array of 9 floats, cannot be null + * @param gamma Gamma to use as the transfer function + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • Gamma is negative.
  • + *
+ * + * @see #get(Named) + */ + public Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(9) float[] toXYZ, + double gamma) { + this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f, MIN_ID); + } + /** + *

Creates a new RGB color space using a specified set of primaries + * and a specified white point.

+ * + *

The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param gamma Gamma to use as the transfer function + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • The primaries array is null or has a length that is neither 6 or 9.
  • + *
  • The white point array is null or has a length that is neither 2 or 3.
  • + *
  • Gamma is negative.
  • + *
+ * + * @see #get(Named) + */ + public Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(min = 6, max = 9) float[] primaries, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + double gamma) { + this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MIN_ID); + } + /** + *

Creates a new RGB color space using a specified set of primaries + * and a specified white point.

+ * + *

The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param gamma Gamma to use as the transfer function + * @param min The minimum valid value in this color space's RGB range + * @param max The maximum valid value in this color space's RGB range + * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID} + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • The primaries array is null or has a length that is neither 6 or 9.
  • + *
  • The white point array is null or has a length that is neither 2 or 3.
  • + *
  • The minimum valid value is >= the maximum valid value.
  • + *
  • The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
  • + *
  • Gamma is negative.
  • + *
+ * + * @see #get(Named) + */ + private Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(min = 6, max = 9) float[] primaries, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + double gamma, + float min, + float max, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + this(name, primaries, whitePoint, null, + gamma == 1.0 ? DoubleUnaryOperator.identity() : + x -> Math.pow(x < 0.0 ? 0.0 : x, 1 / gamma), + gamma == 1.0 ? DoubleUnaryOperator.identity() : + x -> Math.pow(x < 0.0 ? 0.0 : x, gamma), + min, max, new TransferParameters(1.0, 0.0, 0.0, 0.0, gamma), id); + } + /** + *

Creates a new RGB color space using a specified set of primaries + * and a specified white point.

+ * + *

The primaries and white point can be specified in the CIE xyY space + * or in CIE XYZ. The length of the arrays depends on the chosen space:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

When the primaries and/or white point are specified in xyY, the Y component + * does not need to be specified and is assumed to be 1.0. Only the xy components + * are required.

+ * + * @param name Name of the color space, cannot be null, its length must be >= 1 + * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats + * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats + * @param transform Computed transform matrix that converts from RGB to XYZ, or + * {@code null} to compute it from {@code primaries} and {@code whitePoint}. + * @param oetf Opto-electronic transfer function, cannot be null + * @param eotf Electro-optical transfer function, cannot be null + * @param min The minimum valid value in this color space's RGB range + * @param max The maximum valid value in this color space's RGB range + * @param transferParameters Parameters for the transfer functions + * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID} + * + * @throws IllegalArgumentException If any of the following conditions is met: + *
    + *
  • The name is null or has a length of 0.
  • + *
  • The primaries array is null or has a length that is neither 6 or 9.
  • + *
  • The white point array is null or has a length that is neither 2 or 3.
  • + *
  • The OETF is null or the EOTF is null.
  • + *
  • The minimum valid value is >= the maximum valid value.
  • + *
  • The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
  • + *
+ * + * @see #get(Named) + */ + private Rgb( + @NonNull @Size(min = 1) String name, + @NonNull @Size(min = 6, max = 9) float[] primaries, + @NonNull @Size(min = 2, max = 3) float[] whitePoint, + @Nullable @Size(9) float[] transform, + @NonNull DoubleUnaryOperator oetf, + @NonNull DoubleUnaryOperator eotf, + float min, + float max, + @Nullable TransferParameters transferParameters, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + super(name, Model.RGB, id); + if (primaries == null || (primaries.length != 6 && primaries.length != 9)) { + throw new IllegalArgumentException("The color space's primaries must be " + + "defined as an array of 6 floats in xyY or 9 floats in XYZ"); + } + if (whitePoint == null || (whitePoint.length != 2 && whitePoint.length != 3)) { + throw new IllegalArgumentException("The color space's white point must be " + + "defined as an array of 2 floats in xyY or 3 float in XYZ"); + } + if (oetf == null || eotf == null) { + throw new IllegalArgumentException("The transfer functions of a color space " + + "cannot be null"); + } + if (min >= max) { + throw new IllegalArgumentException("Invalid range: min=" + min + ", max=" + max + + "; min must be strictly < max"); + } + mWhitePoint = xyWhitePoint(whitePoint); + mPrimaries = xyPrimaries(primaries); + if (transform == null) { + mTransform = computeXYZMatrix(mPrimaries, mWhitePoint); + } else { + if (transform.length != 9) { + throw new IllegalArgumentException("Transform must have 9 entries! Has " + + transform.length); + } + mTransform = transform; + } + mInverseTransform = inverse3x3(mTransform); + mOetf = oetf; + mEotf = eotf; + mMin = min; + mMax = max; + DoubleUnaryOperator clamp = this::clamp; + mClampedOetf = oetf.andThen(clamp); + mClampedEotf = clamp.andThen(eotf); + mTransferParameters = transferParameters; + // A color space is wide-gamut if its area is >90% of NTSC 1953 and + // if it entirely contains the Color space definition in xyY + mIsWideGamut = isWideGamut(mPrimaries, min, max); + mIsSrgb = isSrgb(mPrimaries, mWhitePoint, oetf, eotf, min, max, id); + + mNativePtr = 0; + } + /** + * Creates a copy of the specified color space with a new transform. + * + * @param colorSpace The color space to create a copy of + */ + private Rgb(Rgb colorSpace, + @NonNull @Size(9) float[] transform, + @NonNull @Size(min = 2, max = 3) float[] whitePoint) { + this(colorSpace.getName(), colorSpace.mPrimaries, whitePoint, transform, + colorSpace.mOetf, colorSpace.mEotf, colorSpace.mMin, colorSpace.mMax, + colorSpace.mTransferParameters, MIN_ID); + } + /** + * Copies the non-adapted CIE xyY white point of this color space in + * specified array. The Y component is assumed to be 1 and is therefore + * not copied into the destination. The x and y components are written + * in the array at positions 0 and 1 respectively. + * + * @param whitePoint The destination array, cannot be null, its length + * must be >= 2 + * + * @return The destination array passed as a parameter + * + * @see #getWhitePoint() + */ + @NonNull + @Size(min = 2) + public float[] getWhitePoint(@NonNull @Size(min = 2) float[] whitePoint) { + whitePoint[0] = mWhitePoint[0]; + whitePoint[1] = mWhitePoint[1]; + return whitePoint; + } + /** + * Returns the non-adapted CIE xyY white point of this color space as + * a new array of 2 floats. The Y component is assumed to be 1 and is + * therefore not copied into the destination. The x and y components + * are written in the array at positions 0 and 1 respectively. + * + * @return A new non-null array of 2 floats + * + * @see #getWhitePoint(float[]) + */ + @NonNull + @Size(2) + public float[] getWhitePoint() { + return Arrays.copyOf(mWhitePoint, mWhitePoint.length); + } + /** + * Copies the primaries of this color space in specified array. The Y + * component is assumed to be 1 and is therefore not copied into the + * destination. The x and y components of the first primary are written + * in the array at positions 0 and 1 respectively. + * + *

Note: Some ColorSpaces represent gray profiles. The concept of + * primaries for such a ColorSpace does not make sense, so we use a special + * set of primaries that are all 1s.

+ * + * @param primaries The destination array, cannot be null, its length + * must be >= 6 + * + * @return The destination array passed as a parameter + * + * @see #getPrimaries() + */ + @NonNull + @Size(min = 6) + public float[] getPrimaries(@NonNull @Size(min = 6) float[] primaries) { + System.arraycopy(mPrimaries, 0, primaries, 0, mPrimaries.length); + return primaries; + } + /** + * Returns the primaries of this color space as a new array of 6 floats. + * The Y component is assumed to be 1 and is therefore not copied into + * the destination. The x and y components of the first primary are + * written in the array at positions 0 and 1 respectively. + * + *

Note: Some ColorSpaces represent gray profiles. The concept of + * primaries for such a ColorSpace does not make sense, so we use a special + * set of primaries that are all 1s.

+ * + * @return A new non-null array of 2 floats + * + * @see #getPrimaries(float[]) + */ + @NonNull + @Size(6) + public float[] getPrimaries() { + return Arrays.copyOf(mPrimaries, mPrimaries.length); + } + /** + *

Copies the transform of this color space in specified array. The + * transform is used to convert from RGB to XYZ (with the same white + * point as this color space). To connect color spaces, you must first + * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the + * same white point.

+ *

It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.

+ * + * @param transform The destination array, cannot be null, its length + * must be >= 9 + * + * @return The destination array passed as a parameter + * + * @see #getTransform() + */ + @NonNull + @Size(min = 9) + public float[] getTransform(@NonNull @Size(min = 9) float[] transform) { + System.arraycopy(mTransform, 0, transform, 0, mTransform.length); + return transform; + } + /** + *

Returns the transform of this color space as a new array. The + * transform is used to convert from RGB to XYZ (with the same white + * point as this color space). To connect color spaces, you must first + * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the + * same white point.

+ *

It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.

+ * + * @return A new array of 9 floats + * + * @see #getTransform(float[]) + */ + @NonNull + @Size(9) + public float[] getTransform() { + return Arrays.copyOf(mTransform, mTransform.length); + } + /** + *

Copies the inverse transform of this color space in specified array. + * The inverse transform is used to convert from XYZ to RGB (with the + * same white point as this color space). To connect color spaces, you + * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them + * to the same white point.

+ *

It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.

+ * + * @param inverseTransform The destination array, cannot be null, its length + * must be >= 9 + * + * @return The destination array passed as a parameter + * + * @see #getInverseTransform() + */ + @NonNull + @Size(min = 9) + public float[] getInverseTransform(@NonNull @Size(min = 9) float[] inverseTransform) { + System.arraycopy(mInverseTransform, 0, inverseTransform, 0, mInverseTransform.length); + return inverseTransform; + } + /** + *

Returns the inverse transform of this color space as a new array. + * The inverse transform is used to convert from XYZ to RGB (with the + * same white point as this color space). To connect color spaces, you + * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them + * to the same white point.

+ *

It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)} + * to convert between color spaces.

+ * + * @return A new array of 9 floats + * + * @see #getInverseTransform(float[]) + */ + @NonNull + @Size(9) + public float[] getInverseTransform() { + return Arrays.copyOf(mInverseTransform, mInverseTransform.length); + } + /** + *

Returns the opto-electronic transfer function (OETF) of this color space. + * The inverse function is the electro-optical transfer function (EOTF) returned + * by {@link #getEotf()}. These functions are defined to satisfy the following + * equality for \(x \in [0..1]\):

+ * + * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$ + * + *

For RGB colors, this function can be used to convert from linear space + * to "gamma space" (gamma encoded). The terms gamma space and gamma encoded + * are frequently used because many OETFs can be closely approximated using + * a simple power function of the form \(x^{\frac{1}{\gamma}}\) (the + * approximation of the {@link Named#SRGB sRGB} OETF uses \(\gamma=2.2\) + * for instance).

+ * + * @return A transfer function that converts from linear space to "gamma space" + * + * @see #getEotf() + * @see #getTransferParameters() + */ + @NonNull + public DoubleUnaryOperator getOetf() { + return mClampedOetf; + } + /** + *

Returns the electro-optical transfer function (EOTF) of this color space. + * The inverse function is the opto-electronic transfer function (OETF) + * returned by {@link #getOetf()}. These functions are defined to satisfy the + * following equality for \(x \in [0..1]\):

+ * + * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$ + * + *

For RGB colors, this function can be used to convert from "gamma space" + * (gamma encoded) to linear space. The terms gamma space and gamma encoded + * are frequently used because many EOTFs can be closely approximated using + * a simple power function of the form \(x^\gamma\) (the approximation of the + * {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) for instance).

+ * + * @return A transfer function that converts from "gamma space" to linear space + * + * @see #getOetf() + * @see #getTransferParameters() + */ + @NonNull + public DoubleUnaryOperator getEotf() { + return mClampedEotf; + } + /** + *

Returns the parameters used by the {@link #getEotf() electro-optical} + * and {@link #getOetf() opto-electronic} transfer functions. If the transfer + * functions do not match the ICC parametric curves defined in ICC.1:2004-10 + * (section 10.15), this method returns null.

+ * + *

See {@link TransferParameters} for a full description of the transfer + * functions.

+ * + * @return An instance of {@link TransferParameters} or null if this color + * space's transfer functions do not match the equation defined in + * {@link TransferParameters} + */ + @Nullable + public TransferParameters getTransferParameters() { + return mTransferParameters; + } + @Override + public boolean isSrgb() { + return mIsSrgb; + } + @Override + public boolean isWideGamut() { + return mIsWideGamut; + } + @Override + public float getMinValue(int component) { + return mMin; + } + @Override + public float getMaxValue(int component) { + return mMax; + } + /** + *

Decodes an RGB value to linear space. This is achieved by + * applying this color space's electro-optical transfer function + * to the supplied values.

+ * + *

Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.

+ * + * @param r The red component to decode to linear space + * @param g The green component to decode to linear space + * @param b The blue component to decode to linear space + * @return A new array of 3 floats containing linear RGB values + * + * @see #toLinear(float[]) + * @see #fromLinear(float, float, float) + */ + @NonNull + @Size(3) + public float[] toLinear(float r, float g, float b) { + return toLinear(new float[] { r, g, b }); + } + /** + *

Decodes an RGB value to linear space. This is achieved by + * applying this color space's electro-optical transfer function + * to the first 3 values of the supplied array. The result is + * stored back in the input array.

+ * + *

Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.

+ * + * @param v A non-null array of non-linear RGB values, its length + * must be at least 3 + * @return The specified array + * + * @see #toLinear(float, float, float) + * @see #fromLinear(float[]) + */ + @NonNull + @Size(min = 3) + public float[] toLinear(@NonNull @Size(min = 3) float[] v) { + v[0] = (float) mClampedEotf.applyAsDouble(v[0]); + v[1] = (float) mClampedEotf.applyAsDouble(v[1]); + v[2] = (float) mClampedEotf.applyAsDouble(v[2]); + return v; + } + /** + *

Encodes an RGB value from linear space to this color space's + * "gamma space". This is achieved by applying this color space's + * opto-electronic transfer function to the supplied values.

+ * + *

Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.

+ * + * @param r The red component to encode from linear space + * @param g The green component to encode from linear space + * @param b The blue component to encode from linear space + * @return A new array of 3 floats containing non-linear RGB values + * + * @see #fromLinear(float[]) + * @see #toLinear(float, float, float) + */ + @NonNull + @Size(3) + public float[] fromLinear(float r, float g, float b) { + return fromLinear(new float[] { r, g, b }); + } + /** + *

Encodes an RGB value from linear space to this color space's + * "gamma space". This is achieved by applying this color space's + * opto-electronic transfer function to the first 3 values of the + * supplied array. The result is stored back in the input array.

+ * + *

Refer to the documentation of {@link ColorSpace.Rgb} for + * more information about transfer functions and their use for + * encoding and decoding RGB values.

+ * + * @param v A non-null array of linear RGB values, its length + * must be at least 3 + * @return A new array of 3 floats containing non-linear RGB values + * + * @see #fromLinear(float[]) + * @see #toLinear(float, float, float) + */ + @NonNull + @Size(min = 3) + public float[] fromLinear(@NonNull @Size(min = 3) float[] v) { + v[0] = (float) mClampedOetf.applyAsDouble(v[0]); + v[1] = (float) mClampedOetf.applyAsDouble(v[1]); + v[2] = (float) mClampedOetf.applyAsDouble(v[2]); + return v; + } + @Override + @NonNull + @Size(min = 3) + public float[] toXyz(@NonNull @Size(min = 3) float[] v) { + v[0] = (float) mClampedEotf.applyAsDouble(v[0]); + v[1] = (float) mClampedEotf.applyAsDouble(v[1]); + v[2] = (float) mClampedEotf.applyAsDouble(v[2]); + return mul3x3Float3(mTransform, v); + } + @Override + @NonNull + @Size(min = 3) + public float[] fromXyz(@NonNull @Size(min = 3) float[] v) { + mul3x3Float3(mInverseTransform, v); + v[0] = (float) mClampedOetf.applyAsDouble(v[0]); + v[1] = (float) mClampedOetf.applyAsDouble(v[1]); + v[2] = (float) mClampedOetf.applyAsDouble(v[2]); + return v; + } + private double clamp(double x) { + return x < mMin ? mMin : x > mMax ? mMax : x; + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Rgb rgb = (Rgb) o; + if (Float.compare(rgb.mMin, mMin) != 0) return false; + if (Float.compare(rgb.mMax, mMax) != 0) return false; + if (!Arrays.equals(mWhitePoint, rgb.mWhitePoint)) return false; + if (!Arrays.equals(mPrimaries, rgb.mPrimaries)) return false; + if (mTransferParameters != null) { + return mTransferParameters.equals(rgb.mTransferParameters); + } else if (rgb.mTransferParameters == null) { + return true; + } + //noinspection SimplifiableIfStatement + if (!mOetf.equals(rgb.mOetf)) return false; + return mEotf.equals(rgb.mEotf); + } + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(mWhitePoint); + result = 31 * result + Arrays.hashCode(mPrimaries); + result = 31 * result + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0); + result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0); + result = 31 * result + + (mTransferParameters != null ? mTransferParameters.hashCode() : 0); + if (mTransferParameters == null) { + result = 31 * result + mOetf.hashCode(); + result = 31 * result + mEotf.hashCode(); + } + return result; + } + /** + * Computes whether a color space is the sRGB color space or at least + * a close approximation. + * + * @param primaries The set of RGB primaries in xyY as an array of 6 floats + * @param whitePoint The white point in xyY as an array of 2 floats + * @param OETF The opto-electronic transfer function + * @param EOTF The electro-optical transfer function + * @param min The minimum value of the color space's range + * @param max The minimum value of the color space's range + * @param id The ID of the color space + * @return True if the color space can be considered as the sRGB color space + * + * @see #isSrgb() + */ + @SuppressWarnings("RedundantIfStatement") + private static boolean isSrgb( + @NonNull @Size(6) float[] primaries, + @NonNull @Size(2) float[] whitePoint, + @NonNull DoubleUnaryOperator OETF, + @NonNull DoubleUnaryOperator EOTF, + float min, + float max, + @IntRange(from = MIN_ID, to = MAX_ID) int id) { + if (id == 0) return true; + if (!ColorSpace.compare(primaries, SRGB_PRIMARIES)) { + return false; + } + if (!ColorSpace.compare(whitePoint, ILLUMINANT_D65)) { + return false; + } + if (min != 0.0f) return false; + if (max != 1.0f) return false; + // We would have already returned true if this was SRGB itself, so + // it is safe to reference it here. + ColorSpace.Rgb srgb = (ColorSpace.Rgb) get(Named.SRGB); + for (double x = 0.0; x <= 1.0; x += 1 / 255.0) { + if (!compare(x, OETF, srgb.mOetf)) return false; + if (!compare(x, EOTF, srgb.mEotf)) return false; + } + return true; + } + /** + * Report whether this matrix is a special gray matrix. + * @param toXYZ A XYZD50 matrix. Skia uses a special form for a gray profile. + * @return true if this is a special gray matrix. + */ + private static boolean isGray(@NonNull @Size(9) float[] toXYZ) { + return toXYZ.length == 9 && toXYZ[1] == 0 && toXYZ[2] == 0 && toXYZ[3] == 0 + && toXYZ[5] == 0 && toXYZ[6] == 0 && toXYZ[7] == 0; + } + private static boolean compare(double point, @NonNull DoubleUnaryOperator a, + @NonNull DoubleUnaryOperator b) { + double rA = a.applyAsDouble(point); + double rB = b.applyAsDouble(point); + return Math.abs(rA - rB) <= 1e-3; + } + /** + * Computes whether the specified CIE xyY or XYZ primaries (with Y set to 1) form + * a wide color gamut. A color gamut is considered wide if its area is > 90% + * of the area of NTSC 1953 and if it contains the sRGB color gamut entirely. + * If the conditions above are not met, the color space is considered as having + * a wide color gamut if its range is larger than [0..1]. + * + * @param primaries RGB primaries in CIE xyY as an array of 6 floats + * @param min The minimum value of the color space's range + * @param max The minimum value of the color space's range + * @return True if the color space has a wide gamut, false otherwise + * + * @see #isWideGamut() + * @see #area(float[]) + */ + private static boolean isWideGamut(@NonNull @Size(6) float[] primaries, + float min, float max) { + return (area(primaries) / area(NTSC_1953_PRIMARIES) > 0.9f && + contains(primaries, SRGB_PRIMARIES)) || (min < 0.0f && max > 1.0f); + } + /** + * Computes the area of the triangle represented by a set of RGB primaries + * in the CIE xyY space. + * + * @param primaries The triangle's vertices, as RGB primaries in an array of 6 floats + * @return The area of the triangle + * + * @see #isWideGamut(float[], float, float) + */ + private static float area(@NonNull @Size(6) float[] primaries) { + float Rx = primaries[0]; + float Ry = primaries[1]; + float Gx = primaries[2]; + float Gy = primaries[3]; + float Bx = primaries[4]; + float By = primaries[5]; + float det = Rx * Gy + Ry * Bx + Gx * By - Gy * Bx - Ry * Gx - Rx * By; + float r = 0.5f * det; + return r < 0.0f ? -r : r; + } + /** + * Computes the cross product of two 2D vectors. + * + * @param ax The x coordinate of the first vector + * @param ay The y coordinate of the first vector + * @param bx The x coordinate of the second vector + * @param by The y coordinate of the second vector + * @return The result of a x b + */ + private static float cross(float ax, float ay, float bx, float by) { + return ax * by - ay * bx; + } + /** + * Decides whether a 2D triangle, identified by the 6 coordinates of its + * 3 vertices, is contained within another 2D triangle, also identified + * by the 6 coordinates of its 3 vertices. + * + * In the illustration below, we want to test whether the RGB triangle + * is contained within the triangle XYZ formed by the 3 vertices at + * the "+" locations. + * + * Y . + * . + . + * . .. + * . . + * . . + * . G + * * + * * * + * ** * + * * ** + * * * + * ** * + * * * + * * * + * ** * + * * * + * * ** + * ** * R ... + * * * ..... + * * ***** .. + * ** ************ . + + * B * ************ . X + * ......***** . + * ...... . . + * .. + * + . + * Z . + * + * RGB is contained within XYZ if all the following conditions are true + * (with "x" the cross product operator): + * + * --> --> + * GR x RX >= 0 + * --> --> + * RX x BR >= 0 + * --> --> + * RG x GY >= 0 + * --> --> + * GY x RG >= 0 + * --> --> + * RB x BZ >= 0 + * --> --> + * BZ x GB >= 0 + * + * @param p1 The enclosing triangle + * @param p2 The enclosed triangle + * @return True if the triangle p1 contains the triangle p2 + * + * @see #isWideGamut(float[], float, float) + */ + @SuppressWarnings("RedundantIfStatement") + private static boolean contains(@NonNull @Size(6) float[] p1, @NonNull @Size(6) float[] p2) { + // Translate the vertices p1 in the coordinates system + // with the vertices p2 as the origin + float[] p0 = new float[] { + p1[0] - p2[0], p1[1] - p2[1], + p1[2] - p2[2], p1[3] - p2[3], + p1[4] - p2[4], p1[5] - p2[5], + }; + // Check the first vertex of p1 + if (cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 || + cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0) { + return false; + } + // Check the second vertex of p1 + if (cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 || + cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0) { + return false; + } + // Check the third vertex of p1 + if (cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 || + cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0) { + return false; + } + return true; + } + /** + * Computes the primaries of a color space identified only by + * its RGB->XYZ transform matrix. This method assumes that the + * range of the color space is [0..1]. + * + * @param toXYZ The color space's 3x3 transform matrix to XYZ + * @return A new array of 6 floats containing the color space's + * primaries in CIE xyY + */ + @NonNull + @Size(6) + private static float[] computePrimaries(@NonNull @Size(9) float[] toXYZ) { + float[] r = mul3x3Float3(toXYZ, new float[] { 1.0f, 0.0f, 0.0f }); + float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, 1.0f, 0.0f }); + float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, 1.0f }); + float rSum = r[0] + r[1] + r[2]; + float gSum = g[0] + g[1] + g[2]; + float bSum = b[0] + b[1] + b[2]; + return new float[] { + r[0] / rSum, r[1] / rSum, + g[0] / gSum, g[1] / gSum, + b[0] / bSum, b[1] / bSum, + }; + } + /** + * Computes the white point of a color space identified only by + * its RGB->XYZ transform matrix. This method assumes that the + * range of the color space is [0..1]. + * + * @param toXYZ The color space's 3x3 transform matrix to XYZ + * @return A new array of 2 floats containing the color space's + * white point in CIE xyY + */ + @NonNull + @Size(2) + private static float[] computeWhitePoint(@NonNull @Size(9) float[] toXYZ) { + float[] w = mul3x3Float3(toXYZ, new float[] { 1.0f, 1.0f, 1.0f }); + float sum = w[0] + w[1] + w[2]; + return new float[] { w[0] / sum, w[1] / sum }; + } + /** + * Converts the specified RGB primaries point to xyY if needed. The primaries + * can be specified as an array of 6 floats (in CIE xyY) or 9 floats + * (in CIE XYZ). If no conversion is needed, the input array is copied. + * + * @param primaries The primaries in xyY or XYZ + * @return A new array of 6 floats containing the primaries in xyY + */ + @NonNull + @Size(6) + private static float[] xyPrimaries(@NonNull @Size(min = 6, max = 9) float[] primaries) { + float[] xyPrimaries = new float[6]; + // XYZ to xyY + if (primaries.length == 9) { + float sum; + sum = primaries[0] + primaries[1] + primaries[2]; + xyPrimaries[0] = primaries[0] / sum; + xyPrimaries[1] = primaries[1] / sum; + sum = primaries[3] + primaries[4] + primaries[5]; + xyPrimaries[2] = primaries[3] / sum; + xyPrimaries[3] = primaries[4] / sum; + sum = primaries[6] + primaries[7] + primaries[8]; + xyPrimaries[4] = primaries[6] / sum; + xyPrimaries[5] = primaries[7] / sum; + } else { + System.arraycopy(primaries, 0, xyPrimaries, 0, 6); + } + return xyPrimaries; + } + /** + * Converts the specified white point to xyY if needed. The white point + * can be specified as an array of 2 floats (in CIE xyY) or 3 floats + * (in CIE XYZ). If no conversion is needed, the input array is copied. + * + * @param whitePoint The white point in xyY or XYZ + * @return A new array of 2 floats containing the white point in xyY + */ + @NonNull + @Size(2) + private static float[] xyWhitePoint(@Size(min = 2, max = 3) float[] whitePoint) { + float[] xyWhitePoint = new float[2]; + // XYZ to xyY + if (whitePoint.length == 3) { + float sum = whitePoint[0] + whitePoint[1] + whitePoint[2]; + xyWhitePoint[0] = whitePoint[0] / sum; + xyWhitePoint[1] = whitePoint[1] / sum; + } else { + System.arraycopy(whitePoint, 0, xyWhitePoint, 0, 2); + } + return xyWhitePoint; + } + /** + * Computes the matrix that converts from RGB to XYZ based on RGB + * primaries and a white point, both specified in the CIE xyY space. + * The Y component of the primaries and white point is implied to be 1. + * + * @param primaries The RGB primaries in xyY, as an array of 6 floats + * @param whitePoint The white point in xyY, as an array of 2 floats + * @return A 3x3 matrix as a new array of 9 floats + */ + @NonNull + @Size(9) + private static float[] computeXYZMatrix( + @NonNull @Size(6) float[] primaries, + @NonNull @Size(2) float[] whitePoint) { + float Rx = primaries[0]; + float Ry = primaries[1]; + float Gx = primaries[2]; + float Gy = primaries[3]; + float Bx = primaries[4]; + float By = primaries[5]; + float Wx = whitePoint[0]; + float Wy = whitePoint[1]; + float oneRxRy = (1 - Rx) / Ry; + float oneGxGy = (1 - Gx) / Gy; + float oneBxBy = (1 - Bx) / By; + float oneWxWy = (1 - Wx) / Wy; + float RxRy = Rx / Ry; + float GxGy = Gx / Gy; + float BxBy = Bx / By; + float WxWy = Wx / Wy; + float BY = + ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) / + ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy)); + float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy); + float RY = 1 - GY - BY; + float RYRy = RY / Ry; + float GYGy = GY / Gy; + float BYBy = BY / By; + return new float[] { + RYRy * Rx, RY, RYRy * (1 - Rx - Ry), + GYGy * Gx, GY, GYGy * (1 - Gx - Gy), + BYBy * Bx, BY, BYBy * (1 - Bx - By) + }; + } + } + /** + * {@usesMathJax} + * + *

A connector transforms colors from a source color space to a destination + * color space.

+ * + *

A source color space is connected to a destination color space using the + * color transform \(C\) computed from their respective transforms noted + * \(T_{src}\) and \(T_{dst}\) in the following equation:

+ * + * $$C = T^{-1}_{dst} . T_{src}$$ + * + *

The transform \(C\) shown above is only valid when the source and + * destination color spaces have the same profile connection space (PCS). + * We know that instances of {@link ColorSpace} always use CIE XYZ as their + * PCS but their white points might differ. When they do, we must perform + * a chromatic adaptation of the color spaces' transforms. To do so, we + * use the von Kries method described in the documentation of {@link Adaptation}, + * using the CIE standard illuminant {@link ColorSpace#ILLUMINANT_D50 D50} + * as the target white point.

+ * + *

Example of conversion from {@link Named#SRGB sRGB} to + * {@link Named#DCI_P3 DCI-P3}:

+ * + *
+     * ColorSpace.Connector connector = ColorSpace.connect(
+     *         ColorSpace.get(ColorSpace.Named.SRGB),
+     *         ColorSpace.get(ColorSpace.Named.DCI_P3));
+     * float[] p3 = connector.transform(1.0f, 0.0f, 0.0f);
+     * // p3 contains { 0.9473, 0.2740, 0.2076 }
+     * 
+ * + * @see Adaptation + * @see ColorSpace#adapt(ColorSpace, float[], Adaptation) + * @see ColorSpace#adapt(ColorSpace, float[]) + * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent) + * @see ColorSpace#connect(ColorSpace, ColorSpace) + * @see ColorSpace#connect(ColorSpace, RenderIntent) + * @see ColorSpace#connect(ColorSpace) + */ + @AnyThread + public static class Connector { + @NonNull private final ColorSpace mSource; + @NonNull private final ColorSpace mDestination; + @NonNull private final ColorSpace mTransformSource; + @NonNull private final ColorSpace mTransformDestination; + @NonNull private final RenderIntent mIntent; + @NonNull @Size(3) private final float[] mTransform; + /** + * Creates a new connector between a source and a destination color space. + * + * @param source The source color space, cannot be null + * @param destination The destination color space, cannot be null + * @param intent The render intent to use when compressing gamuts + */ + Connector(@NonNull ColorSpace source, @NonNull ColorSpace destination, + @NonNull RenderIntent intent) { + this(source, destination, + source.getModel() == Model.RGB ? adapt(source, ILLUMINANT_D50_XYZ) : source, + destination.getModel() == Model.RGB ? + adapt(destination, ILLUMINANT_D50_XYZ) : destination, + intent, computeTransform(source, destination, intent)); + } + /** + * To connect between color spaces, we might need to use adapted transforms. + * This should be transparent to the user so this constructor takes the + * original source and destinations (returned by the getters), as well as + * possibly adapted color spaces used by transform(). + */ + private Connector( + @NonNull ColorSpace source, @NonNull ColorSpace destination, + @NonNull ColorSpace transformSource, @NonNull ColorSpace transformDestination, + @NonNull RenderIntent intent, @Nullable @Size(3) float[] transform) { + mSource = source; + mDestination = destination; + mTransformSource = transformSource; + mTransformDestination = transformDestination; + mIntent = intent; + mTransform = transform; + } + /** + * Computes an extra transform to apply in XYZ space depending on the + * selected rendering intent. + */ + @Nullable + private static float[] computeTransform(@NonNull ColorSpace source, + @NonNull ColorSpace destination, @NonNull RenderIntent intent) { + if (intent != RenderIntent.ABSOLUTE) return null; + boolean srcRGB = source.getModel() == Model.RGB; + boolean dstRGB = destination.getModel() == Model.RGB; + if (srcRGB && dstRGB) return null; + if (srcRGB || dstRGB) { + ColorSpace.Rgb rgb = (ColorSpace.Rgb) (srcRGB ? source : destination); + float[] srcXYZ = srcRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ; + float[] dstXYZ = dstRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ; + return new float[] { + srcXYZ[0] / dstXYZ[0], + srcXYZ[1] / dstXYZ[1], + srcXYZ[2] / dstXYZ[2], + }; + } + return null; + } + /** + * Returns the source color space this connector will convert from. + * + * @return A non-null instance of {@link ColorSpace} + * + * @see #getDestination() + */ + @NonNull + public ColorSpace getSource() { + return mSource; + } + /** + * Returns the destination color space this connector will convert to. + * + * @return A non-null instance of {@link ColorSpace} + * + * @see #getSource() + */ + @NonNull + public ColorSpace getDestination() { + return mDestination; + } + /** + * Returns the render intent this connector will use when mapping the + * source color space to the destination color space. + * + * @return A non-null {@link RenderIntent} + * + * @see RenderIntent + */ + public RenderIntent getRenderIntent() { + return mIntent; + } + /** + *

Transforms the specified color from the source color space + * to a color in the destination color space. This convenience + * method assumes a source color model with 3 components + * (typically RGB). To transform from color models with more than + * 3 components, such as {@link Model#CMYK CMYK}, use + * {@link #transform(float[])} instead.

+ * + * @param r The red component of the color to transform + * @param g The green component of the color to transform + * @param b The blue component of the color to transform + * @return A new array of 3 floats containing the specified color + * transformed from the source space to the destination space + * + * @see #transform(float[]) + */ + @NonNull + @Size(3) + public float[] transform(float r, float g, float b) { + return transform(new float[] { r, g, b }); + } + /** + *

Transforms the specified color from the source color space + * to a color in the destination color space.

+ * + * @param v A non-null array of 3 floats containing the value to transform + * and that will hold the result of the transform + * @return The v array passed as a parameter, containing the specified color + * transformed from the source space to the destination space + * + * @see #transform(float, float, float) + */ + @NonNull + @Size(min = 3) + public float[] transform(@NonNull @Size(min = 3) float[] v) { + float[] xyz = mTransformSource.toXyz(v); + if (mTransform != null) { + xyz[0] *= mTransform[0]; + xyz[1] *= mTransform[1]; + xyz[2] *= mTransform[2]; + } + return mTransformDestination.fromXyz(xyz); + } + /** + * Optimized connector for RGB->RGB conversions. + */ + private static class Rgb extends Connector { + @NonNull private final ColorSpace.Rgb mSource; + @NonNull private final ColorSpace.Rgb mDestination; + @NonNull private final float[] mTransform; + Rgb(@NonNull ColorSpace.Rgb source, @NonNull ColorSpace.Rgb destination, + @NonNull RenderIntent intent) { + super(source, destination, source, destination, intent, null); + mSource = source; + mDestination = destination; + mTransform = computeTransform(source, destination, intent); + } + @Override + public float[] transform(@NonNull @Size(min = 3) float[] rgb) { + rgb[0] = (float) mSource.mClampedEotf.applyAsDouble(rgb[0]); + rgb[1] = (float) mSource.mClampedEotf.applyAsDouble(rgb[1]); + rgb[2] = (float) mSource.mClampedEotf.applyAsDouble(rgb[2]); + mul3x3Float3(mTransform, rgb); + rgb[0] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[0]); + rgb[1] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[1]); + rgb[2] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[2]); + return rgb; + } + /** + *

Computes the color transform that connects two RGB color spaces.

+ * + *

We can only connect color spaces if they use the same profile + * connection space. We assume the connection space is always + * CIE XYZ but we maybe need to perform a chromatic adaptation to + * match the white points. If an adaptation is needed, we use the + * CIE standard illuminant D50. The unmatched color space is adapted + * using the von Kries transform and the {@link Adaptation#BRADFORD} + * matrix.

+ * + * @param source The source color space, cannot be null + * @param destination The destination color space, cannot be null + * @param intent The render intent to use when compressing gamuts + * @return An array of 9 floats containing the 3x3 matrix transform + */ + @NonNull + @Size(9) + private static float[] computeTransform( + @NonNull ColorSpace.Rgb source, + @NonNull ColorSpace.Rgb destination, + @NonNull RenderIntent intent) { + if (compare(source.mWhitePoint, destination.mWhitePoint)) { + // RGB->RGB using the PCS of both color spaces since they have the same + return mul3x3(destination.mInverseTransform, source.mTransform); + } else { + // RGB->RGB using CIE XYZ D50 as the PCS + float[] transform = source.mTransform; + float[] inverseTransform = destination.mInverseTransform; + float[] srcXYZ = xyYToXyz(source.mWhitePoint); + float[] dstXYZ = xyYToXyz(destination.mWhitePoint); + if (!compare(source.mWhitePoint, ILLUMINANT_D50)) { + float[] srcAdaptation = chromaticAdaptation( + Adaptation.BRADFORD.mTransform, srcXYZ, + Arrays.copyOf(ILLUMINANT_D50_XYZ, 3)); + transform = mul3x3(srcAdaptation, source.mTransform); + } + if (!compare(destination.mWhitePoint, ILLUMINANT_D50)) { + float[] dstAdaptation = chromaticAdaptation( + Adaptation.BRADFORD.mTransform, dstXYZ, + Arrays.copyOf(ILLUMINANT_D50_XYZ, 3)); + inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.mTransform)); + } + if (intent == RenderIntent.ABSOLUTE) { + transform = mul3x3Diag( + new float[] { + srcXYZ[0] / dstXYZ[0], + srcXYZ[1] / dstXYZ[1], + srcXYZ[2] / dstXYZ[2], + }, transform); + } + return mul3x3(inverseTransform, transform); + } + } + } + /** + * Returns the identity connector for a given color space. + * + * @param source The source and destination color space + * @return A non-null connector that does not perform any transform + * + * @see ColorSpace#connect(ColorSpace, ColorSpace) + */ + static Connector identity(ColorSpace source) { + return new Connector(source, source, RenderIntent.RELATIVE) { + @Override + public float[] transform(@NonNull @Size(min = 3) float[] v) { + return v; + } + }; + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/FontCache.java b/Vision/src/main/java/android/graphics/FontCache.java new file mode 100644 index 00000000..822d6a9a --- /dev/null +++ b/Vision/src/main/java/android/graphics/FontCache.java @@ -0,0 +1,26 @@ +package android.graphics; + +import io.github.humbleui.skija.Font; +import io.github.humbleui.skija.Typeface; + +import java.util.HashMap; + +class FontCache { + + private static HashMap> cache = new HashMap<>(); + + public static Font makeFont(Typeface theTypeface, float textSize) { + if(!cache.containsKey(theTypeface)) { + cache.put(theTypeface, new HashMap<>()); + } + + HashMap sizeCache = cache.get(theTypeface); + + if(!sizeCache.containsKey((int) (textSize * 1000))) { + sizeCache.put((int) (textSize * 1000), new Font(theTypeface, textSize)); + } + + return sizeCache.get((int) (textSize * 1000)); + } + +} diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java new file mode 100644 index 00000000..30314523 --- /dev/null +++ b/Vision/src/main/java/android/graphics/Paint.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2023 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 android.graphics; + +import io.github.humbleui.skija.PaintStrokeCap; +import io.github.humbleui.skija.PaintStrokeJoin; + +public class Paint { + + /* + * Copyright (C) 2006 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. + */ + + + /** + * The Style specifies if the primitive being drawn is filled, stroked, or + * both (in the same color). The default is FILL. + */ + public enum Style { + /** + * Geometry and text drawn with this style will be filled, ignoring all + * stroke-related settings in the paint. + */ + FILL (0), + /** + * Geometry and text drawn with this style will be stroked, respecting + * the stroke-related fields on the paint. + */ + STROKE (1), + /** + * Geometry and text drawn with this style will be both filled and + * stroked at the same time, respecting the stroke-related fields on + * the paint. This mode can give unexpected results if the geometry + * is oriented counter-clockwise. This restriction does not apply to + * either FILL or STROKE. + */ + FILL_AND_STROKE (2); + Style(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + /** + * The Cap specifies the treatment for the beginning and ending of + * stroked lines and paths. The default is BUTT. + */ + public enum Cap { + /** + * The stroke ends with the path, and does not project beyond it. + */ + BUTT (0), + /** + * The stroke projects out as a semicircle, with the center at the + * end of the path. + */ + ROUND (1), + /** + * The stroke projects out as a square, with the center at the end + * of the path. + */ + SQUARE (2); + private Cap(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + /** + * The Join specifies the treatment where lines and curve segments + * join on a stroked path. The default is MITER. + */ + public enum Join { + /** + * The outer edges of a join meet at a sharp angle + */ + MITER (0), + /** + * The outer edges of a join meet in a circular arc. + */ + ROUND (1), + /** + * The outer edges of a join meet with a straight line + */ + BEVEL (2); + private Join(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + /** + * Align specifies how drawText aligns its text relative to the + * [x,y] coordinates. The default is LEFT. + */ + public enum Align { + /** + * The text is drawn to the right of the x,y origin + */ + LEFT (0), + /** + * The text is drawn centered horizontally on the x,y origin + */ + CENTER (1), + /** + * The text is drawn to the left of the x,y origin + */ + RIGHT (2); + private Align(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + public final io.github.humbleui.skija.Paint thePaint; + + private Typeface typeface; + private float textSize; + + public Paint() { + thePaint = new io.github.humbleui.skija.Paint(); + } + + public Paint setColor(int color) { + thePaint.setColor(color); + return this; + } + + public Paint setAntiAlias(boolean value) { + thePaint.setAntiAlias(value); + return this; + } + + public Paint setStyle(Style style) { + // TODO: uh oh... + return this; + } + + public Paint setTypeface(Typeface typeface) { + this.typeface = typeface; + return this; + } + + public Paint setTextSize(float v) { + textSize = v; + return this; + } + + public void setStrokeJoin(Join join) { + PaintStrokeJoin strokeJoin = null; + + // conversion + switch(join) { + case MITER: + strokeJoin = PaintStrokeJoin.MITER; + break; + case ROUND: + strokeJoin = PaintStrokeJoin.ROUND; + break; + case BEVEL: + strokeJoin = PaintStrokeJoin.BEVEL; + break; + } + + thePaint.setStrokeJoin(strokeJoin); + } + public void setStrokeCap(Cap cap) { + PaintStrokeCap strokeCap = null; + + // conversion + switch(cap) { + case BUTT: + strokeCap = PaintStrokeCap.BUTT; + break; + case ROUND: + strokeCap = PaintStrokeCap.ROUND; + break; + case SQUARE: + strokeCap = PaintStrokeCap.SQUARE; + break; + } + + thePaint.setStrokeCap(strokeCap); + } + + public void setStrokeWidth(float width) { + thePaint.setStrokeWidth(width); + } + + public void setStrokeMiter(float miter) { + thePaint.setStrokeMiter(miter); + } + + // write getters here + public int getColor() { + return thePaint.getColor(); + } + + public boolean isAntiAlias() { + return thePaint.isAntiAlias(); + } + + public Style getStyle() { + return Style.FILL; // TODO: uh oh... + } + + public float getStrokeWidth() { + return thePaint.getStrokeWidth(); + } + + public Cap getStrokeCap() { + switch(thePaint.getStrokeCap()) { + case ROUND: + return Cap.ROUND; + case SQUARE: + return Cap.SQUARE; + default: + return Cap.BUTT; + } + } + + public Join getStrokeJoin() { + switch(thePaint.getStrokeJoin()) { + case ROUND: + return Join.ROUND; + case BEVEL: + return Join.BEVEL; + default: + return Join.MITER; + } + } + + public float getStrokeMiter() { + return thePaint.getStrokeMiter(); + } + + public Typeface getTypeface() { + return typeface; + } + + public float getTextSize() { + return textSize; + } + +} diff --git a/Vision/src/main/java/android/graphics/Rect.java b/Vision/src/main/java/android/graphics/Rect.java new file mode 100644 index 00000000..8fda9a67 --- /dev/null +++ b/Vision/src/main/java/android/graphics/Rect.java @@ -0,0 +1,616 @@ +/* + * Copyright (C) 2006 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 android.graphics; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.awt.*; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +/** + * Rect holds four integer coordinates for a rectangle. The rectangle is + * represented by the coordinates of its 4 edges (left, top, right bottom). + * These fields can be accessed directly. Use width() and height() to retrieve + * the rectangle's width and height. Note: most methods do not check to see that + * the coordinates are sorted correctly (i.e. left <= right and top <= bottom). + *

+ * Note that the right and bottom coordinates are exclusive. This means a Rect + * being drawn untransformed onto a {@link android.graphics.Canvas} will draw + * into the column and row described by its left and top coordinates, but not + * those of its bottom and right. + */ +public final class Rect { + public int left; + public int top; + public int right; + public int bottom; + /** + * A helper class for flattened rectange pattern recognition. A separate + * class to avoid an initialization dependency on a regular expression + * causing Rect to not be initializable with an ahead-of-time compilation + * scheme. + */ + private static final class UnflattenHelper { + private static final Pattern FLATTENED_PATTERN = Pattern.compile( + "(-?\\d+) (-?\\d+) (-?\\d+) (-?\\d+)"); + static Matcher getMatcher(String str) { + return FLATTENED_PATTERN.matcher(str); + } + } + /** + * Create a new empty Rect. All coordinates are initialized to 0. + */ + public Rect() {} + /** + * Create a new rectangle with the specified coordinates. Note: no range + * checking is performed, so the caller must ensure that left <= right and + * top <= bottom. + * + * @param left The X coordinate of the left side of the rectangle + * @param top The Y coordinate of the top of the rectangle + * @param right The X coordinate of the right side of the rectangle + * @param bottom The Y coordinate of the bottom of the rectangle + */ + public Rect(int left, int top, int right, int bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + /** + * Create a new rectangle, initialized with the values in the specified + * rectangle (which is left unmodified). + * + * @param r The rectangle whose coordinates are copied into the new + * rectangle. + */ + public Rect(@Nullable Rect r) { + if (r == null) { + left = top = right = bottom = 0; + } else { + left = r.left; + top = r.top; + right = r.right; + bottom = r.bottom; + } + } + /** + * @hide + */ + public Rect(@Nullable Insets r) { + if (r == null) { + left = top = right = bottom = 0; + } else { + left = r.left; + top = r.top; + right = r.right; + bottom = r.bottom; + } + } + /** + * Returns a copy of {@code r} if {@code r} is not {@code null}, or {@code null} otherwise. + * + * @hide + */ + @Nullable + public static Rect copyOrNull(@Nullable Rect r) { + return r == null ? null : new Rect(r); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Rect r = (Rect) o; + return left == r.left && top == r.top && right == r.right && bottom == r.bottom; + } + @Override + public int hashCode() { + int result = left; + result = 31 * result + top; + result = 31 * result + right; + result = 31 * result + bottom; + return result; + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(32); + sb.append("Rect("); sb.append(left); sb.append(", "); + sb.append(top); sb.append(" - "); sb.append(right); + sb.append(", "); sb.append(bottom); sb.append(")"); + return sb.toString(); + } + /** + * Return a string representation of the rectangle in a compact form. + */ + @NonNull + public String toShortString() { + return toShortString(new StringBuilder(32)); + } + + /** + * Return a string representation of the rectangle in a compact form. + * @hide + */ + @NonNull + public String toShortString(@NonNull StringBuilder sb) { + sb.setLength(0); + sb.append('['); sb.append(left); sb.append(','); + sb.append(top); sb.append("]["); sb.append(right); + sb.append(','); sb.append(bottom); sb.append(']'); + return sb.toString(); + } + /** + * Return a string representation of the rectangle in a well-defined format. + * + * @return Returns a new String of the form "left top right bottom" + */ + @NonNull + public String flattenToString() { + StringBuilder sb = new StringBuilder(32); + // WARNING: Do not change the format of this string, it must be + // preserved because Rects are saved in this flattened format. + sb.append(left); + sb.append(' '); + sb.append(top); + sb.append(' '); + sb.append(right); + sb.append(' '); + sb.append(bottom); + return sb.toString(); + } + + /** + * Print short representation to given writer. + * @hide + */ + public void printShortString(@NonNull PrintWriter pw) { + pw.print('['); pw.print(left); pw.print(','); + pw.print(top); pw.print("]["); pw.print(right); + pw.print(','); pw.print(bottom); pw.print(']'); + } + /** + * Returns true if the rectangle is empty (left >= right or top >= bottom) + */ + public final boolean isEmpty() { + return left >= right || top >= bottom; + } + /** + * @return {@code true} if the rectangle is valid (left <= right and top <= bottom). + * @hide + */ + public boolean isValid() { + return left <= right && top <= bottom; + } + /** + * @return the rectangle's width. This does not check for a valid rectangle + * (i.e. left <= right) so the result may be negative. + */ + public final int width() { + return right - left; + } + /** + * @return the rectangle's height. This does not check for a valid rectangle + * (i.e. top <= bottom) so the result may be negative. + */ + public final int height() { + return bottom - top; + } + + /** + * @return the horizontal center of the rectangle. If the computed value + * is fractional, this method returns the largest integer that is + * less than the computed value. + */ + public final int centerX() { + return (left + right) >> 1; + } + + /** + * @return the vertical center of the rectangle. If the computed value + * is fractional, this method returns the largest integer that is + * less than the computed value. + */ + public final int centerY() { + return (top + bottom) >> 1; + } + + /** + * @return the exact horizontal center of the rectangle as a float. + */ + public final float exactCenterX() { + return (left + right) * 0.5f; + } + + /** + * @return the exact vertical center of the rectangle as a float. + */ + public final float exactCenterY() { + return (top + bottom) * 0.5f; + } + /** + * Set the rectangle to (0,0,0,0) + */ + public void setEmpty() { + left = right = top = bottom = 0; + } + /** + * Set the rectangle's coordinates to the specified values. Note: no range + * checking is performed, so it is up to the caller to ensure that + * left <= right and top <= bottom. + * + * @param left The X coordinate of the left side of the rectangle + * @param top The Y coordinate of the top of the rectangle + * @param right The X coordinate of the right side of the rectangle + * @param bottom The Y coordinate of the bottom of the rectangle + */ + public void set(int left, int top, int right, int bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + /** + * Copy the coordinates from src into this rectangle. + * + * @param src The rectangle whose coordinates are copied into this + * rectangle. + */ + public void set(@NonNull Rect src) { + this.left = src.left; + this.top = src.top; + this.right = src.right; + this.bottom = src.bottom; + } + /** + * Offset the rectangle by adding dx to its left and right coordinates, and + * adding dy to its top and bottom coordinates. + * + * @param dx The amount to add to the rectangle's left and right coordinates + * @param dy The amount to add to the rectangle's top and bottom coordinates + */ + public void offset(int dx, int dy) { + left += dx; + top += dy; + right += dx; + bottom += dy; + } + /** + * Offset the rectangle to a specific (left, top) position, + * keeping its width and height the same. + * + * @param newLeft The new "left" coordinate for the rectangle + * @param newTop The new "top" coordinate for the rectangle + */ + public void offsetTo(int newLeft, int newTop) { + right += newLeft - left; + bottom += newTop - top; + left = newLeft; + top = newTop; + } + /** + * Inset the rectangle by (dx,dy). If dx is positive, then the sides are + * moved inwards, making the rectangle narrower. If dx is negative, then the + * sides are moved outwards, making the rectangle wider. The same holds true + * for dy and the top and bottom. + * + * @param dx The amount to add(subtract) from the rectangle's left(right) + * @param dy The amount to add(subtract) from the rectangle's top(bottom) + */ + public void inset(int dx, int dy) { + left += dx; + top += dy; + right -= dx; + bottom -= dy; + } + /** + * Insets the rectangle on all sides specified by the dimensions of the {@code insets} + * rectangle. + * @hide + * @param insets The rectangle specifying the insets on all side. + */ + public void inset(@NonNull Rect insets) { + left += insets.left; + top += insets.top; + right -= insets.right; + bottom -= insets.bottom; + } + /** + * Insets the rectangle on all sides specified by the dimensions of {@code insets}. + * + * @param insets The insets to inset the rect by. + */ + public void inset(@NonNull Insets insets) { + left += insets.left; + top += insets.top; + right -= insets.right; + bottom -= insets.bottom; + } + /** + * Insets the rectangle on all sides specified by the insets. + * + * @param left The amount to add from the rectangle's left + * @param top The amount to add from the rectangle's top + * @param right The amount to subtract from the rectangle's right + * @param bottom The amount to subtract from the rectangle's bottom + */ + public void inset(int left, int top, int right, int bottom) { + this.left += left; + this.top += top; + this.right -= right; + this.bottom -= bottom; + } + /** + * Returns true if (x,y) is inside the rectangle. The left and top are + * considered to be inside, while the right and bottom are not. This means + * that for a x,y to be contained: left <= x < right and top <= y < bottom. + * An empty rectangle never contains any point. + * + * @param x The X coordinate of the point being tested for containment + * @param y The Y coordinate of the point being tested for containment + * @return true iff (x,y) are contained by the rectangle, where containment + * means left <= x < right and top <= y < bottom + */ + public boolean contains(int x, int y) { + return left < right && top < bottom // check for empty first + && x >= left && x < right && y >= top && y < bottom; + } + /** + * Returns true iff the 4 specified sides of a rectangle are inside or equal + * to this rectangle. i.e. is this rectangle a superset of the specified + * rectangle. An empty rectangle never contains another rectangle. + * + * @param left The left side of the rectangle being tested for containment + * @param top The top of the rectangle being tested for containment + * @param right The right side of the rectangle being tested for containment + * @param bottom The bottom of the rectangle being tested for containment + * @return true iff the the 4 specified sides of a rectangle are inside or + * equal to this rectangle + */ + public boolean contains(int left, int top, int right, int bottom) { + // check for empty first + return this.left < this.right && this.top < this.bottom + // now check for containment + && this.left <= left && this.top <= top + && this.right >= right && this.bottom >= bottom; + } + /** + * Returns true iff the specified rectangle r is inside or equal to this + * rectangle. An empty rectangle never contains another rectangle. + * + * @param r The rectangle being tested for containment. + * @return true iff the specified rectangle r is inside or equal to this + * rectangle + */ + public boolean contains(@NonNull Rect r) { + // check for empty first + return this.left < this.right && this.top < this.bottom + // now check for containment + && left <= r.left && top <= r.top && right >= r.right && bottom >= r.bottom; + } + /** + * If the rectangle specified by left,top,right,bottom intersects this + * rectangle, return true and set this rectangle to that intersection, + * otherwise return false and do not change this rectangle. No check is + * performed to see if either rectangle is empty. Note: To just test for + * intersection, use {@link #intersects(Rect, Rect)}. + * + * @param left The left side of the rectangle being intersected with this + * rectangle + * @param top The top of the rectangle being intersected with this rectangle + * @param right The right side of the rectangle being intersected with this + * rectangle. + * @param bottom The bottom of the rectangle being intersected with this + * rectangle. + * @return true if the specified rectangle and this rectangle intersect + * (and this rectangle is then set to that intersection) else + * return false and do not change this rectangle. + */ + public boolean intersect(int left, int top, int right, int bottom) { + if (this.left < right && left < this.right && this.top < bottom && top < this.bottom) { + if (this.left < left) this.left = left; + if (this.top < top) this.top = top; + if (this.right > right) this.right = right; + if (this.bottom > bottom) this.bottom = bottom; + return true; + } + return false; + } + + /** + * If the specified rectangle intersects this rectangle, return true and set + * this rectangle to that intersection, otherwise return false and do not + * change this rectangle. No check is performed to see if either rectangle + * is empty. To just test for intersection, use intersects() + * + * @param r The rectangle being intersected with this rectangle. + * @return true if the specified rectangle and this rectangle intersect + * (and this rectangle is then set to that intersection) else + * return false and do not change this rectangle. + */ + public boolean intersect(@NonNull Rect r) { + return intersect(r.left, r.top, r.right, r.bottom); + } + /** + * If the specified rectangle intersects this rectangle, set this rectangle to that + * intersection, otherwise set this rectangle to the empty rectangle. + * @see #inset(int, int, int, int) but without checking if the rects overlap. + * @hide + */ + public void intersectUnchecked(@NonNull Rect other) { + left = Math.max(left, other.left); + top = Math.max(top, other.top); + right = Math.min(right, other.right); + bottom = Math.min(bottom, other.bottom); + } + /** + * If rectangles a and b intersect, return true and set this rectangle to + * that intersection, otherwise return false and do not change this + * rectangle. No check is performed to see if either rectangle is empty. + * To just test for intersection, use intersects() + * + * @param a The first rectangle being intersected with + * @param b The second rectangle being intersected with + * @return true iff the two specified rectangles intersect. If they do, set + * this rectangle to that intersection. If they do not, return + * false and do not change this rectangle. + */ + public boolean setIntersect(@NonNull Rect a, @NonNull Rect b) { + if (a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom) { + left = Math.max(a.left, b.left); + top = Math.max(a.top, b.top); + right = Math.min(a.right, b.right); + bottom = Math.min(a.bottom, b.bottom); + return true; + } + return false; + } + /** + * Returns true if this rectangle intersects the specified rectangle. + * In no event is this rectangle modified. No check is performed to see + * if either rectangle is empty. To record the intersection, use intersect() + * or setIntersect(). + * + * @param left The left side of the rectangle being tested for intersection + * @param top The top of the rectangle being tested for intersection + * @param right The right side of the rectangle being tested for + * intersection + * @param bottom The bottom of the rectangle being tested for intersection + * @return true iff the specified rectangle intersects this rectangle. In + * no event is this rectangle modified. + */ + public boolean intersects(int left, int top, int right, int bottom) { + return this.left < right && left < this.right && this.top < bottom && top < this.bottom; + } + /** + * Returns true iff the two specified rectangles intersect. In no event are + * either of the rectangles modified. To record the intersection, + * use {@link #intersect(Rect)} or {@link #setIntersect(Rect, Rect)}. + * + * @param a The first rectangle being tested for intersection + * @param b The second rectangle being tested for intersection + * @return true iff the two specified rectangles intersect. In no event are + * either of the rectangles modified. + */ + public static boolean intersects(@NonNull Rect a, @NonNull Rect b) { + return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; + } + /** + * Update this Rect to enclose itself and the specified rectangle. If the + * specified rectangle is empty, nothing is done. If this rectangle is empty + * it is set to the specified rectangle. + * + * @param left The left edge being unioned with this rectangle + * @param top The top edge being unioned with this rectangle + * @param right The right edge being unioned with this rectangle + * @param bottom The bottom edge being unioned with this rectangle + */ + public void union(int left, int top, int right, int bottom) { + if ((left < right) && (top < bottom)) { + if ((this.left < this.right) && (this.top < this.bottom)) { + if (this.left > left) this.left = left; + if (this.top > top) this.top = top; + if (this.right < right) this.right = right; + if (this.bottom < bottom) this.bottom = bottom; + } else { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + } + } + /** + * Update this Rect to enclose itself and the specified rectangle. If the + * specified rectangle is empty, nothing is done. If this rectangle is empty + * it is set to the specified rectangle. + * + * @param r The rectangle being unioned with this rectangle + */ + public void union(@NonNull Rect r) { + union(r.left, r.top, r.right, r.bottom); + } + + /** + * Update this Rect to enclose itself and the [x,y] coordinate. There is no + * check to see that this rectangle is non-empty. + * + * @param x The x coordinate of the point to add to the rectangle + * @param y The y coordinate of the point to add to the rectangle + */ + public void union(int x, int y) { + if (x < left) { + left = x; + } else if (x > right) { + right = x; + } + if (y < top) { + top = y; + } else if (y > bottom) { + bottom = y; + } + } + /** + * Swap top/bottom or left/right if there are flipped (i.e. left > right + * and/or top > bottom). This can be called if + * the edges are computed separately, and may have crossed over each other. + * If the edges are already correct (i.e. left <= right and top <= bottom) + * then nothing is done. + */ + public void sort() { + if (left > right) { + int temp = left; + left = right; + right = temp; + } + if (top > bottom) { + int temp = top; + top = bottom; + bottom = temp; + } + } + /** + * Splits this Rect into small rects of the same width. + * @hide + */ + public void splitVertically(@NonNull Rect ...splits) { + final int count = splits.length; + final int splitWidth = width() / count; + for (int i = 0; i < count; i++) { + final Rect split = splits[i]; + split.left = left + (splitWidth * i); + split.top = top; + split.right = split.left + splitWidth; + split.bottom = bottom; + } + } + /** + * Splits this Rect into small rects of the same height. + * @hide + */ + public void splitHorizontally(@NonNull Rect ...outSplits) { + final int count = outSplits.length; + final int splitHeight = height() / count; + for (int i = 0; i < count; i++) { + final Rect split = outSplits[i]; + split.left = left; + split.top = top + (splitHeight * i); + split.right = right; + split.bottom = split.top + splitHeight; + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Typeface.java b/Vision/src/main/java/android/graphics/Typeface.java new file mode 100644 index 00000000..cfb40b06 --- /dev/null +++ b/Vision/src/main/java/android/graphics/Typeface.java @@ -0,0 +1,32 @@ +package android.graphics; + +import io.github.humbleui.skija.Font; +import io.github.humbleui.skija.FontMgr; +import io.github.humbleui.skija.FontStyle; + +public class Typeface { + + public static Typeface DEFAULT = new Typeface(FontMgr.getDefault().matchFamilyStyle(null, FontStyle.NORMAL)); + public static Typeface DEFAULT_BOLD = new Typeface(FontMgr.getDefault().matchFamilyStyle(null, FontStyle.BOLD)); + public static Typeface DEFAULT_ITALIC = new Typeface(FontMgr.getDefault().matchFamilyStyle(null, FontStyle.ITALIC)); + + public io.github.humbleui.skija.Typeface theTypeface; + + public Typeface(long ptr) { + theTypeface = new io.github.humbleui.skija.Typeface(ptr); + } + + private Typeface(io.github.humbleui.skija.Typeface typeface) { + theTypeface = typeface; + } + + public Rect getBounds() { + return new Rect( + (int) theTypeface.getBounds().getLeft(), + (int) theTypeface.getBounds().getTop(), + (int) theTypeface.getBounds().getRight(), + (int) theTypeface.getBounds().getBottom() + ); + } + +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java deleted file mode 100644 index 0d2a0544..00000000 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java +++ /dev/null @@ -1,482 +0,0 @@ -/* - * Copyright (c) 2023 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; - -import android.util.Size; - -import java.util.ArrayList; -import java.util.List; - -import org.firstinspires.ftc.robotcore.external.ClassFactory; -import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; -import org.firstinspires.ftc.robotcore.internal.system.AppUtil; -import org.openftc.easyopencv.OpenCvCameraFactory; -import org.openftc.easyopencv.OpenCvWebcam; - -public abstract class VisionPortal -{ - public static final int DEFAULT_VIEW_CONTAINER_ID = AppUtil.getDefContext().getResources().getIdentifier("cameraMonitorViewId", "id", AppUtil.getDefContext().getPackageName()); - - /** - * StreamFormat is only applicable if using a webcam - */ - public enum StreamFormat - { - /** The only format that was supported historically; it is uncompressed but - * chroma subsampled and uses lots of bandwidth - this limits frame rate - * at higher resolutions and also limits the ability to use two cameras - * on the same bus to lower resolutions - */ - YUY2(OpenCvWebcam.StreamFormat.YUY2), - - /** Compressed motion JPEG stream format; allows for higher resolutions at - * full frame rate, and better ability to use two cameras on the same bus. - * Requires extra CPU time to run decompression routine. - */ - MJPEG(OpenCvWebcam.StreamFormat.MJPEG); - - final OpenCvWebcam.StreamFormat eocvStreamFormat; - - StreamFormat(OpenCvWebcam.StreamFormat eocvStreamFormat) - { - this.eocvStreamFormat = eocvStreamFormat; - } - } - - /** - * If you are using multiple vision portals with live previews concurrently, - * you need to split up the screen to make room for both portals - */ - public enum MultiPortalLayout - { - /** - * Divides the screen vertically - */ - VERTICAL(OpenCvCameraFactory.ViewportSplitMethod.VERTICALLY), - - /** - * Divides the screen horizontally - */ - HORIZONTAL(OpenCvCameraFactory.ViewportSplitMethod.HORIZONTALLY); - - private final OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod; - - MultiPortalLayout(OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod) - { - this.viewportSplitMethod = viewportSplitMethod; - } - } - - /** - * Split up the screen for using multiple vision portals with live views simultaneously - * @param numPortals the number of portals to create space for on the screen - * @param mpl the methodology for laying out the multiple live views on the screen - * @return an array of view IDs, whose elements may be passed to {@link Builder#setCameraMonitorViewId(int)} - */ - public static int[] makeMultiPortalView(int numPortals, MultiPortalLayout mpl) - { - return OpenCvCameraFactory.getInstance().splitLayoutForMultipleViewports( - DEFAULT_VIEW_CONTAINER_ID, numPortals, mpl.viewportSplitMethod - ); - } - - /** - * Create a VisionPortal for an internal camera using default configuration parameters, and - * skipping the use of the {@link Builder} pattern. - * @param cameraDirection the internal camera to use - * @param processors all the processors you want to inject into the portal - * @return a configured, ready to use VisionPortal - */ - public static VisionPortal easyCreateWithDefaults(BuiltinCameraDirection cameraDirection, VisionProcessor... processors) - { - return new Builder() - .setCamera(cameraDirection) - .addProcessors(processors) - .build(); - } - - /** - * Create a VisionPortal for a webcam using default configuration parameters, and - * skipping the use of the {@link Builder} pattern. - * @param processors all the processors you want to inject into the portal - * @return a configured, ready to use VisionPortal - */ - public static VisionPortal easyCreateWithDefaults(CameraName cameraName, VisionProcessor... processors) - { - return new Builder() - .setCamera(cameraName) - .addProcessors(processors) - .build(); - } - - public static class Builder - { - // STATIC ! - private static final ArrayList attachedProcessors = new ArrayList<>(); - - private CameraName camera; - private int cameraMonitorViewId = DEFAULT_VIEW_CONTAINER_ID; // 0 == none - private boolean autoStopLiveView = true; - private Size cameraResolution = new Size(640, 480); - private StreamFormat streamFormat = null; - private StreamFormat STREAM_FORMAT_DEFAULT = StreamFormat.YUY2; - private final List processors = new ArrayList<>(); - - /** - * Configure the portal to use a webcam - * @param camera the WebcamName of the camera to use - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder setCamera(CameraName camera) - { - this.camera = camera; - return this; - } - - /** - * Configure the portal to use an internal camera - * @param cameraDirection the internal camera to use - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder setCamera(BuiltinCameraDirection cameraDirection) - { - this.camera = ClassFactory.getInstance().getCameraManager().nameFromCameraDirection(cameraDirection); - return this; - } - - /** - * Configure the vision portal to stream from the camera in a certain image format - * THIS APPLIES TO WEBCAMS ONLY! - * @param streamFormat the desired streaming format - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder setStreamFormat(StreamFormat streamFormat) - { - this.streamFormat = streamFormat; - return this; - } - - /** - * Configure the vision portal to use (or not to use) a live camera preview - * @param enableLiveView whether or not to use a live preview - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder enableCameraMonitoring(boolean enableLiveView) - { - int viewId; - if (enableLiveView) - { - viewId = DEFAULT_VIEW_CONTAINER_ID; - } - else - { - viewId = 0; - } - return setCameraMonitorViewId(viewId); - } - - /** - * Configure whether the portal should automatically pause the live camera - * view if all attached processors are disabled; this can save computational resources - * @param autoPause whether to enable this feature or not - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder setAutoStopLiveView(boolean autoPause) - { - this.autoStopLiveView = autoPause; - return this; - } - - /** - * A more advanced version of {@link #enableCameraMonitoring(boolean)}; allows you - * to specify a specific view ID to use as a container, rather than just using the default one - * @param cameraMonitorViewId view ID of container for live view - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder setCameraMonitorViewId(int cameraMonitorViewId) - { - this.cameraMonitorViewId = cameraMonitorViewId; - return this; - } - - /** - * Specify the resolution in which to stream images from the camera. To find out what resolutions - * your camera supports, simply call this with some random numbers (e.g. new Size(4634, 11115)) - * and the error message will provide a list of supported resolutions. - * @param cameraResolution the resolution in which to stream images from the camera - * @return the {@link Builder} object, to allow for method chaining - */ - public Builder setCameraResolution(Size cameraResolution) - { - this.cameraResolution = cameraResolution; - return this; - } - - /** - * Send a {@link VisionProcessor} into this portal to allow it to process camera frames. - * @param processor the processor to attach - * @return the {@link Builder} object, to allow for method chaining - * @throws RuntimeException if the specified processor is already inside another portal - */ - public Builder addProcessor(VisionProcessor processor) - { - synchronized (attachedProcessors) - { - if (attachedProcessors.contains(processor)) - { - throw new RuntimeException("This VisionProcessor has already been attached to a VisionPortal, either a different one or perhaps even this same portal."); - } - else - { - attachedProcessors.add(processor); - } - } - - processors.add(processor); - return this; - } - - /** - * Send multiple {@link VisionProcessor}s into this portal to allow them to process camera frames. - * @param processors the processors to attach - * @return the {@link Builder} object, to allow for method chaining - * @throws RuntimeException if the specified processor is already inside another portal - */ - public Builder addProcessors(VisionProcessor... processors) - { - for (VisionProcessor p : processors) - { - addProcessor(p); - } - - return this; - } - - /** - * Actually create the {@link VisionPortal} i.e. spool up the camera and live view - * and begin sending image data to any attached {@link VisionProcessor}s - * @return a configured, ready to use portal - * @throws RuntimeException if you didn't specify what camera to use - * @throws IllegalStateException if you tried to set the stream format when not using a webcam - */ - public VisionPortal build() - { - if (camera == null) - { - throw new RuntimeException("You can't build a vision portal without setting a camera!"); - } - - if (streamFormat != null) - { - if (!camera.isWebcam() && !camera.isSwitchable()) - { - throw new IllegalStateException("setStreamFormat() may only be used with a webcam"); - } - } - else - { - // Only used with webcams, will be ignored for internal camera - streamFormat = STREAM_FORMAT_DEFAULT; - } - - return new VisionPortalImpl( - camera, cameraMonitorViewId, autoStopLiveView, cameraResolution, streamFormat, - processors.toArray(new VisionProcessor[processors.size()])); - } - } - - /** - * Enable or disable a {@link VisionProcessor} that is attached to this portal. - * Disabled processors are not passed new image data and do not consume any computational - * resources. Of course, they also don't give you any useful data when disabled. - * This takes effect immediately (on the next frame interval) - * @param processor the processor to enable or disable - * @param enabled should it be enabled or disabled? - * @throws IllegalArgumentException if the processor specified isn't inside this portal - */ - public abstract void setProcessorEnabled(VisionProcessor processor, boolean enabled); - - /** - * Queries whether a given processor is enabled - * @param processor the processor in question - * @return whether the processor in question is enabled - * @throws IllegalArgumentException if the processor specified isn't inside this portal - */ - public abstract boolean getProcessorEnabled(VisionProcessor processor); - - /** - * The various states that the camera may be in at any given time - */ - public enum CameraState - { - /** - * The camera device handle is being opened - */ - OPENING_CAMERA_DEVICE, - - /** - * The camera device handle has been opened and the camera - * is now ready to start streaming - */ - CAMERA_DEVICE_READY, - - /** - * The camera stream is starting - */ - STARTING_STREAM, - - /** - * The camera streaming session is in flight and providing image data - * to any attached {@link VisionProcessor}s - */ - STREAMING, - - /** - * The camera stream is being shut down - */ - STOPPING_STREAM, - - /** - * The camera device handle is being closed - */ - CLOSING_CAMERA_DEVICE, - - /** - * The camera device handle has been closed; you must create a new - * portal if you wish to use the camera again - */ - CAMERA_DEVICE_CLOSED, - - /** - * The camera was having a bad day and refused to cooperate with configuration for either - * opening the device handle or starting the streaming session - */ - ERROR - } - - /** - * Query the current state of the camera (e.g. is a streaming session in flight?) - * @return the current state of the camera - */ - public abstract CameraState getCameraState(); - - public abstract void saveNextFrameRaw(String filename); - - /** - * Stop the streaming session. This is an asynchronous call which does not take effect - * immediately. You may use {@link #getCameraState()} to monitor for when this command - * has taken effect. If you call {@link #resumeStreaming()} before the operation is complete, - * it will SYNCHRONOUSLY await completion of the stop command - * - * Stopping the streaming session is a good way to save computational resources if there may - * be long (e.g. 10+ second) periods of match play in which vision processing is not required. - * When streaming is stopped, no new image data is acquired from the camera and any attached - * {@link VisionProcessor}s will lie dormant until such time as {@link #resumeStreaming()} is called. - * - * Stopping and starting the stream can take a second or two, and thus is not advised for use - * cases where instantaneously enabling/disabling vision processing is required. - */ - public abstract void stopStreaming(); - - /** - * Resume the streaming session if previously stopped by {@link #stopStreaming()}. This is - * an asynchronous call which does not take effect immediately. If you call {@link #stopStreaming()} - * before the operation is complete, it will SYNCHRONOUSLY await completion of the resume command. - * - * See notes about use case on {@link #stopStreaming()} - */ - public abstract void resumeStreaming(); - - /** - * Temporarily stop the live view on the RC screen. This DOES NOT affect the ability to get - * a camera frame on the Driver Station's "Camera Stream" feature. - * - * This has no effect if you didn't set up a live view. - * - * Stopping the live view is recommended during competition to save CPU resources when - * a live view is not required for debugging purposes. - */ - public abstract void stopLiveView(); - - /** - * Start the live view again, if it was previously stopped with {@link #stopLiveView()} - * - * This has no effect if you didn't set up a live view. - */ - public abstract void resumeLiveView(); - - /** - * Get the current rate at which frames are passing through the vision portal - * (and all processors therein) per second - frames per second - * @return the current vision frame rate in frames per second - */ - public abstract float getFps(); - - /** - * Get a camera control handle - * ONLY APPLICABLE TO WEBCAMS - * @param controlType the type of control to get - * @return the requested control - * @throws UnsupportedOperationException if you are not using a webcam - */ - public abstract T getCameraControl(Class controlType); - - /** - * Switches the active camera to the indicated camera. - * ONLY APPLICABLE IF USING A SWITCHABLE WEBCAM - * @param webcamName the name of the to-be-activated camera - * @throws UnsupportedOperationException if you are not using a switchable webcam - */ - public abstract void setActiveCamera(WebcamName webcamName); - - /** - * Returns the name of the currently active camera - * ONLY APPLIES IF USING A SWITCHABLE WEBCAM - * @return the name of the currently active camera - * @throws UnsupportedOperationException if you are not using a switchable webcam - */ - public abstract WebcamName getActiveCamera(); - - /** - * Teardown everything prior to the end of the OpMode (perhaps to save resources) at which point - * it will be torn down automagically anyway. - * - * This will stop all vision related processing, shut down the camera, and remove the live view. - * A closed portal may not be re-opened: if you wish to use the camera again, you must make a new portal - */ - public abstract void close(); -} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java deleted file mode 100644 index 6cbd81a4..00000000 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java +++ /dev/null @@ -1,501 +0,0 @@ -/* - * Copyright (c) 2023 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; - -import android.graphics.Canvas; -import android.util.Size; - -import com.qualcomm.robotcore.util.RobotLog; - -import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; -import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationHelper; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationIdentity; -import org.firstinspires.ftc.robotcore.internal.camera.delegating.SwitchableCameraName; -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvCamera; -import org.openftc.easyopencv.OpenCvCameraFactory; -import org.openftc.easyopencv.OpenCvCameraRotation; -import org.openftc.easyopencv.OpenCvInternalCamera; -import org.openftc.easyopencv.OpenCvSwitchableWebcam; -import org.openftc.easyopencv.OpenCvWebcam; -import org.openftc.easyopencv.TimestampedOpenCvPipeline; - -public class VisionPortalImpl extends VisionPortal -{ - protected OpenCvCamera camera; - protected volatile CameraState cameraState = CameraState.CAMERA_DEVICE_CLOSED; - protected VisionProcessor[] processors; - protected volatile boolean[] processorsEnabled; - protected volatile CameraCalibration calibration; - protected final boolean autoPauseCameraMonitor; - protected final Object userStateMtx = new Object(); - protected final Size cameraResolution; - protected final StreamFormat webcamStreamFormat; - protected static final OpenCvCameraRotation CAMERA_ROTATION = OpenCvCameraRotation.SENSOR_NATIVE; - protected String captureNextFrame; - protected final Object captureFrameMtx = new Object(); - - public VisionPortalImpl(CameraName camera, int cameraMonitorViewId, boolean autoPauseCameraMonitor, Size cameraResolution, StreamFormat webcamStreamFormat, VisionProcessor[] processors) - { - this.processors = processors; - this.cameraResolution = cameraResolution; - this.webcamStreamFormat = webcamStreamFormat; - processorsEnabled = new boolean[processors.length]; - - for (int i = 0; i < processors.length; i++) - { - processorsEnabled[i] = true; - } - - this.autoPauseCameraMonitor = autoPauseCameraMonitor; - - createCamera(camera, cameraMonitorViewId); - startCamera(); - } - - protected void startCamera() - { - if (camera == null) - { - throw new IllegalStateException("This should never happen"); - } - - if (cameraResolution == null) // was the user a silly silly - { - throw new IllegalArgumentException("parameters.cameraResolution == null"); - } - - camera.setViewportRenderer(OpenCvCamera.ViewportRenderer.NATIVE_VIEW); - - if(!(camera instanceof OpenCvWebcam)) - { - camera.setViewportRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW); - } - - cameraState = CameraState.OPENING_CAMERA_DEVICE; - camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() - { - @Override - public void onOpened() - { - cameraState = CameraState.CAMERA_DEVICE_READY; - cameraState = CameraState.STARTING_STREAM; - - if (camera instanceof OpenCvWebcam) - { - ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); - } - else - { - camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); - } - - if (camera instanceof OpenCvWebcam) - { - CameraCalibrationIdentity identity = ((OpenCvWebcam) camera).getCalibrationIdentity(); - - if (identity != null) - { - calibration = CameraCalibrationHelper.getInstance().getCalibration(identity, cameraResolution.getWidth(), cameraResolution.getHeight()); - } - } - - camera.setPipeline(new ProcessingPipeline()); - cameraState = CameraState.STREAMING; - } - - @Override - public void onError(int errorCode) - { - cameraState = CameraState.ERROR; - RobotLog.ee("VisionPortalImpl", "Camera opening failed."); - } - }); - } - - protected void createCamera(CameraName cameraName, int cameraMonitorViewId) - { - if (cameraName == null) // was the user a silly silly - { - throw new IllegalArgumentException("parameters.camera == null"); - } - else if (cameraName.isWebcam()) // Webcams - { - if (cameraMonitorViewId != 0) - { - camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); - } - else - { - camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName); - } - } - else if (cameraName.isCameraDirection()) // Internal cameras - { - if (cameraMonitorViewId != 0) - { - camera = OpenCvCameraFactory.getInstance().createInternalCamera( - ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT, cameraMonitorViewId); - } - else - { - camera = OpenCvCameraFactory.getInstance().createInternalCamera( - ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT); - } - } - else if (cameraName.isSwitchable()) - { - SwitchableCameraName switchableCameraName = (SwitchableCameraName) cameraName; - if (switchableCameraName.allMembersAreWebcams()) { - CameraName[] members = switchableCameraName.getMembers(); - WebcamName[] webcamNames = new WebcamName[members.length]; - for (int i = 0; i < members.length; i++) - { - webcamNames[i] = (WebcamName) members[i]; - } - - if (cameraMonitorViewId != 0) - { - camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(cameraMonitorViewId, webcamNames); - } - else - { - camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(webcamNames); - } - } - else - { - throw new IllegalArgumentException("All members of a switchable camera name must be webcam names"); - } - } - else // ¯\_(ツ)_/¯ - { - throw new IllegalArgumentException("Unknown camera name"); - } - } - - @Override - public void setProcessorEnabled(VisionProcessor processor, boolean enabled) - { - int numProcessorsEnabled = 0; - boolean ok = false; - - for (int i = 0; i < processors.length; i++) - { - if (processor == processors[i]) - { - processorsEnabled[i] = enabled; - ok = true; - } - - if (processorsEnabled[i]) - { - numProcessorsEnabled++; - } - } - - if (ok) - { - if (autoPauseCameraMonitor) - { - if (numProcessorsEnabled == 0) - { - camera.pauseViewport(); - } - else - { - camera.resumeViewport(); - } - } - } - else - { - throw new IllegalArgumentException("Processor not attached to this helper!"); - } - } - - @Override - public boolean getProcessorEnabled(VisionProcessor processor) - { - for (int i = 0; i < processors.length; i++) - { - if (processor == processors[i]) - { - return processorsEnabled[i]; - } - } - - throw new IllegalArgumentException("Processor not attached to this helper!"); - } - - @Override - public CameraState getCameraState() - { - return cameraState; - } - - @Override - public void setActiveCamera(WebcamName webcamName) - { - if (camera instanceof OpenCvSwitchableWebcam) - { - ((OpenCvSwitchableWebcam) camera).setActiveCamera(webcamName); - } - else - { - throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); - } - } - - @Override - public WebcamName getActiveCamera() - { - if (camera instanceof OpenCvSwitchableWebcam) - { - return ((OpenCvSwitchableWebcam) camera).getActiveCamera(); - } - else - { - throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); - } - } - - @Override - public T getCameraControl(Class controlType) - { - if (cameraState == CameraState.STREAMING) - { - if (camera instanceof OpenCvWebcam) - { - return ((OpenCvWebcam) camera).getControl(controlType); - } - else - { - throw new UnsupportedOperationException("Getting controls is only supported for webcams"); - } - } - else - { - throw new IllegalStateException("You cannot use camera controls until the camera is streaming"); - } - } - - class ProcessingPipeline extends TimestampedOpenCvPipeline - { - @Override - public void init(Mat firstFrame) - { - for (VisionProcessor processor : processors) - { - processor.init(firstFrame.width(), firstFrame.height(), calibration); - } - } - - @Override - public Mat processFrame(Mat input, long captureTimeNanos) - { - synchronized (captureFrameMtx) - { - if (captureNextFrame != null) - { - saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); - } - - captureNextFrame = null; - } - - Object[] processorDrawCtxes = new Object[processors.length]; // cannot re-use frome to frame - - for (int i = 0; i < processors.length; i++) - { - if (processorsEnabled[i]) - { - processorDrawCtxes[i] = processors[i].processFrame(input, captureTimeNanos); - } - } - - requestViewportDrawHook(processorDrawCtxes); - - return input; - } - - @Override - public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) - { - Object[] ctx = (Object[]) userContext; - - for (int i = 0; i < processors.length; i++) - { - if (processorsEnabled[i]) - { - processors[i].onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, ctx[i]); - } - } - } - } - - @Override - public void saveNextFrameRaw(String filepath) - { - synchronized (captureFrameMtx) - { - captureNextFrame = filepath; - } - } - - @Override - public void stopStreaming() - { - synchronized (userStateMtx) - { - if (cameraState == CameraState.STREAMING || cameraState == CameraState.STARTING_STREAM) - { - cameraState = CameraState.STOPPING_STREAM; - new Thread(() -> - { - synchronized (userStateMtx) - { - camera.stopStreaming(); - cameraState = CameraState.CAMERA_DEVICE_READY; - } - }).start(); - } - else if (cameraState == CameraState.STOPPING_STREAM - || cameraState == CameraState.CAMERA_DEVICE_READY - || cameraState == CameraState.CLOSING_CAMERA_DEVICE) - { - // be idempotent - } - else - { - throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); - } - } - } - - @Override - public void resumeStreaming() - { - synchronized (userStateMtx) - { - if (cameraState == CameraState.CAMERA_DEVICE_READY || cameraState == CameraState.STOPPING_STREAM) - { - cameraState = CameraState.STARTING_STREAM; - new Thread(() -> - { - synchronized (userStateMtx) - { - if (camera instanceof OpenCvWebcam) - { - ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); - } - else - { - camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); - } - cameraState = CameraState.STREAMING; - } - }).start(); - } - else if (cameraState == CameraState.STREAMING - || cameraState == CameraState.STARTING_STREAM - || cameraState == CameraState.OPENING_CAMERA_DEVICE) // we start streaming automatically after we open - { - // be idempotent - } - else - { - throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); - } - } - } - - @Override - public void stopLiveView() - { - OpenCvCamera cameraSafe = camera; - - if (cameraSafe != null) - { - camera.pauseViewport(); - } - } - - @Override - public void resumeLiveView() - { - OpenCvCamera cameraSafe = camera; - - if (cameraSafe != null) - { - camera.resumeViewport(); - } - } - - @Override - public float getFps() - { - OpenCvCamera cameraSafe = camera; - - if (cameraSafe != null) - { - return cameraSafe.getFps(); - } - else - { - return 0; - } - } - - @Override - public void close() - { - synchronized (userStateMtx) - { - cameraState = CameraState.CLOSING_CAMERA_DEVICE; - - if (camera != null) - { - camera.closeCameraDeviceAsync(() -> cameraState = CameraState.CAMERA_DEVICE_CLOSED); - } - - camera = null; - } - } -} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java index ad9be96b..484aca1e 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java @@ -36,8 +36,8 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.Typeface; +import android.graphics.Typeface; import org.opencv.calib3d.Calib3d; import org.opencv.core.Mat; import org.opencv.core.MatOfDouble; diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java index f6ba3161..f30f3b3d 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java @@ -36,7 +36,6 @@ import org.firstinspires.ftc.robotcore.external.matrices.VectorF; import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; import org.firstinspires.ftc.robotcore.external.navigation.Quaternion; -import org.firstinspires.ftc.vision.VisionPortal; import java.util.ArrayList; diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java index 97a39cbc..c7728504 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java @@ -34,10 +34,8 @@ package org.firstinspires.ftc.vision.apriltag; import android.graphics.Canvas; -import android.util.Log; import com.qualcomm.robotcore.util.MovingStatistics; -import com.qualcomm.robotcore.util.RobotLog; import org.firstinspires.ftc.robotcore.external.matrices.GeneralMatrixF; import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; @@ -57,12 +55,14 @@ import org.opencv.imgproc.Imgproc; import org.openftc.apriltag.AprilTagDetectorJNI; import org.openftc.apriltag.ApriltagDetectionJNI; +import org.slf4j.Logger; import java.util.ArrayList; public class AprilTagProcessorImpl extends AprilTagProcessor { public static final String TAG = "AprilTagProcessorImpl"; + Logger logger = org.slf4j.LoggerFactory.getLogger(TAG); private long nativeApriltagPtr; private Mat grey = new Mat(); @@ -143,7 +143,7 @@ public void init(int width, int height, CameraCalibration calibration) cx = calibration.principalPointX; cy = calibration.principalPointY; - Log.d(TAG, String.format("User did not provide a camera calibration; but we DO have a built in calibration we can use.\n [%dx%d] (may be scaled) %s\nfx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", + logger.warn(String.format("User did not provide a camera calibration; but we DO have a built in calibration we can use.\n [%dx%d] (may be scaled) %s\nfx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", calibration.getSize().getWidth(), calibration.getSize().getHeight(), calibration.getIdentity().toString(), fx, fy, cx, cy)); } else if (fx == 0 && fy == 0 && cx == 0 && cy == 0) @@ -151,17 +151,17 @@ else if (fx == 0 && fy == 0 && cx == 0 && cy == 0) // set it to *something* so we don't crash the native code String warning = "User did not provide a camera calibration, nor was a built-in calibration found for this camera; 6DOF pose data will likely be inaccurate."; - Log.d(TAG, warning); - RobotLog.addGlobalWarningMessage(warning); + logger.warn(TAG, warning); + // RobotLog.addGlobalWarningMessage(warning); fx = 578.272; fy = 578.272; - cx = width/2; - cy = height/2; + cx = (double) width /2; + cy = (double) height /2; } else { - Log.d(TAG, String.format("User provided their own camera calibration fx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", + logger.warn(String.format("User provided their own camera calibration fx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f", fx, fy, cx, cy)); } diff --git a/build.gradle b/build.gradle index e582e633..369c6678 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,8 @@ buildscript { slf4j_version = "1.7.32" log4j_version = "2.17.1" opencv_version = "4.5.5-1" - apriltag_plugin_version = "1.2.1" + apriltag_plugin_version = "2.0.0-A" + skija_version = "0.109.2" classgraph_version = "4.8.108" opencsv_version = "5.5.2" @@ -42,6 +43,8 @@ allprojects { mavenCentral() mavenLocal() + google() + maven { url "https://jitpack.io" } maven { url 'https://maven.openimaj.org/' } maven { url 'https://maven.ecs.soton.ac.uk/content/repositories/thirdparty/' } @@ -72,4 +75,4 @@ allprojects { file.delete() } } -} +} \ No newline at end of file From 5a6aedb388bbde9ed294f121e3084b644924640f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 9 Aug 2023 19:52:04 -0600 Subject: [PATCH 12/46] Add new EOCV/FTC SDK vision classes & redesign EOCV-Sim viewport architecture --- Common/src/main/java/android/util/Size.java | 2 + ...Viewport.java => SwingOpenCvViewport.java} | 292 +++++++++--------- .../pipeline/handler/PipelineHandler.kt | 17 + .../handler/SpecificPipelineHandler.kt | 4 + .../eventloop/opmode/OpModePipelineHandler.kt | 4 + .../ftc/teamcode/AprilTagTestOpMode.java | 30 ++ .../robotcore/hardware/HardwareMap.java | 36 +++ .../deltacv/vision/util/FrameQueue.java | 2 + .../camera/CameraCharacteristics.java | 159 ++++++++++ .../external/hardware/camera/CameraName.java | 120 +++++++ .../external/hardware/camera/WebcamName.java | 4 + .../ftc/vision/VisionPortalImpl.java | 2 + .../main/java/org/opencv/android/Utils.java | 2 + .../org/openftc/easyopencv/OpenCvCamera.java | 2 + .../openftc/easyopencv/OpenCvCameraBase.java | 2 + .../easyopencv/OpenCvCameraRotation.java | 2 + .../easyopencv/OpenCvViewRenderer.java | 2 + .../openftc/easyopencv/OpenCvViewport.java | 2 + .../PipelineRecordingParameters.java | 2 + .../openftc/easyopencv/QueueOpenCvCamera.java | 2 + .../openftc/easyopencv/ViewportPipeline.java | 36 +++ 21 files changed, 578 insertions(+), 146 deletions(-) create mode 100644 Common/src/main/java/android/util/Size.java rename EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/{Viewport.java => SwingOpenCvViewport.java} (97%) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt create mode 100644 EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java create mode 100644 Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java create mode 100644 Vision/src/main/java/org/opencv/android/Utils.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java diff --git a/Common/src/main/java/android/util/Size.java b/Common/src/main/java/android/util/Size.java new file mode 100644 index 00000000..2d5d7631 --- /dev/null +++ b/Common/src/main/java/android/util/Size.java @@ -0,0 +1,2 @@ +package android.util;public class Size { +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java similarity index 97% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java index 3eceb84e..c9e6e165 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java @@ -1,146 +1,146 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.gui.component; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.util.MatPoster; -import com.github.serivesmejia.eocvsim.util.image.DynamicBufferedImageRecycler; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.qualcomm.robotcore.util.Range; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; - -public class Viewport extends JPanel { - - public final ImageX image = new ImageX(); - public final MatPoster matPoster; - - private Mat lastVisualizedMat = null; - private Mat lastVisualizedScaledMat = null; - - private final DynamicBufferedImageRecycler buffImgGiver = new DynamicBufferedImageRecycler(); - - private volatile BufferedImage lastBuffImage; - private volatile Dimension lastDimension; - - private double scale; - - private final EOCVSim eocvSim; - - Logger logger = LoggerFactory.getLogger(getClass()); - - public Viewport(EOCVSim eocvSim, int maxQueueItems) { - super(new GridBagLayout()); - - this.eocvSim = eocvSim; - setViewportScale(eocvSim.configManager.getConfig().zoom); - - add(image, new GridBagConstraints()); - - matPoster = new MatPoster("Viewport", maxQueueItems); - attachToPoster(matPoster); - } - - public void postMatAsync(Mat mat) { - matPoster.post(mat); - } - - public synchronized void postMat(Mat mat) { - if(lastVisualizedMat == null) lastVisualizedMat = new Mat(); //create latest mat if we have null reference - if(lastVisualizedScaledMat == null) lastVisualizedScaledMat = new Mat(); //create last scaled mat if null reference - - JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this); - - mat.copyTo(lastVisualizedMat); //copy given mat to viewport latest one - - double wScale = (double) frame.getWidth() / mat.width(); - double hScale = (double) frame.getHeight() / mat.height(); - - double calcScale = (wScale / hScale) * 1.5; - double finalScale = Math.max(0.1, Math.min(3, scale * calcScale)); - - Size size = new Size(mat.width() * finalScale, mat.height() * finalScale); - Imgproc.resize(mat, lastVisualizedScaledMat, size, 0.0, 0.0, Imgproc.INTER_AREA); //resize mat to lastVisualizedScaledMat - - Dimension newDimension = new Dimension(lastVisualizedScaledMat.width(), lastVisualizedScaledMat.height()); - - if(lastBuffImage != null) buffImgGiver.returnBufferedImage(lastBuffImage); - - lastBuffImage = buffImgGiver.giveBufferedImage(newDimension, 2); - lastDimension = newDimension; - - CvUtil.matToBufferedImage(lastVisualizedScaledMat, lastBuffImage); - - image.setImage(lastBuffImage); //set buff image to ImageX component - - eocvSim.configManager.getConfig().zoom = scale; //store latest scale if store setting turned on - } - - public void attachToPoster(MatPoster poster) { - poster.addPostable((m) -> { - try { - Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2BGR); - postMat(m); - } catch(Exception ex) { - logger.error("Couldn't visualize last mat", ex); - } - }); - } - - public void flush() { - buffImgGiver.flushAll(); - } - - public void stop() { - matPoster.stop(); - flush(); - } - - public synchronized void setViewportScale(double scale) { - scale = Range.clip(scale, 0.1, 3); - - boolean scaleChanged = this.scale != scale; - this.scale = scale; - - if(lastVisualizedMat != null && scaleChanged) - postMat(lastVisualizedMat); - - } - - public synchronized Mat getLastVisualizedMat() { - return lastVisualizedMat; - } - - public synchronized double getViewportScale() { - return scale; - } - -} +/* + * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.gui.component; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.util.MatPoster; +import com.github.serivesmejia.eocvsim.util.image.DynamicBufferedImageRecycler; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import com.qualcomm.robotcore.util.Range; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class Viewport extends JPanel { + + public final ImageX image = new ImageX(); + public final MatPoster matPoster; + + private Mat lastVisualizedMat = null; + private Mat lastVisualizedScaledMat = null; + + private final DynamicBufferedImageRecycler buffImgGiver = new DynamicBufferedImageRecycler(); + + private volatile BufferedImage lastBuffImage; + private volatile Dimension lastDimension; + + private double scale; + + private final EOCVSim eocvSim; + + Logger logger = LoggerFactory.getLogger(getClass()); + + public Viewport(EOCVSim eocvSim, int maxQueueItems) { + super(new GridBagLayout()); + + this.eocvSim = eocvSim; + setViewportScale(eocvSim.configManager.getConfig().zoom); + + add(image, new GridBagConstraints()); + + matPoster = new MatPoster("Viewport", maxQueueItems); + attachToPoster(matPoster); + } + + public void postMatAsync(Mat mat) { + matPoster.post(mat); + } + + public synchronized void postMat(Mat mat) { + if(lastVisualizedMat == null) lastVisualizedMat = new Mat(); //create latest mat if we have null reference + if(lastVisualizedScaledMat == null) lastVisualizedScaledMat = new Mat(); //create last scaled mat if null reference + + JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this); + + mat.copyTo(lastVisualizedMat); //copy given mat to viewport latest one + + double wScale = (double) frame.getWidth() / mat.width(); + double hScale = (double) frame.getHeight() / mat.height(); + + double calcScale = (wScale / hScale) * 1.5; + double finalScale = Math.max(0.1, Math.min(3, scale * calcScale)); + + Size size = new Size(mat.width() * finalScale, mat.height() * finalScale); + Imgproc.resize(mat, lastVisualizedScaledMat, size, 0.0, 0.0, Imgproc.INTER_AREA); //resize mat to lastVisualizedScaledMat + + Dimension newDimension = new Dimension(lastVisualizedScaledMat.width(), lastVisualizedScaledMat.height()); + + if(lastBuffImage != null) buffImgGiver.returnBufferedImage(lastBuffImage); + + lastBuffImage = buffImgGiver.giveBufferedImage(newDimension, 2); + lastDimension = newDimension; + + CvUtil.matToBufferedImage(lastVisualizedScaledMat, lastBuffImage); + + image.setImage(lastBuffImage); //set buff image to ImageX component + + eocvSim.configManager.getConfig().zoom = scale; //store latest scale if store setting turned on + } + + public void attachToPoster(MatPoster poster) { + poster.addPostable((m) -> { + try { + Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2BGR); + postMat(m); + } catch(Exception ex) { + logger.error("Couldn't visualize last mat", ex); + } + }); + } + + public void flush() { + buffImgGiver.flushAll(); + } + + public void stop() { + matPoster.stop(); + flush(); + } + + public synchronized void setViewportScale(double scale) { + scale = Range.clip(scale, 0.1, 3); + + boolean scaleChanged = this.scale != scale; + this.scale = scale; + + if(lastVisualizedMat != null && scaleChanged) + postMat(lastVisualizedMat); + + } + + public synchronized Mat getLastVisualizedMat() { + return lastVisualizedMat; + } + + public synchronized double getViewportScale() { + return scale; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt new file mode 100644 index 00000000..359ce028 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt @@ -0,0 +1,17 @@ +package com.github.serivesmejia.eocvsim.pipeline.util + +import com.github.serivesmejia.eocvsim.input.InputSource +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline + +interface PipelineHandler { + + fun preInit(pipeline: OpenCvPipeline, telemetry: Telemetry) + + fun init() + + fun processFrame(currentInputSource: InputSource?) + + fun onChange() + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt new file mode 100644 index 00000000..4f2e2f8f --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt @@ -0,0 +1,4 @@ +package com.github.serivesmejia.eocvsim.pipeline.handler + +class SpecificPipelineHandler { +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt new file mode 100644 index 00000000..bca16ec3 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -0,0 +1,4 @@ +package com.qualcomm.robotcore.eventloop.opmode + +class OpModePipelineHandler { +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java new file mode 100644 index 00000000..5c8f0375 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java @@ -0,0 +1,30 @@ +package org.firstinspires.ftc.teamcode; + +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvPipeline; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +public class AprilTagProcessorTest extends TimestampedOpenCvPipeline { + + AprilTagProcessor processor = new AprilTagProcessor.Builder() + .setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) + .setDrawCubeProjection(true) + .setDrawTagID(true) + .setOutputUnits(DistanceUnit.CM, AngleUnit.DEGREES) + .build(); + + @Override + public void init(Mat firstFrame) { + processor.init(firstFrame.width(), firstFrame.height(), null); + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) { + processor.processFrame(input, captureTimeNanos); + return input; + } + +} diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java new file mode 100644 index 00000000..fe498118 --- /dev/null +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java @@ -0,0 +1,36 @@ +package com.qualcomm.robotcore.external.hardware; + +import com.qualcomm.robotcore.hardware.camera.CameraName; +import io.github.deltacv.vision.util.FrameQueue; +import org.openftc.easyopencv.QueueOpenCvCamera; + +public class HardwareMap { + + private FrameQueue cameraFramesQueue; + private FrameQueue viewportOutputQueue; + + + public HardwareMap(FrameQueue cameraFramesQueue, FrameQueue viewportOutputQueue) { + this.cameraFramesQueue = cameraFramesQueue; + this.viewportOutputQueue = viewportOutputQueue; + } + + public static boolean hasSuperclass(Class clazz, Class superClass) { + try { + clazz.asSubclass(superClass); + return true; + } catch (ClassCastException ex) { + return false; + } + } + + @SuppressWarnings("unchecked") + public T get(Class classType, String deviceName) { + if(hasSuperclass(classType, CameraName.class)) { + return (T) new QueueOpenCvCamera(cameraFramesQueue, viewportOutputQueue); + } + + return null; + } + +} \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java b/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java new file mode 100644 index 00000000..29682cd9 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java @@ -0,0 +1,2 @@ +package io.github.deltacv.vision.util;public class FrameQueue { +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java new file mode 100644 index 00000000..29f1f12b --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java @@ -0,0 +1,159 @@ +/* +Copyright (c) 2017 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.external.hardware.camera; + + +import com.qualcomm.robotcore.util.ElapsedTime; + +import org.firstinspires.ftc.robotcore.external.android.util.Size; +import org.firstinspires.ftc.robotcore.internal.system.Misc; + +import java.util.List; + +/** + * THIS INTERFACE IS EXPERIMENTAL. Its form and function may change in whole or in part + * before being finalized for production use. Caveat emptor. + * + * Metadata regarding the configuration of video streams that a camera might produce. + * Modelled after {link android.hardware.camera2.params.StreamConfigurationMap}, though + * significantly simplified here. + */ +@SuppressWarnings("WeakerAccess") +public interface CameraCharacteristics +{ + //---------------------------------------------------------------------------------------------- + // Accessing + //---------------------------------------------------------------------------------------------- + + /** + * Get the image {@code format} output formats in this camera + */ + int[] getAndroidFormats(); + + /** + * Get a list of sizes compatible with the requested image {@code format}. + * + *

The {@code format} should be a supported format (one of the formats returned by + * {@link #getAndroidFormats}).

+ * + * @param androidFormat an image format from {@link ImageFormat} or {@link PixelFormat} + * @return + * an array of supported sizes, + * or {@code null} if the {@code format} is not a supported output + * + * @see ImageFormat + * @see PixelFormat + * @see #getAndroidFormats + */ + Size[] getSizes(int androidFormat); + + /** Gets the device-recommended optimum size for the indicated format */ + Size getDefaultSize(int androidFormat); + + /** + * Get the minimum frame duration for the format/size combination (in nanoseconds). + * + *

{@code format} should be one of the ones returned by {@link #getAndroidFormats()}.

+ *

{@code size} should be one of the ones returned by {@link #getSizes(int)}.

+ * + * @param androidFormat an image format from {@link ImageFormat} or {@link PixelFormat} + * @param size an output-compatible size + * @return a minimum frame duration {@code >} 0 in nanoseconds, or + * 0 if the minimum frame duration is not available. + * + * @throws IllegalArgumentException if {@code format} or {@code size} was not supported + * @throws NullPointerException if {@code size} was {@code null} + * + * @see ImageFormat + * @see PixelFormat + */ + long getMinFrameDuration(int androidFormat, Size size); + + /** + * Returns the maximum fps rate supported for the given format. + * + * @return the maximum fps rate supported for the given format. + */ + int getMaxFramesPerSecond(int androidFormat, Size size); + + + + class CameraMode + { + public final int androidFormat; + public final Size size; + public final long nsFrameDuration; + public final int fps; + public final boolean isDefaultSize; // not used in equalitor + + public CameraMode(int androidFormat, Size size, long nsFrameDuration, boolean isDefaultSize) + { + this.androidFormat = androidFormat; + this.size = size; + this.nsFrameDuration = nsFrameDuration; + this.fps = (int) (ElapsedTime.SECOND_IN_NANO / nsFrameDuration); + this.isDefaultSize = isDefaultSize; + } + + @Override public String toString() + { + return Misc.formatInvariant("CameraMode(format=%d %dx%d fps=%d)", androidFormat, size.getWidth(), size.getHeight(), fps); + } + + @Override public boolean equals(Object o) + { + if (o instanceof CameraMode) + { + CameraMode them = (CameraMode)o; + return androidFormat == them.androidFormat + && size.equals(them.size) + && nsFrameDuration == them.nsFrameDuration + && fps == them.fps; + } + else + return super.equals(o); + } + + @Override public int hashCode() + { + return Integer.valueOf(androidFormat).hashCode() ^ size.hashCode() ^ Integer.valueOf(fps).hashCode(); + } + } + + /** + * Returns all the combinatorial format, size, and fps camera modes supported. + * + * @return all the combinatorial format, size, and fps camera modes supported + */ + List getAllCameraModes(); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java new file mode 100644 index 00000000..86bd191c --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java @@ -0,0 +1,120 @@ +/* +Copyright (c) 2017 Robert Atkinson + +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 Robert Atkinson nor the names of his 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 com.qualcomm.robotcore.external.hardware.camera; + +import android.content.Context; + +import com.qualcomm.robotcore.hardware.HardwareDevice; + +import org.firstinspires.ftc.robotcore.external.function.Consumer; +import org.firstinspires.ftc.robotcore.external.function.Continuation; +import org.firstinspires.ftc.robotcore.internal.system.Deadline; + +/** + * {@link CameraName} identifies a {@link HardwareDevice} which is a camera. + */ +public interface CameraName +{ + /** + * Returns whether or not this name is that of a webcam. If true, then the + * {@link CameraName} can be cast to a {@link WebcamName}. + * + * @return whether or not this name is that of a webcam + * @see WebcamName + */ + boolean isWebcam(); + + /** + * Returns whether or not this name is that of a builtin phone camera. If true, then the + * {@link CameraName} can be cast to a {@link BuiltinCameraName}. + * + * @return whether or not this name is that of a builtin phone camera + */ + boolean isCameraDirection(); + + /** + * Returns whether this name is one representing the ability to switch amongst a + * series of member cameras. If true, then the receiver can be cast to a + * {@link SwitchableCameraName}. + * + * @return whether this is a {@link SwitchableCameraName} + */ + boolean isSwitchable(); + + + /** + * Returns whether or not this name represents that of an unknown or indeterminate camera. + * @return whether or not this name represents that of an unknown or indeterminate camera + */ + boolean isUnknown(); + + /** + * Requests from the user permission to use the camera if same has not already been granted. + * This may take a long time, as interaction with the user may be necessary. When the outcome + * is known, the reportResult continuation is called with the result. The report may occur either + * before or after the call to {@link #asyncRequestCameraPermission} has itself returned. The report will + * be delivered using the indicated {@link Continuation} + * + * @param context the context in which the permission request should run + * @param deadline the time by which the request must be honored or given up as ungranted. + * If this {@link Deadline} is cancelled while the request is outstanding, + * then the permission request will be aborted and false reported as + * the result of the request. + * @param continuation the dispatcher used to deliver results of the permission request + * + * @throws IllegalArgumentException if the cameraName does not match any known camera device. + * + * @see #requestCameraPermission + */ + void asyncRequestCameraPermission(Context context, Deadline deadline, final Continuation> continuation); + + /** + * Requests from the user permission to use the camera if same has not already been granted. + * This may take a long time, as interaction with the user may be necessary. The call is made + * synchronously: the calling thread blocks until an answer is obtained. + * + * @param deadline the time by which the request must be honored or given up as ungranted + * @return whether or not permission to use the camera has been granted. + * + * @see #asyncRequestCameraPermission + */ + boolean requestCameraPermission(Deadline deadline); + + /** + *

Query the capabilities of a camera device. These capabilities are + * immutable for a given camera.

+ * + * @return The properties of the given camera. A degenerate empty set of properties is returned on error. + */ + CameraCharacteristics getCameraCharacteristics(); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java new file mode 100644 index 00000000..84200276 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java @@ -0,0 +1,4 @@ +package com.qualcomm.robotcore.external.hardware.camera; + +public class WebcamName { +} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java new file mode 100644 index 00000000..4c1d560f --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java @@ -0,0 +1,2 @@ +package org.firstinspires.ftc.vision;public class VisionPortalImpl { +} diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java new file mode 100644 index 00000000..92569ce6 --- /dev/null +++ b/Vision/src/main/java/org/opencv/android/Utils.java @@ -0,0 +1,2 @@ +package org.opencv.android;public class Utils { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java new file mode 100644 index 00000000..c1da5530 --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class OpenCvCamera { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java new file mode 100644 index 00000000..4865cf5f --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class OpenCvCameraBase { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java new file mode 100644 index 00000000..af0365e4 --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class OpenCvCameraRotation { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java new file mode 100644 index 00000000..f8ac842d --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class OpenCvViewRenderer { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java new file mode 100644 index 00000000..ded7f587 --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class OpenCvViewport { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java b/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java new file mode 100644 index 00000000..f7c6bbf9 --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class PipelineRecordingParametes { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java b/Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java new file mode 100644 index 00000000..b6c1832a --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java @@ -0,0 +1,2 @@ +package org.openftc.easyopencv;public class QueueOpenCvPipeline { +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java new file mode 100644 index 00000000..3b5c45fb --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java @@ -0,0 +1,36 @@ +package io.github.deltacv.vision.util; + +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvCamera; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +public abstract class ViewportPipeline extends TimestampedOpenCvPipeline implements OpenCvCamera { + + protected final FrameQueue queue; + + private final Mat emptyMat; + + protected ViewportPipeline(int maxQueueItems) { + queue = new FrameQueue(maxQueueItems + 3); + emptyMat = queue.takeMat(); + } + + protected ViewportPipeline() { + this(10); + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) { + Mat output = queue.poll(); + if(output == null) { + output = emptyMat; + } + + return output; + } + + public FrameQueue getOutputQueue() { + return queue; + } + +} \ No newline at end of file From adfba8425c5b3d7aab31228bd05a82f5b456c51f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 11 Aug 2023 01:41:55 -0600 Subject: [PATCH 13/46] Skija replaced by Skiko --- Common/build.gradle | 3 +- Common/src/main/java/android/util/Size.java | 142 ++++- .../common}/image/BufferedImageRecycler.java | 214 ++++---- .../image/DynamicBufferedImageRecycler.java | 152 +++--- .../deltacv/common/image/MatPoster.java | 13 + .../org/openftc/easyopencv/MatRecycler.java | 12 + .../openftc/easyopencv/OpenCvPipeline.java | 13 - EOCV-Sim/build.gradle | 2 +- .../github/serivesmejia/eocvsim/EOCVSim.kt | 20 +- .../github/serivesmejia/eocvsim/gui/Icons.kt | 3 +- .../serivesmejia/eocvsim/gui/Visualizer.java | 43 +- .../gui/component/SwingOpenCvViewport.java | 146 ----- .../gui/component/tuner/ColorPicker.kt | 3 +- .../tuner/TunableFieldPanelOptions.kt | 8 +- .../gui/component/visualizer/TopMenuBar.kt | 5 +- .../eocvsim/gui/dialog/About.java | 3 +- .../gui/dialog/source/CreateImageSource.java | 2 +- .../gui/dialog/source/CreateVideoSource.java | 2 +- .../eocvsim/gui/util/GuiUtil.java | 21 +- .../{MatPoster.java => MatPosterImpl.java} | 439 +++++++-------- .../eocvsim/output/VideoRecordingSession.kt | 8 +- .../eocvsim/pipeline/PipelineManager.kt | 44 +- .../pipeline/handler/PipelineHandler.kt | 6 +- .../handler/SpecificPipelineHandler.kt | 26 +- .../eocvsim/util/ClasspathScan.kt | 41 +- .../eventloop/opmode/OpModePipelineHandler.kt | 28 +- .../easyopencv/TimestampedPipelineHandler.kt | 18 +- TeamCode/build.gradle | 2 +- .../teamcode/AprilTagDetectionPipeline.java | 10 +- .../ftc/teamcode/AprilTagTestOpMode.java | 25 +- .../ftc/teamcode/SimpleThresholdPipeline.java | 1 + Vision/build.gradle | 12 +- .../main/java/android/graphics/Bitmap.java | 201 ++++++- .../main/java/android/graphics/Canvas.java | 91 +++- .../main/java/android/graphics/FontCache.java | 28 +- .../src/main/java/android/graphics/Paint.java | 12 +- .../src/main/java/android/graphics/Rect.java | 4 + .../main/java/android/graphics/Typeface.java | 40 +- .../eventloop/opmode/LinearOpMode.java | 113 ++++ .../robotcore/eventloop/opmode/OpMode.java | 131 +++++ .../robotcore/hardware/HardwareDevice.java | 83 +++ .../robotcore/hardware/HardwareMap.java | 18 +- .../deltacv/vision/SourcedOpenCvCamera.java | 117 ++++ .../io/github/deltacv/vision/gui/SkiaPanel.kt | 34 ++ .../deltacv/vision/gui/SwingOpenCvViewport.kt | 350 ++++++++++++ .../deltacv/vision}/gui/component/ImageX.java | 175 +++--- .../deltacv/vision/gui/util/ImgUtil.java | 28 + .../github/deltacv/vision/source/Source.java | 18 + .../deltacv/vision/source/SourceHander.java | 7 + .../github/deltacv/vision/source/Sourced.java | 7 + .../github/deltacv/vision}/util/CvUtil.java | 4 +- .../deltacv/vision/util/FrameQueue.java | 49 +- .../deltacv/vision}/util/extension/CvExt.kt | 102 ++-- .../camera/CameraCharacteristics.java | 12 +- .../external/hardware/camera/CameraName.java | 38 +- .../external/hardware/camera/WebcamName.java | 2 +- .../ftc/vision/VisionPortal.javaa | 475 +++++++++++++++++ .../ftc/vision/VisionPortalImpl.java | 2 - .../ftc/vision/VisionPortalImpl.javaa | 501 ++++++++++++++++++ .../vision/apriltag/AprilTagProcessor.java | 4 +- .../main/java/org/opencv/android/Utils.java | 107 +++- .../org/openftc/easyopencv/OpenCvCamera.java | 340 +++++++++++- .../openftc/easyopencv/OpenCvCameraBase.java | 294 +++++++++- .../easyopencv/OpenCvCameraException.java | 30 ++ .../easyopencv/OpenCvCameraRotation.java | 33 +- .../openftc/easyopencv/OpenCvPipeline.java | 74 +++ .../org/openftc/easyopencv/OpenCvTracker.java | 68 +-- .../easyopencv/OpenCvTrackerApiPipeline.java | 142 ++--- .../easyopencv/OpenCvViewRenderer.java | 333 +++++++++++- .../openftc/easyopencv/OpenCvViewport.java | 76 ++- .../PipelineRecordingParameters.java | 120 ++++- .../openftc/easyopencv/QueueOpenCvCamera.java | 2 - .../easyopencv/TimestampedOpenCvPipeline.java | 82 +-- .../java/org/openftc/easyopencv/Util.java | 75 +++ .../openftc/easyopencv/ViewportPipeline.java | 36 -- build.common.gradle | 7 +- build.gradle | 7 +- 77 files changed, 4827 insertions(+), 1112 deletions(-) rename {EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util => Common/src/main/java/io/github/deltacv/common}/image/BufferedImageRecycler.java (96%) rename {EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util => Common/src/main/java/io/github/deltacv/common}/image/DynamicBufferedImageRecycler.java (93%) create mode 100644 Common/src/main/java/io/github/deltacv/common/image/MatPoster.java delete mode 100644 Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java delete mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java rename EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/{MatPoster.java => MatPosterImpl.java} (91%) create mode 100644 Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java create mode 100644 Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java create mode 100644 Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt create mode 100644 Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt rename {EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim => Vision/src/main/java/io/github/deltacv/vision}/gui/component/ImageX.java (86%) create mode 100644 Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/source/Source.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java rename {EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim => Vision/src/main/java/io/github/deltacv/vision}/util/CvUtil.java (98%) rename {EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim => Vision/src/main/java/io/github/deltacv/vision}/util/extension/CvExt.kt (94%) create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa delete mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java rename {Common => Vision}/src/main/java/org/openftc/easyopencv/OpenCvTracker.java (97%) rename {Common => Vision}/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java (97%) delete mode 100644 Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java rename {Common => Vision}/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java (97%) create mode 100644 Vision/src/main/java/org/openftc/easyopencv/Util.java delete mode 100644 Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java diff --git a/Common/build.gradle b/Common/build.gradle index cc150939..b57da9d7 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -22,6 +22,7 @@ publishing { dependencies { api "org.openpnp:opencv:$opencv_version" + implementation "org.slf4j:slf4j-api:$slf4j_version" - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation 'org.jetbrains.kotlin:kotlin-stdlib' } \ No newline at end of file diff --git a/Common/src/main/java/android/util/Size.java b/Common/src/main/java/android/util/Size.java index 2d5d7631..35b40059 100644 --- a/Common/src/main/java/android/util/Size.java +++ b/Common/src/main/java/android/util/Size.java @@ -1,2 +1,140 @@ -package android.util;public class Size { -} +/* + * Copyright (C) 2013 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 android.util; +// import static com.android.internal.util.Preconditions.checkNotNull; +/** + * Immutable class for describing width and height dimensions in pixels. + */ +public final class Size { + /** + * Create a new immutable Size instance. + * + * @param width The width of the size, in pixels + * @param height The height of the size, in pixels + */ + public Size(int width, int height) { + mWidth = width; + mHeight = height; + } + /** + * Get the width of the size (in pixels). + * @return width + */ + public int getWidth() { + return mWidth; + } + /** + * Get the height of the size (in pixels). + * @return height + */ + public int getHeight() { + return mHeight; + } + /** + * Check if this size is equal to another size. + *

+ * Two sizes are equal if and only if both their widths and heights are + * equal. + *

+ *

+ * A size object is never equal to any other type of object. + *

+ * + * @return {@code true} if the objects were equal, {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof Size) { + Size other = (Size) obj; + return mWidth == other.mWidth && mHeight == other.mHeight; + } + return false; + } + /** + * Return the size represented as a string with the format {@code "WxH"} + * + * @return string representation of the size + */ + @Override + public String toString() { + return mWidth + "x" + mHeight; + } + private static NumberFormatException invalidSize(String s) { + throw new NumberFormatException("Invalid Size: \"" + s + "\""); + } + /** + * Parses the specified string as a size value. + *

+ * The ASCII characters {@code \}{@code u002a} ('*') and + * {@code \}{@code u0078} ('x') are recognized as separators between + * the width and height.

+ *

+ * For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}. + * However, the method also handles sizes expressed in the + * following forms:

+ *

+ * "width{@code x}height" or + * "width{@code *}height" {@code => new Size(width, height)}, + * where width and height are string integers potentially + * containing a sign, such as "-10", "+7" or "5".

+ * + *
{@code
+     * Size.parseSize("3*+6").equals(new Size(3, 6)) == true
+     * Size.parseSize("-3x-6").equals(new Size(-3, -6)) == true
+     * Size.parseSize("4 by 3") => throws NumberFormatException
+     * }
+ * + * @param string the string representation of a size value. + * @return the size value represented by {@code string}. + * + * @throws NumberFormatException if {@code string} cannot be parsed + * as a size value. + * @throws NullPointerException if {@code string} was {@code null} + */ + public static Size parseSize(String string) + throws NumberFormatException { + // checkNotNull(string, "string must not be null"); + int sep_ix = string.indexOf('*'); + if (sep_ix < 0) { + sep_ix = string.indexOf('x'); + } + if (sep_ix < 0) { + throw invalidSize(string); + } + try { + return new Size(Integer.parseInt(string.substring(0, sep_ix)), + Integer.parseInt(string.substring(sep_ix + 1))); + } catch (NumberFormatException e) { + throw invalidSize(string); + } + } + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); + } + private final int mWidth; + private final int mHeight; +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java b/Common/src/main/java/io/github/deltacv/common/image/BufferedImageRecycler.java similarity index 96% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java rename to Common/src/main/java/io/github/deltacv/common/image/BufferedImageRecycler.java index d2b21b3f..2da834a7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java +++ b/Common/src/main/java/io/github/deltacv/common/image/BufferedImageRecycler.java @@ -1,107 +1,107 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.util.image; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.concurrent.ArrayBlockingQueue; - -public class BufferedImageRecycler { - - private final RecyclableBufferedImage[] allBufferedImages; - private final ArrayBlockingQueue availableBufferedImages; - - public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight, int allImgType) { - allBufferedImages = new RecyclableBufferedImage[num]; - availableBufferedImages = new ArrayBlockingQueue<>(num); - - for (int i = 0; i < allBufferedImages.length; i++) { - allBufferedImages[i] = new RecyclableBufferedImage(i, allImgWidth, allImgHeight, allImgType); - availableBufferedImages.add(allBufferedImages[i]); - } - } - - public BufferedImageRecycler(int num, Dimension allImgSize, int allImgType) { - this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), allImgType); - } - - public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight) { - this(num, allImgWidth, allImgHeight, BufferedImage.TYPE_3BYTE_BGR); - } - - public BufferedImageRecycler(int num, Dimension allImgSize) { - this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), BufferedImage.TYPE_3BYTE_BGR); - } - - public boolean isOnUse() { return allBufferedImages.length != availableBufferedImages.size(); } - - public synchronized RecyclableBufferedImage takeBufferedImage() { - - if (availableBufferedImages.size() == 0) { - throw new RuntimeException("All buffered images have been checked out!"); - } - - RecyclableBufferedImage buffImg = null; - try { - buffImg = availableBufferedImages.take(); - buffImg.checkedOut = true; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - return buffImg; - - } - - public synchronized void returnBufferedImage(RecyclableBufferedImage buffImg) { - if (buffImg != allBufferedImages[buffImg.idx]) { - throw new IllegalArgumentException("This BufferedImage does not belong to this recycler!"); - } - - if (buffImg.checkedOut) { - buffImg.checkedOut = false; - buffImg.flush(); - availableBufferedImages.add(buffImg); - } else { - throw new IllegalArgumentException("This BufferedImage has already been returned!"); - } - } - - public synchronized void flushAll() { - for(BufferedImage img : allBufferedImages) { - img.flush(); - } - } - - public static class RecyclableBufferedImage extends BufferedImage { - private int idx = -1; - private volatile boolean checkedOut = false; - - private RecyclableBufferedImage(int idx, int width, int height, int imageType) { - super(width, height, imageType); - this.idx = idx; - } - } - -} +/* + * Copyright (c) 2021 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.common.image; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.concurrent.ArrayBlockingQueue; + +public class BufferedImageRecycler { + + private final RecyclableBufferedImage[] allBufferedImages; + private final ArrayBlockingQueue availableBufferedImages; + + public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight, int allImgType) { + allBufferedImages = new RecyclableBufferedImage[num]; + availableBufferedImages = new ArrayBlockingQueue<>(num); + + for (int i = 0; i < allBufferedImages.length; i++) { + allBufferedImages[i] = new RecyclableBufferedImage(i, allImgWidth, allImgHeight, allImgType); + availableBufferedImages.add(allBufferedImages[i]); + } + } + + public BufferedImageRecycler(int num, Dimension allImgSize, int allImgType) { + this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), allImgType); + } + + public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight) { + this(num, allImgWidth, allImgHeight, BufferedImage.TYPE_3BYTE_BGR); + } + + public BufferedImageRecycler(int num, Dimension allImgSize) { + this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), BufferedImage.TYPE_3BYTE_BGR); + } + + public boolean isOnUse() { return allBufferedImages.length != availableBufferedImages.size(); } + + public synchronized RecyclableBufferedImage takeBufferedImage() { + + if (availableBufferedImages.size() == 0) { + throw new RuntimeException("All buffered images have been checked out!"); + } + + RecyclableBufferedImage buffImg = null; + try { + buffImg = availableBufferedImages.take(); + buffImg.checkedOut = true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return buffImg; + + } + + public synchronized void returnBufferedImage(RecyclableBufferedImage buffImg) { + if (buffImg != allBufferedImages[buffImg.idx]) { + throw new IllegalArgumentException("This BufferedImage does not belong to this recycler!"); + } + + if (buffImg.checkedOut) { + buffImg.checkedOut = false; + buffImg.flush(); + availableBufferedImages.add(buffImg); + } else { + throw new IllegalArgumentException("This BufferedImage has already been returned!"); + } + } + + public synchronized void flushAll() { + for(BufferedImage img : allBufferedImages) { + img.flush(); + } + } + + public static class RecyclableBufferedImage extends BufferedImage { + private int idx = -1; + private volatile boolean checkedOut = false; + + private RecyclableBufferedImage(int idx, int width, int height, int imageType) { + super(width, height, imageType); + this.idx = idx; + } + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java b/Common/src/main/java/io/github/deltacv/common/image/DynamicBufferedImageRecycler.java similarity index 93% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java rename to Common/src/main/java/io/github/deltacv/common/image/DynamicBufferedImageRecycler.java index b4b50f3f..50e5cd41 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java +++ b/Common/src/main/java/io/github/deltacv/common/image/DynamicBufferedImageRecycler.java @@ -1,78 +1,76 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.util.image; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -public class DynamicBufferedImageRecycler { - - private final HashMap recyclers = new HashMap<>(); - - public synchronized BufferedImage giveBufferedImage(Dimension size, int recyclerSize) { - - //look for existing buff image recycler with desired dimensions - for(Map.Entry entry : recyclers.entrySet()) { - Dimension dimension = entry.getKey(); - BufferedImageRecycler recycler = entry.getValue(); - - if(dimension.equals(size)) { - BufferedImage buffImg = recycler.takeBufferedImage(); - buffImg.flush(); - return buffImg; - } else if(!recycler.isOnUse()) { - recycler.flushAll(); - recyclers.remove(dimension); - } - } - - //create new one if didn't found an existing recycler - BufferedImageRecycler recycler = new BufferedImageRecycler(recyclerSize, size); - recyclers.put(size, recycler); - - BufferedImage buffImg = recycler.takeBufferedImage(); - - return buffImg; - } - - public synchronized void returnBufferedImage(BufferedImage buffImg) { - Dimension dimension = new Dimension(buffImg.getWidth(), buffImg.getHeight()); - - BufferedImageRecycler recycler = recyclers.get(dimension); - - if(recycler != null) - recycler.returnBufferedImage((BufferedImageRecycler.RecyclableBufferedImage) buffImg); - } - - public synchronized void flushAll() { - for(BufferedImageRecycler recycler : recyclers.values()) { - recycler.flushAll(); - } - } - +/* + * Copyright (c) 2021 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.common.image; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; + +public class DynamicBufferedImageRecycler { + + private final HashMap recyclers = new HashMap<>(); + + public synchronized BufferedImage giveBufferedImage(Dimension size, int recyclerSize) { + + //look for existing buff image recycler with desired dimensions + for(Map.Entry entry : recyclers.entrySet()) { + Dimension dimension = entry.getKey(); + BufferedImageRecycler recycler = entry.getValue(); + + if(dimension.equals(size)) { + BufferedImage buffImg = recycler.takeBufferedImage(); + buffImg.flush(); + return buffImg; + } else if(!recycler.isOnUse()) { + recycler.flushAll(); + recyclers.remove(dimension); + } + } + + //create new one if didn't found an existing recycler + BufferedImageRecycler recycler = new BufferedImageRecycler(recyclerSize, size); + recyclers.put(size, recycler); + + BufferedImage buffImg = recycler.takeBufferedImage(); + + return buffImg; + } + + public synchronized void returnBufferedImage(BufferedImage buffImg) { + Dimension dimension = new Dimension(buffImg.getWidth(), buffImg.getHeight()); + + BufferedImageRecycler recycler = recyclers.get(dimension); + + if(recycler != null) + recycler.returnBufferedImage((BufferedImageRecycler.RecyclableBufferedImage) buffImg); + } + + public synchronized void flushAll() { + for(BufferedImageRecycler recycler : recyclers.values()) { + recycler.flushAll(); + } + } + } \ No newline at end of file diff --git a/Common/src/main/java/io/github/deltacv/common/image/MatPoster.java b/Common/src/main/java/io/github/deltacv/common/image/MatPoster.java new file mode 100644 index 00000000..dd7357b7 --- /dev/null +++ b/Common/src/main/java/io/github/deltacv/common/image/MatPoster.java @@ -0,0 +1,13 @@ +package io.github.deltacv.common.image; + +import org.opencv.core.Mat; + +public interface MatPoster { + + default void post(Mat m) { + post(m, null); + } + + void post(Mat m, Object context); + +} diff --git a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java index 0acc74d9..1ea1c7f9 100644 --- a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java +++ b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java @@ -109,6 +109,18 @@ private RecyclableMat(int idx, int rows, int cols, int type) { this.idx = idx; } + private Object context; + + public void setContext(Object context) + { + this.context = context; + } + + public Object getContext() + { + return context; + } + public void returnMat() { synchronized(MatRecycler.this) { try { diff --git a/Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java deleted file mode 100644 index c108e94b..00000000 --- a/Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.openftc.easyopencv; - -import org.opencv.core.Mat; - -public abstract class OpenCvPipeline { - - public abstract Mat processFrame(Mat input); - - public void onViewportTapped() { } - - public void init(Mat mat) { } - -} \ No newline at end of file diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 522c1081..8c6d0154 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -37,7 +37,7 @@ dependencies { api project(':Common') api project(':Vision') - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation 'org.jetbrains.kotlin:kotlin-stdlib' implementation "org.eclipse.jdt:ecj:3.21.0" 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 5393cabc..96d2a6d7 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 @@ -45,8 +45,11 @@ 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 nu.pattern.OpenCV import org.opencv.core.Size +import org.openftc.easyopencv.TimestampedPipelineHandler import java.awt.Dimension import java.io.File import javax.swing.SwingUtilities @@ -58,6 +61,7 @@ class EOCVSim(val params: Parameters = Parameters()) { companion object { const val VERSION = Build.versionString + const val DEFAULT_EOCV_WIDTH = 320 const val DEFAULT_EOCV_HEIGHT = 240 @@ -184,7 +188,11 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.initAsync(configManager.config.simTheme) //create gui in the EDT inputSourceManager.init() //loading user created input sources + pipelineManager.init() //init pipeline manager (scan for pipelines) + pipelineManager.subscribePipelineHandler(TimestampedPipelineHandler()) + pipelineManager.subscribePipelineHandler(OpModePipelineHandler()) + tunerManager.init() //init tunable variables manager //shows a warning when a pipeline gets "stuck" @@ -211,7 +219,15 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.pipelineSelectorPanel.selectedIndex = 0 //post output mats from the pipeline to the visualizer viewport - pipelineManager.pipelineOutputPosters.add(visualizer.viewport.matPoster) + pipelineManager.pipelineOutputPosters.add(visualizer.viewport) + + pipelineManager.onPipelineChange { + if(pipelineManager.currentPipeline !is OpMode && pipelineManager.currentPipeline != null) { + visualizer.viewport.activate() + } else { + visualizer.viewport.deactivate() + } + } start() } @@ -386,7 +402,7 @@ class EOCVSim(val params: Parameters = Parameters()) { val workspaceMsg = " - ${workspaceManager.workspaceFile.absolutePath} $isBuildRunning" val pipelineFpsMsg = " (${pipelineManager.pipelineFpsCounter.fps} Pipeline FPS)" - val posterFpsMsg = " (${visualizer.viewport.matPoster.fpsCounter.fps} Viewport FPS)" + val posterFpsMsg = " (${visualizer.viewport} Viewport FPS)" val isPaused = if (pipelineManager.paused) " (Paused)" else "" val isRecording = if (isCurrentlyRecording()) " RECORDING" else "" diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt index d603460d..69ead2c8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt @@ -25,6 +25,7 @@ package com.github.serivesmejia.eocvsim.gui import com.github.serivesmejia.eocvsim.gui.util.GuiUtil import com.github.serivesmejia.eocvsim.util.loggerForThis +import io.github.deltacv.vision.gui.util.ImgUtil import java.awt.image.BufferedImage import java.util.NoSuchElementException import javax.swing.ImageIcon @@ -94,7 +95,7 @@ object Icons { val icon = if(resizedIcons.contains(resIconName)) { resizedIcons[resIconName] } else { - resizedIcons[resIconName] = GuiUtil.scaleImage(getImage(name), width, height) + resizedIcons[resIconName] = ImgUtil.scaleImage(getImage(name), width, height) resizedIcons[resIconName] } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 12fc1dcd..b356f501 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -26,7 +26,7 @@ import com.formdev.flatlaf.FlatLaf; import com.github.serivesmejia.eocvsim.Build; import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.Viewport; +import io.github.deltacv.vision.gui.SwingOpenCvViewport; import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.gui.component.visualizer.InputSourceDropTarget; @@ -41,6 +41,7 @@ import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher; import com.github.serivesmejia.eocvsim.workspace.util.template.GradleWorkspaceTemplate; import kotlin.Unit; +import org.opencv.core.Size; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +51,6 @@ import java.awt.event.*; import java.util.ArrayList; import java.util.List; -import java.util.Objects; public class Visualizer { @@ -64,7 +64,8 @@ public class Visualizer { private final EOCVSim eocvSim; public JFrame frame; - public Viewport viewport = null; + public SwingOpenCvViewport viewport = null; + public TopMenuBar menuBar = null; public JPanel tunerMenuPanel = new JPanel(); @@ -122,7 +123,11 @@ public void init(Theme theme) { //instantiate all swing elements after theme installation frame = new JFrame(); - viewport = new Viewport(eocvSim, eocvSim.getConfig().pipelineMaxFps.getFps()); + + viewport = new SwingOpenCvViewport(new Size(1080, 720)); + JLayeredPane panel = viewport.skiaPanel(); + + frame.add(panel); menuBar = new TopMenuBar(this, eocvSim); @@ -144,7 +149,7 @@ public void init(Theme theme) { * IMG VISUALIZER & SCROLL PANE */ - imgScrollPane = new JScrollPane(viewport); + imgScrollPane = new JScrollPane(); imgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); imgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); @@ -157,25 +162,29 @@ public void init(Theme theme) { /* * PIPELINE SELECTOR */ + /* pipelineSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(pipelineSelectorPanel); + rightContainer.add(pipelineSelectorPanel); */ /* * SOURCE SELECTOR */ - sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(sourceSelectorPanel); +/* sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); + rightContainer.add(sourceSelectorPanel);/* /* * TELEMETRY */ + + /* telemetryPanel.setBorder(new EmptyBorder(0, 20, 20, 20)); - rightContainer.add(telemetryPanel); + rightContainer.add(telemetryPanel);*/ /* * SPLIT */ + /* //left side, image scroll & tuner menu split panel imageTunerSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, imgScrollPane, tunerMenuPanel); @@ -192,7 +201,7 @@ public void init(Theme theme) { globalSplitPane.setDropTarget(new InputSourceDropTarget(eocvSim)); - frame.add(globalSplitPane, BorderLayout.CENTER); + frame.add(globalSplitPane, BorderLayout.CENTER); */ //initialize other various stuff of the frame frame.setSize(780, 645); @@ -207,9 +216,9 @@ public void init(Theme theme) { frame.setExtendedState(JFrame.MAXIMIZED_BOTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - globalSplitPane.setDividerLocation(1070); + // globalSplitPane.setDividerLocation(1070); - colorPicker = new ColorPicker(viewport.image); + // colorPicker = new ColorPicker(viewport.image); frame.setVisible(true); @@ -238,7 +247,7 @@ public void windowClosing(WindowEvent e) { }); //handling onViewportTapped evts - viewport.addMouseListener(new MouseAdapter() { + viewport.getComponent().addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { if(!colorPicker.isPicking()) eocvSim.pipelineManager.callViewportTapped(); @@ -248,8 +257,8 @@ public void mouseClicked(MouseEvent e) { //VIEWPORT RESIZE HANDLING imgScrollPane.addMouseWheelListener(e -> { if (isCtrlPressed) { //check if control key is pressed - double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation()); - viewport.setViewportScale(scale); + // double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation()); + // viewport.setViewportScale(scale); } }); @@ -311,7 +320,7 @@ public void waitForFinishingInit() { public void close() { SwingUtilities.invokeLater(() -> { frame.setVisible(false); - viewport.stop(); + viewport.deactivate(); //close all asyncpleasewait dialogs for (AsyncPleaseWaitDialog dialog : pleaseWaitDialogs) { @@ -342,7 +351,7 @@ public void close() { childDialogs.clear(); frame.dispose(); - viewport.flush(); + viewport.deactivate(); }); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java deleted file mode 100644 index c9e6e165..00000000 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SwingOpenCvViewport.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.gui.component; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.util.MatPoster; -import com.github.serivesmejia.eocvsim.util.image.DynamicBufferedImageRecycler; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.qualcomm.robotcore.util.Range; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; - -public class Viewport extends JPanel { - - public final ImageX image = new ImageX(); - public final MatPoster matPoster; - - private Mat lastVisualizedMat = null; - private Mat lastVisualizedScaledMat = null; - - private final DynamicBufferedImageRecycler buffImgGiver = new DynamicBufferedImageRecycler(); - - private volatile BufferedImage lastBuffImage; - private volatile Dimension lastDimension; - - private double scale; - - private final EOCVSim eocvSim; - - Logger logger = LoggerFactory.getLogger(getClass()); - - public Viewport(EOCVSim eocvSim, int maxQueueItems) { - super(new GridBagLayout()); - - this.eocvSim = eocvSim; - setViewportScale(eocvSim.configManager.getConfig().zoom); - - add(image, new GridBagConstraints()); - - matPoster = new MatPoster("Viewport", maxQueueItems); - attachToPoster(matPoster); - } - - public void postMatAsync(Mat mat) { - matPoster.post(mat); - } - - public synchronized void postMat(Mat mat) { - if(lastVisualizedMat == null) lastVisualizedMat = new Mat(); //create latest mat if we have null reference - if(lastVisualizedScaledMat == null) lastVisualizedScaledMat = new Mat(); //create last scaled mat if null reference - - JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this); - - mat.copyTo(lastVisualizedMat); //copy given mat to viewport latest one - - double wScale = (double) frame.getWidth() / mat.width(); - double hScale = (double) frame.getHeight() / mat.height(); - - double calcScale = (wScale / hScale) * 1.5; - double finalScale = Math.max(0.1, Math.min(3, scale * calcScale)); - - Size size = new Size(mat.width() * finalScale, mat.height() * finalScale); - Imgproc.resize(mat, lastVisualizedScaledMat, size, 0.0, 0.0, Imgproc.INTER_AREA); //resize mat to lastVisualizedScaledMat - - Dimension newDimension = new Dimension(lastVisualizedScaledMat.width(), lastVisualizedScaledMat.height()); - - if(lastBuffImage != null) buffImgGiver.returnBufferedImage(lastBuffImage); - - lastBuffImage = buffImgGiver.giveBufferedImage(newDimension, 2); - lastDimension = newDimension; - - CvUtil.matToBufferedImage(lastVisualizedScaledMat, lastBuffImage); - - image.setImage(lastBuffImage); //set buff image to ImageX component - - eocvSim.configManager.getConfig().zoom = scale; //store latest scale if store setting turned on - } - - public void attachToPoster(MatPoster poster) { - poster.addPostable((m) -> { - try { - Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2BGR); - postMat(m); - } catch(Exception ex) { - logger.error("Couldn't visualize last mat", ex); - } - }); - } - - public void flush() { - buffImgGiver.flushAll(); - } - - public void stop() { - matPoster.stop(); - flush(); - } - - public synchronized void setViewportScale(double scale) { - scale = Range.clip(scale, 0.1, 3); - - boolean scaleChanged = this.scale != scale; - this.scale = scale; - - if(lastVisualizedMat != null && scaleChanged) - postMat(lastVisualizedMat); - - } - - public synchronized Mat getLastVisualizedMat() { - return lastVisualizedMat; - } - - public synchronized double getViewportScale() { - return scale; - } - -} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt index 3244f25a..519318fe 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt @@ -25,8 +25,7 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.gui.Icons -import com.github.serivesmejia.eocvsim.gui.component.ImageX -import com.github.serivesmejia.eocvsim.gui.component.Viewport +import io.github.deltacv.vision.gui.component.ImageX import com.github.serivesmejia.eocvsim.util.event.EventHandler import org.opencv.core.Scalar import java.awt.Color 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 9be6461a..30b02da3 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 @@ -26,7 +26,7 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.Icons import com.github.serivesmejia.eocvsim.gui.component.PopupX -import com.github.serivesmejia.eocvsim.util.extension.cvtColor +import io.github.deltacv.vision.util.extension.cvtColor import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero import java.awt.FlowLayout import java.awt.GridLayout @@ -118,12 +118,12 @@ class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, //start picking if global color picker is not being used by other panel if(!colorPicker.isPicking && colorPickButton.isSelected) { - startPicking(colorPicker) + // startPicking(colorPicker) // TODO: Fix color picker } else { //handles cases when cancelling picking - colorPicker.stopPicking() + //colorPicker.stopPicking() // TODO: Fix color picker //if we weren't the ones controlling the last picking, //start picking again to gain control for this panel - if(colorPickButton.isSelected) startPicking(colorPicker) + // if(colorPickButton.isSelected) startPicking(colorPicker) } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index 0956403c..0679428e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -78,13 +78,14 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { val fileSaveMat = JMenuItem("Save current image") + /* fileSaveMat.addActionListener { GuiUtil.saveMatFileChooser( visualizer.frame, - visualizer.viewport.lastVisualizedMat, + visualizer.viewport., eocvSim ) - } + }*/ // TODO: Fix this mFileMenu.add(fileSaveMat) mFileMenu.addSeparator() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java index 768f62a8..f9bc955b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java @@ -25,8 +25,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.Icons; -import com.github.serivesmejia.eocvsim.gui.Visualizer; -import com.github.serivesmejia.eocvsim.gui.component.ImageX; +import io.github.deltacv.vision.gui.component.ImageX; import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; import com.github.serivesmejia.eocvsim.util.StrUtil; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java index b7826535..dfe8dc62 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java @@ -27,7 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; import com.github.serivesmejia.eocvsim.input.source.ImageSource; -import com.github.serivesmejia.eocvsim.util.CvUtil; +import io.github.deltacv.vision.util.CvUtil; import com.github.serivesmejia.eocvsim.util.FileFilters; import com.github.serivesmejia.eocvsim.util.StrUtil; import org.opencv.core.Size; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java index 97115b81..c4e3d165 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java @@ -27,7 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; import com.github.serivesmejia.eocvsim.input.source.VideoSource; -import com.github.serivesmejia.eocvsim.util.CvUtil; +import io.github.deltacv.vision.util.CvUtil; import com.github.serivesmejia.eocvsim.util.FileFilters; import com.github.serivesmejia.eocvsim.util.StrUtil; import org.opencv.core.Mat; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java index a1fb63f4..a94e653d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java @@ -26,7 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.DialogFactory; import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists; -import com.github.serivesmejia.eocvsim.util.CvUtil; +import io.github.deltacv.vision.util.CvUtil; import com.github.serivesmejia.eocvsim.util.SysUtil; import org.opencv.core.Mat; import org.slf4j.Logger; @@ -83,25 +83,6 @@ public void replace(FilterBypass fb, int offset, int length, String text, Attrib } - public static ImageIcon scaleImage(ImageIcon icon, int w, int h) { - - int nw = icon.getIconWidth(); - int nh = icon.getIconHeight(); - - if (icon.getIconWidth() > w) { - nw = w; - nh = (nw * icon.getIconHeight()) / icon.getIconWidth(); - } - - if (nh > h) { - nh = h; - nw = (icon.getIconWidth() * nh) / icon.getIconHeight(); - } - - return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_SMOOTH)); - - } - public static ImageIcon loadImageIcon(String path) throws IOException { return new ImageIcon(loadBufferedImage(path)); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java similarity index 91% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java index a8638aeb..62cca8f7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java @@ -1,220 +1,221 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.gui.util; - -import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; -import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; -import org.opencv.core.Mat; -import org.openftc.easyopencv.MatRecycler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.concurrent.ArrayBlockingQueue; - -public class MatPoster { - - private final ArrayList postables = new ArrayList<>(); - - private final EvictingBlockingQueue postQueue; - private final MatRecycler matRecycler; - - private final String name; - - private final Thread posterThread; - - public final FpsCounter fpsCounter = new FpsCounter(); - - private final Object lock = new Object(); - - private volatile boolean paused = false; - - private volatile boolean hasPosterThreadStarted = false; - - Logger logger; - - public static MatPoster createWithoutRecycler(String name, int maxQueueItems) { - return new MatPoster(name, maxQueueItems, null); - } - - public MatPoster(String name, int maxQueueItems) { - this(name, new MatRecycler(maxQueueItems + 2)); - } - - public MatPoster(String name, MatRecycler recycler) { - this(name, recycler.getSize(), recycler); - } - - public MatPoster(String name, int maxQueueItems, MatRecycler recycler) { - postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); - matRecycler = recycler; - posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); - - this.name = name; - - logger = LoggerFactory.getLogger("MatPoster-" + name); - - postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue - } - - public void post(Mat m) { - if (m == null || m.empty()) { - logger.warn("Tried to post empty or null mat, skipped this frame."); - return; - } - - if (matRecycler != null) { - if(matRecycler.getAvailableMatsAmount() < 1) { - //evict one if we don't have any available mats in the recycler - evict(postQueue.poll()); - } - - MatRecycler.RecyclableMat recycledMat = matRecycler.takeMat(); - m.copyTo(recycledMat); - - postQueue.offer(recycledMat); - } else { - postQueue.offer(m); - } - } - - public void synchronizedPost(Mat m) { - synchronize(() -> post(m)); - } - - public Mat pull() throws InterruptedException { - synchronized(lock) { - return postQueue.take(); - } - } - - public void clearQueue() { - if(postQueue.size() == 0) return; - - synchronized(lock) { - postQueue.clear(); - } - } - - public void synchronize(Runnable runn) { - synchronized(lock) { - runn.run(); - } - } - - public void addPostable(Postable postable) { - //start mat posting thread if it hasn't been started yet - if (!posterThread.isAlive() && !hasPosterThreadStarted) { - posterThread.start(); - } - - postables.add(postable); - } - - public void stop() { - logger.info("Destroying..."); - - posterThread.interrupt(); - - for (Mat m : postQueue) { - if (m != null) { - if(m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat)m).returnMat(); - } - } - } - - matRecycler.releaseAll(); - } - - private void evict(Mat m) { - if (m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) m).returnMat(); - } - m.release(); - } - - public void setPaused(boolean paused) { - this.paused = paused; - } - - public boolean getPaused() { - synchronized(lock) { - return paused; - } - } - - public String getName() { - return name; - } - - public interface Postable { - void post(Mat m); - } - - private class PosterRunnable implements Runnable { - - private Mat postableMat = new Mat(); - - @Override - public void run() { - hasPosterThreadStarted = true; - - while (!Thread.interrupted()) { - - while(paused && !Thread.currentThread().isInterrupted()) { - Thread.yield(); - } - - if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames - - synchronized(lock) { - fpsCounter.update(); - - try { - Mat takenMat = postQueue.take(); - - for (Postable postable : postables) { - takenMat.copyTo(postableMat); - postable.post(postableMat); - } - - takenMat.release(); - - if (takenMat instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) takenMat).returnMat(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - break; - } catch (Exception ex) { } - } - - } - - logger.warn("Thread interrupted (" + Integer.toHexString(hashCode()) + ")"); - } - } - +/* + * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.gui.util; + +import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; +import io.github.deltacv.common.image.MatPoster; +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; +import org.opencv.core.Mat; +import org.openftc.easyopencv.MatRecycler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; + +public class MatPosterImpl implements MatPoster { + private final ArrayList postables = new ArrayList<>(); + + private final EvictingBlockingQueue postQueue; + private final MatRecycler matRecycler; + + private final String name; + + private final Thread posterThread; + + public final FpsCounter fpsCounter = new FpsCounter(); + + private final Object lock = new Object(); + + private volatile boolean paused = false; + + private volatile boolean hasPosterThreadStarted = false; + + Logger logger; + + public static MatPosterImpl createWithoutRecycler(String name, int maxQueueItems) { + return new MatPosterImpl(name, maxQueueItems, null); + } + + public MatPosterImpl(String name, int maxQueueItems) { + this(name, new MatRecycler(maxQueueItems + 2)); + } + + public MatPosterImpl(String name, MatRecycler recycler) { + this(name, recycler.getSize(), recycler); + } + + public MatPosterImpl(String name, int maxQueueItems, MatRecycler recycler) { + postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); + matRecycler = recycler; + posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); + + this.name = name; + + logger = LoggerFactory.getLogger("MatPoster-" + name); + + postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue + } + + @Override + public void post(Mat m, Object context) { + if (m == null || m.empty()) { + logger.warn("Tried to post empty or null mat, skipped this frame."); + return; + } + + if (matRecycler != null) { + if(matRecycler.getAvailableMatsAmount() < 1) { + //evict one if we don't have any available mats in the recycler + evict(postQueue.poll()); + } + + MatRecycler.RecyclableMat recycledMat = matRecycler.takeMat(); + m.copyTo(recycledMat); + + postQueue.offer(recycledMat); + } else { + postQueue.offer(m); + } + } + + public void synchronizedPost(Mat m) { + synchronize(() -> post(m)); + } + + public Mat pull() throws InterruptedException { + synchronized(lock) { + return postQueue.take(); + } + } + + public void clearQueue() { + if(postQueue.size() == 0) return; + + synchronized(lock) { + postQueue.clear(); + } + } + + public void synchronize(Runnable runn) { + synchronized(lock) { + runn.run(); + } + } + + public void addPostable(Postable postable) { + //start mat posting thread if it hasn't been started yet + if (!posterThread.isAlive() && !hasPosterThreadStarted) { + posterThread.start(); + } + + postables.add(postable); + } + + public void stop() { + logger.info("Destroying..."); + + posterThread.interrupt(); + + for (Mat m : postQueue) { + if (m != null) { + if(m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat)m).returnMat(); + } + } + } + + matRecycler.releaseAll(); + } + + private void evict(Mat m) { + if (m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) m).returnMat(); + } + m.release(); + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean getPaused() { + synchronized(lock) { + return paused; + } + } + + public String getName() { + return name; + } + + public interface Postable { + void post(Mat m); + } + + private class PosterRunnable implements Runnable { + + private Mat postableMat = new Mat(); + + @Override + public void run() { + hasPosterThreadStarted = true; + + while (!Thread.interrupted()) { + + while(paused && !Thread.currentThread().isInterrupted()) { + Thread.yield(); + } + + if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames + + synchronized(lock) { + fpsCounter.update(); + + try { + Mat takenMat = postQueue.take(); + + for (Postable postable : postables) { + takenMat.copyTo(postableMat); + postable.post(postableMat); + } + + takenMat.release(); + + if (takenMat instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) takenMat).returnMat(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } catch (Exception ex) { } + } + + } + + logger.warn("Thread interrupted (" + Integer.toHexString(hashCode()) + ")"); + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt index 81e0d6d9..a31c6737 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt @@ -23,10 +23,10 @@ package com.github.serivesmejia.eocvsim.output -import com.github.serivesmejia.eocvsim.gui.util.MatPoster +import com.github.serivesmejia.eocvsim.gui.util.MatPosterImpl import com.github.serivesmejia.eocvsim.util.StrUtil -import com.github.serivesmejia.eocvsim.util.extension.aspectRatio -import com.github.serivesmejia.eocvsim.util.extension.clipTo +import io.github.deltacv.vision.util.extension.aspectRatio +import io.github.deltacv.vision.util.extension.clipTo import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import org.opencv.core.* import org.opencv.imgproc.Imgproc @@ -47,7 +47,7 @@ class VideoRecordingSession( @Volatile private var videoMat: Mat? = null - val matPoster = MatPoster("VideoRec", videoFps.toInt()) + val matPoster = MatPosterImpl("VideoRec", videoFps.toInt()) private val fpsCounter = FpsCounter() 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 db057edc..4cba9b69 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 @@ -25,8 +25,8 @@ package com.github.serivesmejia.eocvsim.pipeline import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.gui.util.MatPoster import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager +import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot import com.github.serivesmejia.eocvsim.util.StrUtil @@ -34,12 +34,14 @@ import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.qualcomm.robotcore.eventloop.opmode.OpMode +import io.github.deltacv.common.image.MatPoster import kotlinx.coroutines.* import org.firstinspires.ftc.robotcore.external.Telemetry import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl import org.opencv.core.Mat import org.openftc.easyopencv.OpenCvPipeline -import org.openftc.easyopencv.TimestampedPipelineHandler +import org.openftc.easyopencv.OpenCvViewport import java.lang.reflect.Constructor import java.lang.reflect.Field import java.util.* @@ -119,8 +121,9 @@ class PipelineManager(var eocvSim: EOCVSim) { //manages and builds pipelines in runtime @JvmField val compiledPipelineManager = CompiledPipelineManager(this) - //this will be handling the special pipeline "timestamped" type - val timestampedPipelineHandler = TimestampedPipelineHandler() + + private val pipelineHandlers = mutableListOf() + //counting and tracking exceptions for logging and reporting purposes val pipelineExceptionTracker = PipelineExceptionTracker(this) @@ -129,7 +132,6 @@ class PipelineManager(var eocvSim: EOCVSim) { enum class PauseReason { USER_REQUESTED, IMAGE_ONE_ANALYSIS, NOT_PAUSED } - fun init() { logger.info("Initializing...") @@ -180,8 +182,22 @@ class PipelineManager(var eocvSim: EOCVSim) { } } + onUpdate { + if(currentPipeline != null) { + for (pipelineHandler in pipelineHandlers) { + pipelineHandler.processFrame(eocvSim.inputSourceManager.currentInputSource) + } + } + } + onPipelineChange { openedPipelineOutputCount = 0 + + if(currentPipeline != null) { + for (pipelineHandler in pipelineHandlers) { + pipelineHandler.onChange(currentPipeline!!, currentTelemetry!!) + } + } } } @@ -234,8 +250,6 @@ class PipelineManager(var eocvSim: EOCVSim) { return } - timestampedPipelineHandler.update(currentPipeline, eocvSim.inputSourceManager.currentInputSource) - lastPipelineAction = if(!hasInitCurrentPipeline) { "init/processFrame" } else { @@ -255,8 +269,16 @@ class PipelineManager(var eocvSim: EOCVSim) { //haven't done so. if(!hasInitCurrentPipeline && inputMat != null) { + for(pipeHandler in pipelineHandlers) { + pipeHandler.preInit(); + } + currentPipeline?.init(inputMat) + for(pipeHandler in pipelineHandlers) { + pipeHandler.init(); + } + logger.info("Initialized pipeline $currentPipelineName") hasInitCurrentPipeline = true @@ -271,10 +293,10 @@ class PipelineManager(var eocvSim: EOCVSim) { for (poster in pipelineOutputPosters.toTypedArray()) { try { - poster.post(outputMat) + poster.post(outputMat, OpenCvViewport.FrameContext(currentPipeline, currentPipeline?.userContextForDrawHook)) } catch (ex: Exception) { logger.error( - "Uncaught exception thrown while posting pipeline output Mat to ${poster.name} poster", + "Uncaught exception thrown while posting pipeline output Mat to poster", ex ) } @@ -394,6 +416,10 @@ class PipelineManager(var eocvSim: EOCVSim) { } } + fun subscribePipelineHandler(handler: PipelineHandler) { + pipelineHandlers.add(handler) + } + @Suppress("UNCHECKED_CAST") @JvmOverloads fun addPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) { try { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt index 359ce028..8cbb1560 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt @@ -1,4 +1,4 @@ -package com.github.serivesmejia.eocvsim.pipeline.util +package com.github.serivesmejia.eocvsim.pipeline.handler import com.github.serivesmejia.eocvsim.input.InputSource import org.firstinspires.ftc.robotcore.external.Telemetry @@ -6,12 +6,12 @@ import org.openftc.easyopencv.OpenCvPipeline interface PipelineHandler { - fun preInit(pipeline: OpenCvPipeline, telemetry: Telemetry) + fun preInit() fun init() fun processFrame(currentInputSource: InputSource?) - fun onChange() + fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt index 4f2e2f8f..6386c286 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt @@ -1,4 +1,28 @@ package com.github.serivesmejia.eocvsim.pipeline.handler -class SpecificPipelineHandler { +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline + +abstract class SpecificPipelineHandler( + val typeChecker: (OpenCvPipeline) -> Boolean +) : PipelineHandler { + + var pipeline: P? = null + private set + + var telemetry: Telemetry? = null + private set + + @Suppress("UNCHECKED_CAST") + override fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) { + if(typeChecker(pipeline)) { + this.pipeline = pipeline as P + this.telemetry = telemetry + } else { + this.pipeline = null + this.telemetry = null // "don't get paid enough to handle this shit" + // - OpModePipelineHandler, probably + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt index 4115bcff..ef3c7897 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt @@ -43,7 +43,6 @@ class ClasspathScan { "io.github.classgraph", "io.github.deltacv", "com.github.serivesmejia.eocvsim.pipeline", - "org.openftc", "org.lwjgl", "org.apache", "org.codehaus", @@ -79,25 +78,41 @@ class ClasspathScan { logger.info("ClassGraph finished scanning (took ${timer.seconds()}s)") val tunableFieldClassesInfo = scanResult.getClassesWithAnnotation(RegisterTunableField::class.java.name) - val pipelineClassesInfo = scanResult.getSubclasses(OpenCvPipeline::class.java.name) val pipelineClasses = mutableListOf>() - for(pipelineClassInfo in pipelineClassesInfo) { - val clazz = if(classLoader != null) { - classLoader.loadClass(pipelineClassInfo.name) - } else Class.forName(pipelineClassInfo.name) - - if(ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) { - if(clazz.isAnnotationPresent(Disabled::class.java)) { - logger.info("Found @Disabled pipeline ${clazz.typeName}") - } else { - logger.info("Found pipeline ${clazz.typeName}") - pipelineClasses.add(clazz as Class) + // i...don't even know how to name this, sorry, future readers + // but classgraph for some reason does not have a recursive search for subclasses... + fun searchPipelinesOfSuperclass(superclass: String) { + val pipelineClassesInfo = scanResult.getSubclasses(superclass) + + for(pipelineClassInfo in pipelineClassesInfo) { + for(pipelineSubclassInfo in pipelineClassInfo.subclasses) { + searchPipelinesOfSuperclass(pipelineSubclassInfo.name) // naming is my passion + } + + if(pipelineClassInfo.isAbstract || pipelineClassInfo.isInterface) { + continue // nope'd outta here + } + + val clazz = if(classLoader != null) { + classLoader.loadClass(pipelineClassInfo.name) + } else Class.forName(pipelineClassInfo.name) + + if(!pipelineClasses.contains(clazz) && ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) { + if(clazz.isAnnotationPresent(Disabled::class.java)) { + logger.info("Found @Disabled pipeline ${clazz.typeName}") + } else { + logger.info("Found pipeline ${clazz.typeName}") + pipelineClasses.add(clazz as Class) + } } } } + // start recursive hell + searchPipelinesOfSuperclass(OpenCvPipeline::class.java.name) + logger.info("Found ${pipelineClasses.size} pipelines") val tunableFieldClasses = mutableListOf>>() diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index bca16ec3..a4ad3425 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -1,4 +1,30 @@ package com.qualcomm.robotcore.eventloop.opmode -class OpModePipelineHandler { +import com.github.serivesmejia.eocvsim.input.InputSource +import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler +import com.qualcomm.robotcore.hardware.HardwareMap +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline + +class OpModePipelineHandler : SpecificPipelineHandler( + { it is OpMode } +) { + + override fun preInit() { + pipeline?.telemetry = telemetry + pipeline?.hardwareMap = HardwareMap(null, null) + } + + override fun init() { + } + + override fun processFrame(currentInputSource: InputSource?) { + } + + override fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) { + this.pipeline?.requestOpModeStop() + + super.onChange(pipeline, telemetry) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt index 74248561..70809c18 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt @@ -24,13 +24,21 @@ package org.openftc.easyopencv import com.github.serivesmejia.eocvsim.input.InputSource +import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler +import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler +import org.firstinspires.ftc.robotcore.external.Telemetry -class TimestampedPipelineHandler { +class TimestampedPipelineHandler : SpecificPipelineHandler( + { it is TimestampedOpenCvPipeline } +) { + override fun preInit() { + } - fun update(currentPipeline: OpenCvPipeline?, currentInputSource: InputSource?) { - if(currentPipeline is TimestampedOpenCvPipeline) { - currentPipeline.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L) - } + override fun init() { + pipeline?.setTimestamp(0) } + override fun processFrame(currentInputSource: InputSource?) { + pipeline?.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L) + } } \ No newline at end of file diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index d2dfbbac..11c85229 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -12,7 +12,7 @@ dependencies { implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "org.jetbrains.kotlin:kotlin-stdlib" } task(runSim, dependsOn: 'classes', type: JavaExec) { diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java index fe16f173..f16770d3 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java @@ -23,6 +23,7 @@ import com.qualcomm.robotcore.eventloop.opmode.Disabled; import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.robotcore.external.navigation.*; import org.opencv.calib3d.Calib3d; import org.opencv.core.CvType; import org.opencv.core.Mat; @@ -133,13 +134,16 @@ public Mat processFrame(Mat input) drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix); draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix); + Orientation rot = Orientation.getOrientation(detection.pose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, AngleUnit.DEGREES); + telemetry.addLine(String.format("\nDetected tag ID=%d", detection.id)); telemetry.addLine(String.format("Translation X: %.2f feet", detection.pose.x*FEET_PER_METER)); telemetry.addLine(String.format("Translation Y: %.2f feet", detection.pose.y*FEET_PER_METER)); telemetry.addLine(String.format("Translation Z: %.2f feet", detection.pose.z*FEET_PER_METER)); - telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", Math.toDegrees(detection.pose.yaw))); - telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", Math.toDegrees(detection.pose.pitch))); - telemetry.addLine(String.format("Rotation Roll: %.2f degrees", Math.toDegrees(detection.pose.roll))); + + telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", rot.firstAngle)); + telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", rot.secondAngle)); + telemetry.addLine(String.format("Rotation Roll: %.2f degrees", rot.thirdAngle)); } telemetry.update(); diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java index 5c8f0375..bd003c57 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java @@ -1,30 +1,17 @@ package org.firstinspires.ftc.teamcode; -import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; -import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; -import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvPipeline; -import org.openftc.easyopencv.TimestampedOpenCvPipeline; +import com.qualcomm.robotcore.eventloop.opmode.OpMode; -public class AprilTagProcessorTest extends TimestampedOpenCvPipeline { - - AprilTagProcessor processor = new AprilTagProcessor.Builder() - .setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) - .setDrawCubeProjection(true) - .setDrawTagID(true) - .setOutputUnits(DistanceUnit.CM, AngleUnit.DEGREES) - .build(); +public class AprilTagTestOpMode extends OpMode { @Override - public void init(Mat firstFrame) { - processor.init(firstFrame.width(), firstFrame.height(), null); + public void init() { + telemetry.addData("Status", "Initialized"); } @Override - public Mat processFrame(Mat input, long captureTimeNanos) { - processor.processFrame(input, captureTimeNanos); - return input; + public void loop() { + } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java index de6bf360..c6e5fe20 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java @@ -31,6 +31,7 @@ import org.opencv.imgproc.Imgproc; import org.openftc.easyopencv.OpenCvPipeline; +@Disabled public class SimpleThresholdPipeline extends OpenCvPipeline { /* diff --git a/Vision/build.gradle b/Vision/build.gradle index 9e5a815f..bfe7fe40 100644 --- a/Vision/build.gradle +++ b/Vision/build.gradle @@ -36,13 +36,13 @@ dependencies { api "org.openpnp:opencv:$opencv_version" implementation "org.slf4j:slf4j-api:$slf4j_version" - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation 'org.jetbrains.kotlin:kotlin-stdlib' - // Compatibility: Skija supports many platforms but we will only be adding + // Compatibility: Skiko supports many platforms but we will only be adding // those that are supported by AprilTagDesktop as well - implementation("io.github.humbleui:skija-windows-x64:$skija_version") - implementation("io.github.humbleui:skija-linux-x64:$skija_version") - implementation("io.github.humbleui:skija-macos-x64:$skija_version") - implementation("io.github.humbleui:skija-macos-arm64:$skija_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-x64:$skiko_version") + implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-arm64:$skiko_version") } \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Bitmap.java b/Vision/src/main/java/android/graphics/Bitmap.java index 973e762a..de7c3513 100644 --- a/Vision/src/main/java/android/graphics/Bitmap.java +++ b/Vision/src/main/java/android/graphics/Bitmap.java @@ -23,12 +23,209 @@ package android.graphics; +import org.jetbrains.skia.ColorAlphaType; +import org.jetbrains.skia.ColorType; +import org.jetbrains.skia.ImageInfo; + public class Bitmap { - public final io.github.humbleui.skija.Bitmap theBitmap; + /** + * Possible bitmap configurations. A bitmap configuration describes + * how pixels are stored. This affects the quality (color depth) as + * well as the ability to display transparent/translucent colors. + */ + public enum Config { + // these native values must match up with the enum in SkBitmap.h + /** + * Each pixel is stored as a single translucency (alpha) channel. + * This is very useful to efficiently store masks for instance. + * No color information is stored. + * With this configuration, each pixel requires 1 byte of memory. + */ + ALPHA_8(1), + /** + * Each pixel is stored on 2 bytes and only the RGB channels are + * encoded: red is stored with 5 bits of precision (32 possible + * values), green is stored with 6 bits of precision (64 possible + * values) and blue is stored with 5 bits of precision. + * + * This configuration can produce slight visual artifacts depending + * on the configuration of the source. For instance, without + * dithering, the result might show a greenish tint. To get better + * results dithering should be applied. + * + * This configuration may be useful when using opaque bitmaps + * that do not require high color fidelity. + * + *

Use this formula to pack into 16 bits:

+ *
+         * short color = (R & 0x1f) << 11 | (G & 0x3f) << 5 | (B & 0x1f);
+         * 
+ */ + RGB_565(3), + /** + * Each pixel is stored on 2 bytes. The three RGB color channels + * and the alpha channel (translucency) are stored with a 4 bits + * precision (16 possible values.) + * + * This configuration is mostly useful if the application needs + * to store translucency information but also needs to save + * memory. + * + * It is recommended to use {@link #ARGB_8888} instead of this + * configuration. + * + * Note: as of {link android.os.Build.VERSION_CODES#KITKAT}, + * any bitmap created with this configuration will be created + * using {@link #ARGB_8888} instead. + * + * @deprecated Because of the poor quality of this configuration, + * it is advised to use {@link #ARGB_8888} instead. + */ + @Deprecated + ARGB_4444(4), + /** + * Each pixel is stored on 4 bytes. Each channel (RGB and alpha + * for translucency) is stored with 8 bits of precision (256 + * possible values.) + * + * This configuration is very flexible and offers the best + * quality. It should be used whenever possible. + * + *

Use this formula to pack into 32 bits:

+ *
+         * int color = (A & 0xff) << 24 | (B & 0xff) << 16 | (G & 0xff) << 8 | (R & 0xff);
+         * 
+ */ + ARGB_8888(5), + /** + * Each pixel is stored on 8 bytes. Each channel (RGB and alpha + * for translucency) is stored as a + * {@link android.util.Half half-precision floating point value}. + * + * This configuration is particularly suited for wide-gamut and + * HDR content. + * + *

Use this formula to pack into 64 bits:

+ *
+         * long color = (A & 0xffff) << 48 | (B & 0xffff) << 32 | (G & 0xffff) << 16 | (R & 0xffff);
+         * 
+ */ + RGBA_F16(6), + /** + * Special configuration, when bitmap is stored only in graphic memory. + * Bitmaps in this configuration are always immutable. + * + * It is optimal for cases, when the only operation with the bitmap is to draw it on a + * screen. + */ + HARDWARE(7), + /** + * Each pixel is stored on 4 bytes. Each RGB channel is stored with 10 bits of precision + * (1024 possible values). There is an additional alpha channel that is stored with 2 bits + * of precision (4 possible values). + * + * This configuration is suited for wide-gamut and HDR content which does not require alpha + * blending, such that the memory cost is the same as ARGB_8888 while enabling higher color + * precision. + * + *

Use this formula to pack into 32 bits:

+ *
+         * int color = (A & 0x3) << 30 | (B & 0x3ff) << 20 | (G & 0x3ff) << 10 | (R & 0x3ff);
+         * 
+ */ + RGBA_1010102(8); + + final int nativeInt; + private static Config sConfigs[] = { + null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE, RGBA_1010102 + }; + Config(int ni) { + this.nativeInt = ni; + } + + static Config nativeToConfig(int ni) { + return sConfigs[ni]; + } + } + + private static ColorType configToColorType(Config config) { + switch (config) { + case ALPHA_8: + return ColorType.ALPHA_8; + case RGB_565: + return ColorType.RGB_565; + case ARGB_4444: + return ColorType.ARGB_4444; + case ARGB_8888: + return ColorType.BGRA_8888; + case RGBA_F16: + return ColorType.RGBA_F16; + case RGBA_1010102: + return ColorType.RGBA_1010102; + default: + throw new IllegalArgumentException("Unknown config: " + config); + } + } + + public Config colorTypeToConfig(ColorType colorType) { + switch (colorType) { + case ALPHA_8: + return Config.ALPHA_8; + case RGB_565: + return Config.RGB_565; + case ARGB_4444: + return Config.ARGB_4444; + case BGRA_8888: + return Config.ARGB_8888; + case RGBA_F16: + return Config.RGBA_F16; + case RGBA_1010102: + return Config.RGBA_1010102; + default: + throw new IllegalArgumentException("Unknown colorType: " + colorType); + } + } + + public static Bitmap createBitmap(int width, int height) { + Bitmap bm = new Bitmap(); + bm.theBitmap.allocPixels(ImageInfo.Companion.makeS32(width, height, ColorAlphaType.PREMUL)); + + return bm; + } + + public static Bitmap createBitmap(int width, int height, Config config) { + Bitmap bm = new Bitmap(); + bm.theBitmap.allocPixels(new ImageInfo(width, height, configToColorType(config), ColorAlphaType.OPAQUE)); + + bm.theBitmap.erase(0); + + return bm; + } + + public final org.jetbrains.skia.Bitmap theBitmap; public Bitmap() { - theBitmap = new io.github.humbleui.skija.Bitmap(); + theBitmap = new org.jetbrains.skia.Bitmap(); + } + + public Bitmap(org.jetbrains.skia.Bitmap bm) { + theBitmap = bm; + } + + public int getWidth() { + return theBitmap.getWidth(); + } + + public int getHeight() { + return theBitmap.getHeight(); } + public Config getConfig() { + return colorTypeToConfig(theBitmap.getColorType()); + } + + public void recycle() { + theBitmap.close(); + } } diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java index 17404bb1..573484e6 100644 --- a/Vision/src/main/java/android/graphics/Canvas.java +++ b/Vision/src/main/java/android/graphics/Canvas.java @@ -23,16 +23,35 @@ package android.graphics; -import io.github.humbleui.skija.Font; -import io.github.humbleui.types.RRect; -import org.firstinspires.ftc.vision.apriltag.AprilTagCanvasAnnotator; +import org.jetbrains.skia.*; public class Canvas { - public final io.github.humbleui.skija.Canvas theCanvas; + public final org.jetbrains.skia.Canvas theCanvas; + + private Bitmap backingBitmap = null; + + final Object surfaceLock = new Object(); + private int providedWidth; + private int providedHeight; public Canvas(Bitmap bitmap) { - theCanvas = new io.github.humbleui.skija.Canvas(bitmap.theBitmap); + theCanvas = new org.jetbrains.skia.Canvas(bitmap.theBitmap, new SurfaceProps()); + backingBitmap = bitmap; + } + + public Canvas(Surface surface) { + theCanvas = surface.getCanvas(); + + providedWidth = surface.getWidth(); + providedHeight = surface.getHeight(); + } + + public Canvas(org.jetbrains.skia.Canvas skiaCanvas, int width, int height) { + theCanvas = skiaCanvas; + + providedWidth = width; + providedHeight = height; } public Canvas drawLine(float x, float y, float x1, float y1, Paint paint) { @@ -49,7 +68,7 @@ public Canvas drawRoundRect(float l, float t, float r, float b, float xRad, floa public Canvas drawText(String text, float x, float y, Paint paint) { - Font font = FontCache.makeFont(paint.getTypeface().theTypeface, paint.getTextSize()); + Font font = FontCache.makeFont(paint.getTypeface(), paint.getTextSize()); theCanvas.drawString(text, x, y, font, paint.thePaint); return this; @@ -78,4 +97,62 @@ public Canvas drawLines(float[] points, Paint paint) { theCanvas.drawLines(points, paint.thePaint); return this; } -} + + public void drawRect(Rect rect, Paint paint) { + theCanvas.drawRect(rect.toSkijaRect(), paint.thePaint); + } + + public void drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { + int left, top, right, bottom; + if (src == null) { + left = top = 0; + right = bitmap.getWidth(); + bottom = bitmap.getHeight(); + } else { + left = src.left; + top = src.top; + right = src.right; + bottom = src.bottom; + } + + org.jetbrains.skia.Paint thePaint = null; + + if(paint != null) { + thePaint = paint.thePaint; + } + + theCanvas.drawImageRect( + Image.Companion.makeFromBitmap(bitmap.theBitmap), + new org.jetbrains.skia.Rect(left, top, right, bottom), + rect.toSkijaRect(), thePaint + ); + } + + public void translate(int dx, int dy) { + theCanvas.translate(dx, dy); + } + + public void restoreToCount(int saveCount) { + theCanvas.restoreToCount(saveCount); + } + + public void drawColor(int color) { + theCanvas.clear(color); + } + + public int getWidth() { + if(backingBitmap != null) { + return backingBitmap.getWidth(); + } + + return providedWidth; + } + + public int getHeight() { + if(backingBitmap != null) { + return backingBitmap.getHeight(); + } + + return providedHeight; + } +} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/FontCache.java b/Vision/src/main/java/android/graphics/FontCache.java index 822d6a9a..052c090c 100644 --- a/Vision/src/main/java/android/graphics/FontCache.java +++ b/Vision/src/main/java/android/graphics/FontCache.java @@ -1,7 +1,29 @@ +/* + * Copyright (c) 2023 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 android.graphics; -import io.github.humbleui.skija.Font; -import io.github.humbleui.skija.Typeface; +import org.jetbrains.skia.Font; import java.util.HashMap; @@ -17,7 +39,7 @@ public static Font makeFont(Typeface theTypeface, float textSize) { HashMap sizeCache = cache.get(theTypeface); if(!sizeCache.containsKey((int) (textSize * 1000))) { - sizeCache.put((int) (textSize * 1000), new Font(theTypeface, textSize)); + sizeCache.put((int) (textSize * 1000), new Font(theTypeface.theTypeface, textSize)); } return sizeCache.get((int) (textSize * 1000)); diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java index 30314523..ce9a9ede 100644 --- a/Vision/src/main/java/android/graphics/Paint.java +++ b/Vision/src/main/java/android/graphics/Paint.java @@ -23,8 +23,8 @@ package android.graphics; -import io.github.humbleui.skija.PaintStrokeCap; -import io.github.humbleui.skija.PaintStrokeJoin; +import org.jetbrains.skia.PaintStrokeCap; +import org.jetbrains.skia.PaintStrokeJoin; public class Paint { @@ -142,13 +142,13 @@ private Align(int nativeInt) { final int nativeInt; } - public final io.github.humbleui.skija.Paint thePaint; + public final org.jetbrains.skia.Paint thePaint; private Typeface typeface; private float textSize; public Paint() { - thePaint = new io.github.humbleui.skija.Paint(); + thePaint = new org.jetbrains.skia.Paint(); } public Paint setColor(int color) { @@ -265,6 +265,10 @@ public float getStrokeMiter() { } public Typeface getTypeface() { + if(typeface == null) { + typeface = Typeface.DEFAULT; + } + return typeface; } diff --git a/Vision/src/main/java/android/graphics/Rect.java b/Vision/src/main/java/android/graphics/Rect.java index 8fda9a67..97c1aa4e 100644 --- a/Vision/src/main/java/android/graphics/Rect.java +++ b/Vision/src/main/java/android/graphics/Rect.java @@ -613,4 +613,8 @@ public void splitHorizontally(@NonNull Rect ...outSplits) { split.bottom = split.top + splitHeight; } } + + public org.jetbrains.skia.Rect toSkijaRect() { + return new org.jetbrains.skia.Rect(left, top, right, bottom); + } } \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Typeface.java b/Vision/src/main/java/android/graphics/Typeface.java index cfb40b06..b6967f6e 100644 --- a/Vision/src/main/java/android/graphics/Typeface.java +++ b/Vision/src/main/java/android/graphics/Typeface.java @@ -1,22 +1,44 @@ +/* + * Copyright (c) 2023 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 android.graphics; -import io.github.humbleui.skija.Font; -import io.github.humbleui.skija.FontMgr; -import io.github.humbleui.skija.FontStyle; +import org.jetbrains.skia.FontMgr; +import org.jetbrains.skia.FontStyle; public class Typeface { - public static Typeface DEFAULT = new Typeface(FontMgr.getDefault().matchFamilyStyle(null, FontStyle.NORMAL)); - public static Typeface DEFAULT_BOLD = new Typeface(FontMgr.getDefault().matchFamilyStyle(null, FontStyle.BOLD)); - public static Typeface DEFAULT_ITALIC = new Typeface(FontMgr.getDefault().matchFamilyStyle(null, FontStyle.ITALIC)); + public static Typeface DEFAULT = new Typeface(FontMgr.Companion.getDefault().matchFamilyStyle(null, FontStyle.Companion.getNORMAL())); + public static Typeface DEFAULT_BOLD = new Typeface(FontMgr.Companion.getDefault().matchFamilyStyle(null, FontStyle.Companion.getBOLD())); + public static Typeface DEFAULT_ITALIC = new Typeface(FontMgr.Companion.getDefault().matchFamilyStyle(null, FontStyle.Companion.getITALIC())); - public io.github.humbleui.skija.Typeface theTypeface; + public org.jetbrains.skia.Typeface theTypeface; public Typeface(long ptr) { - theTypeface = new io.github.humbleui.skija.Typeface(ptr); + theTypeface = new org.jetbrains.skia.Typeface(ptr); } - private Typeface(io.github.humbleui.skija.Typeface typeface) { + private Typeface(org.jetbrains.skia.Typeface typeface) { theTypeface = typeface; } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java new file mode 100644 index 00000000..594d7daf --- /dev/null +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java @@ -0,0 +1,113 @@ +package com.qualcomm.robotcore.eventloop.opmode; + +public abstract class LinearOpMode extends OpMode { + + public LinearOpMode() { + } + + //------------------------------------------------------------------------------------------------ + // Operations + //------------------------------------------------------------------------------------------------ + + /** + * Override this method and place your code here. + *

+ * Please do not swallow the InterruptedException, as it is used in cases + * where the op mode needs to be terminated early. + * @throws InterruptedException + */ + abstract public void runOpMode() throws InterruptedException; + + /** + * Pauses the Linear Op Mode until start has been pressed or until the current thread + * is interrupted. + */ + public void waitForStart() { + while (!isStarted() && !Thread.currentThread().isInterrupted()) { idle(); } + } + + /** + * Puts the current thread to sleep for a bit as it has nothing better to do. This allows other + * threads in the system to run. + * + *

One can use this method when you have nothing better to do in your code as you await state + * managed by other threads to change. Calling idle() is entirely optional: it just helps make + * the system a little more responsive and a little more efficient.

+ * + * @see #opModeIsActive() + */ + public final void idle() { + // Otherwise, yield back our thread scheduling quantum and give other threads at + // our priority level a chance to run + Thread.yield(); + } + + /** + * Sleeps for the given amount of milliseconds, or until the thread is interrupted. This is + * simple shorthand for the operating-system-provided {@link Thread#sleep(long) sleep()} method. + * + * @param milliseconds amount of time to sleep, in milliseconds + * @see Thread#sleep(long) + */ + public final void sleep(long milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Answer as to whether this opMode is active and the robot should continue onwards. If the + * opMode is not active, the OpMode should terminate at its earliest convenience. + * + *

Note that internally this method calls {@link #idle()}

+ * + * @return whether the OpMode is currently active. If this returns false, you should + * break out of the loop in your {@link #runOpMode()} method and return to its caller. + * @see #runOpMode() + * @see #isStarted() + * @see #isStopRequested() + */ + public final boolean opModeIsActive() { + boolean isActive = !this.isStopRequested() && this.isStarted(); + if (isActive) { + idle(); + } + return isActive; + } + + /** + * Can be used to break out of an Init loop when false is returned. Touching + * Start or Stop will return false. + * + * @return Whether the OpMode is currently in Init. A return of false can exit + * an Init loop and proceed with the next action. + */ + public final boolean opModeInInit() { + return !isStarted() && !isStopRequested(); + } + + /** + * Has the opMode been started? + * + * @return whether this opMode has been started or not + * @see #opModeIsActive() + * @see #isStopRequested() + */ + public final boolean isStarted() { + return this.isStarted || Thread.currentThread().isInterrupted(); + } + + /** + * Has the the stopping of the opMode been requested? + * + * @return whether stopping opMode has been requested or not + * @see #opModeIsActive() + * @see #isStarted() + */ + public final boolean isStopRequested() { + return this.stopRequested || Thread.currentThread().isInterrupted(); + } + +} diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java new file mode 100644 index 00000000..660c5739 --- /dev/null +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -0,0 +1,131 @@ +/* Copyright (c) 2014 Qualcomm Technologies Inc + +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 Qualcomm Technologies Inc 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 com.qualcomm.robotcore.eventloop.opmode; + +import com.qualcomm.robotcore.hardware.HardwareMap; +import io.github.deltacv.vision.util.FrameQueue; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.opencv.core.Mat; + +public abstract class OpMode extends TimestampedOpenCvPipeline { // never in my life would i have imagined... + + public Telemetry telemetry; + + volatile boolean isStarted = false; + volatile boolean stopRequested = false; + + protected FrameQueue inputQueue; + + public HardwareMap hardwareMap; + + public OpMode(int maxInputQueueCapacity) { + inputQueue = new FrameQueue(maxInputQueueCapacity); + } + + public OpMode() { + this(10); + } + + /* BEGIN OpMode abstract methods */ + + /** + * User defined init method + *

+ * This method will be called once when the INIT button is pressed. + */ + abstract public void init(); + + /** + * User defined init_loop method + *

+ * This method will be called repeatedly when the INIT button is pressed. + * This method is optional. By default this method takes no action. + */ + public void init_loop() {}; + + /** + * User defined start method. + *

+ * This method will be called once when the PLAY button is first pressed. + * This method is optional. By default this method takes not action. + * Example usage: Starting another thread. + * + */ + public void start() {}; + + /** + * User defined loop method + *

+ * This method will be called repeatedly in a loop while this op mode is running + */ + abstract public void loop(); + + /** + * User defined stop method + *

+ * This method will be called when this op mode is first disabled. + *

+ * The stop method is optional. By default this method takes no action. + */ + public void stop() {}; + + public void requestOpModeStop() { + stop(); + } + + /* BEGIN OpenCvPipeline Impl */ + + @Override + public final void init(Mat mat) { + init(); + } + + private boolean startCalled = false; + + @Override + public final Mat processFrame(Mat input, long captureTimeNanos) { + if(!startCalled) { + start(); + startCalled = true; + } + + loop(); + + return null; // OpModes don't actually show anything to the viewport, we'll delegate that + } + + @Override + public final void onViewportTapped() { + } + +} diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java new file mode 100644 index 00000000..19dee889 --- /dev/null +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java @@ -0,0 +1,83 @@ +/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc + +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 Qualcomm Technologies Inc 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 com.qualcomm.robotcore.hardware; + +/** + * Interface used by Hardware Devices + */ +public interface HardwareDevice { + + enum Manufacturer { + Unknown, Other, Lego, HiTechnic, ModernRobotics, Adafruit, Matrix, Lynx, AMS, STMicroelectronics, Broadcom + } + + /** + * Returns an indication of the manufacturer of this device. + * @return the device's manufacturer + */ + Manufacturer getManufacturer(); + + /** + * Returns a string suitable for display to the user as to the type of device. + * Note that this is a device-type-specific name; it has nothing to do with the + * name by which a user might have configured the device in a robot configuration. + * + * @return device manufacturer and name + */ + String getDeviceName(); + + /** + * Get connection information about this device in a human readable format + * + * @return connection info + */ + String getConnectionInfo(); + + /** + * Version + * + * @return get the version of this device + */ + int getVersion(); + + /** + * Resets the device's configuration to that which is expected at the beginning of an OpMode. + * For example, motors will reset the their direction to 'forward'. + */ + void resetDeviceConfigurationForOpMode(); + + /** + * Closes this device + */ + void close(); + +} \ No newline at end of file diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java index fe498118..2af68ca1 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java @@ -1,18 +1,12 @@ -package com.qualcomm.robotcore.external.hardware; +package com.qualcomm.robotcore.hardware; -import com.qualcomm.robotcore.hardware.camera.CameraName; -import io.github.deltacv.vision.util.FrameQueue; -import org.openftc.easyopencv.QueueOpenCvCamera; +import io.github.deltacv.vision.source.SourceHander; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.openftc.easyopencv.OpenCvViewport; public class HardwareMap { - private FrameQueue cameraFramesQueue; - private FrameQueue viewportOutputQueue; - - - public HardwareMap(FrameQueue cameraFramesQueue, FrameQueue viewportOutputQueue) { - this.cameraFramesQueue = cameraFramesQueue; - this.viewportOutputQueue = viewportOutputQueue; + public HardwareMap(SourceHander sourceHander, OpenCvViewport viewport) { } public static boolean hasSuperclass(Class clazz, Class superClass) { @@ -27,7 +21,7 @@ public static boolean hasSuperclass(Class clazz, Class superClass) { @SuppressWarnings("unchecked") public T get(Class classType, String deviceName) { if(hasSuperclass(classType, CameraName.class)) { - return (T) new QueueOpenCvCamera(cameraFramesQueue, viewportOutputQueue); + // return (T) new QueueOpenCvCamera(cameraFramesQueue, viewportOutputQueue); } return null; diff --git a/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java new file mode 100644 index 00000000..7863cbc5 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java @@ -0,0 +1,117 @@ +package io.github.deltacv.vision; + +import android.graphics.Canvas; +import io.github.deltacv.vision.source.Source; +import io.github.deltacv.vision.source.Sourced; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.openftc.easyopencv.*; + +public class SourcedOpenCvCamera extends OpenCvCameraBase implements Sourced { + + private final Source source; + OpenCvViewport handedViewport = null; + + boolean streaming = false; + + public SourcedOpenCvCamera(Source source, OpenCvViewport handedViewport) { + this.source = source; + this.handedViewport = handedViewport; + } + + @Override + public int openCameraDevice() { + return source.init(); + } + + @Override + public void openCameraDeviceAsync(AsyncCameraOpenListener cameraOpenListener) { + new Thread(() -> { + int code = openCameraDevice(); + + if(code == 0) { + cameraOpenListener.onOpened(); + } else { + cameraOpenListener.onError(code); + } + }).start(); + } + + @Override + public void closeCameraDevice() { + synchronized (source) { + source.close(); + } + } + + @Override + public void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener) { + new Thread(() -> { + closeCameraDevice(); + cameraCloseListener.onClose(); + }).start(); + } + + @Override + public void startStreaming(int width, int height) { + synchronized (source) { + source.start(new Size(width, height)); + source.attach(this); + } + + streaming = true; + } + + @Override + public void startStreaming(int width, int height, OpenCvCameraRotation rotation) { + startStreaming(width, height); + } + + @Override + public void stopStreaming() { + source.stop(); + } + + @Override + protected OpenCvViewport setupViewport() { + handedViewport.setRenderHook((canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, context) -> { + OpenCvViewport.FrameContext frameContext = (OpenCvViewport.FrameContext) context; + + // We must make sure that we call onDrawFrame() for the same pipeline which set the + // context object when requesting a draw hook. (i.e. we can't just call onDrawFrame() + // for whatever pipeline happens to be currently attached; it might have an entirely + // different notion of what to expect in the context object) + if (frameContext.generatingPipeline != null) + { + frameContext.generatingPipeline.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, frameContext.userContext); + } + }); + + return handedViewport; + } + + @Override + protected OpenCvCameraRotation getDefaultRotation() { + return OpenCvCameraRotation.UPRIGHT; + } + + @Override + protected int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation) { + return 0; + } + + @Override + protected boolean cameraOrientationIsTiedToDeviceOrientation() { + return false; + } + + @Override + protected boolean isStreaming() { + return false; + } + + @Override + public void onNewFrame(Mat frame, long timestamp) { + handleFrameUserCrashable(frame, timestamp); + } +} \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt b/Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt new file mode 100644 index 00000000..da75e32a --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt @@ -0,0 +1,34 @@ +package io.github.deltacv.vision.gui + +import org.jetbrains.skiko.ClipComponent +import org.jetbrains.skiko.SkiaLayer +import java.awt.Color +import java.awt.Component +import javax.swing.JLayeredPane + +class SkiaPanel(private val layer: SkiaLayer) : JLayeredPane() { + + init { + layout = null + background = Color.white + } + + override fun add(component: Component): Component { + layer.clipComponents.add(ClipComponent(component)) + return super.add(component, Integer.valueOf(0)) + } + + override fun doLayout() { + layer.setBounds(0, 0, width, height) + } + + override fun addNotify() { + super.addNotify() + super.add(layer, Integer.valueOf(10)) + } + + override fun removeNotify() { + layer.dispose() + super.removeNotify() + } +} \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt new file mode 100644 index 00000000..01498879 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2023 OpenFTC Team & 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.vision.gui + +import android.graphics.Bitmap +import android.graphics.Canvas +import io.github.deltacv.common.image.DynamicBufferedImageRecycler +import io.github.deltacv.common.image.MatPoster +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue +import org.jetbrains.skia.Color +import org.jetbrains.skiko.ExperimentalSkikoApi +import org.jetbrains.skiko.GenericSkikoView +import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.SkikoView +import org.jetbrains.skiko.swing.SkiaSwingLayer +import org.opencv.core.Mat +import org.opencv.core.Size +import org.openftc.easyopencv.MatRecycler +import org.openftc.easyopencv.OpenCvCamera.ViewportRenderingPolicy +import org.openftc.easyopencv.OpenCvViewRenderer +import org.openftc.easyopencv.OpenCvViewport +import org.openftc.easyopencv.OpenCvViewport.OptimizedRotation +import org.openftc.easyopencv.OpenCvViewport.RenderHook +import org.slf4j.LoggerFactory +import java.awt.GridBagLayout +import java.awt.image.BufferedImage +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.TimeUnit +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { + + private val syncObj = Any() + + @Volatile + private var userRequestedActive = false + + @Volatile + private var userRequestedPause = false + private val needToDeactivateRegardlessOfUser = false + private var surfaceExistsAndIsReady = false + + @Volatile + private var useGpuCanvas = false + + private enum class RenderingState { + STOPPED, + ACTIVE, + PAUSED + } + + private val visionPreviewFrameQueue = EvictingBlockingQueue(ArrayBlockingQueue(VISION_PREVIEW_FRAME_QUEUE_CAPACITY)) + private var framebufferRecycler: MatRecycler? = null + + @Volatile + private var internalRenderingState = RenderingState.STOPPED + val renderer: OpenCvViewRenderer = OpenCvViewRenderer(false) + + private val skiaLayer = SkiaLayer() + val component: JComponent get() = skiaLayer + + var logger = LoggerFactory.getLogger(javaClass) + + private var renderHook: RenderHook? = null + + init { + visionPreviewFrameQueue.setEvictAction { value: MatRecycler.RecyclableMat? -> + /* + * If a Mat is evicted from the queue, we need + * to make sure to return it to the Mat recycler + */ + framebufferRecycler!!.returnMat(value) + } + + skiaLayer.skikoView = GenericSkikoView(skiaLayer, object: SkikoView { + override fun onRender(canvas: org.jetbrains.skia.Canvas, width: Int, height: Int, nanoTime: Long) { + // renderCanvas(Canvas(canvas, width, height)) + + canvas.clear(Color.BLUE) + } + }) + + setSize(size.width.toInt(), size.height.toInt()) + } + + var shouldPaintOrange = true + + fun attachTo(component: Any) { + skiaLayer.attachTo(component) + + SwingUtilities.invokeLater { + skiaLayer.needRedraw() + } + } + + fun skiaPanel() = SkiaPanel(skiaLayer) + + override fun setSize(width: Int, height: Int) { + synchronized(syncObj) { + check(internalRenderingState == RenderingState.STOPPED) { "Cannot set size while renderer is active!" } + + //Make sure we don't have any mats hanging around + //from when we might have been running before + visionPreviewFrameQueue.clear() + framebufferRecycler = MatRecycler(FRAMEBUFFER_RECYCLER_CAPACITY) + + skiaLayer.setSize(width, height) + + surfaceExistsAndIsReady = true + checkState() + } + } + + override fun setFpsMeterEnabled(enabled: Boolean) {} + override fun resume() { + synchronized(syncObj) { + userRequestedPause = false + checkState() + } + } + + override fun pause() { + synchronized(syncObj) { + userRequestedPause = true + checkState() + } + } + + /*** + * Activate the render thread + */ + @Synchronized + override fun activate() { + synchronized(syncObj) { + userRequestedActive = true + checkState() + } + } + + /*** + * Deactivate the render thread + */ + override fun deactivate() { + synchronized(syncObj) { + userRequestedActive = false + checkState() + } + } + + override fun setOptimizedViewRotation(rotation: OptimizedRotation) {} + override fun notifyStatistics(fps: Float, pipelineMs: Int, overheadMs: Int) { + renderer.notifyStatistics(fps, pipelineMs, overheadMs) + } + + override fun setRecording(recording: Boolean) {} + override fun post(mat: Mat, userContext: Any) { + synchronized(syncObj) { + //did they give us null? + requireNotNull(mat) { + //ugh, they did + "cannot post null mat!" + } + + //Are we actually rendering to the display right now? If not, + //no need to waste time doing a memcpy + if (internalRenderingState == RenderingState.ACTIVE) { + /* + * We need to copy this mat before adding it to the queue, + * because the pointer that was passed in here is only known + * to be pointing to a certain frame while we're executing. + */ + + /* + * Grab a framebuffer Mat from the recycler + * instead of doing a new alloc and then having + * to free it after rendering/eviction from queue + */ + val matToCopyTo = framebufferRecycler!!.takeMat() + + mat.copyTo(matToCopyTo) + matToCopyTo.context = userContext + + visionPreviewFrameQueue.offer(matToCopyTo) + } + } + } + + /* + * Called with syncObj held + */ + fun checkState() { + /* + * If the surface isn't ready, don't do anything + */ + if (!surfaceExistsAndIsReady) { + logger.info("CheckState(): surface not ready or doesn't exist") + return + } + + /* + * Does the user want us to stop? + */if (!userRequestedActive || needToDeactivateRegardlessOfUser) { + if (needToDeactivateRegardlessOfUser) { + logger.info("CheckState(): lifecycle mandates deactivation regardless of user") + } else { + logger.info("CheckState(): user requested that we deactivate") + } + + /* + * We only need to stop the render thread if it's not + * already stopped + */if (internalRenderingState != RenderingState.STOPPED) { + logger.info("CheckState(): deactivating viewport") + + /* + * Wait for him to die non-interuptibly + */ + internalRenderingState = RenderingState.STOPPED + } else { + logger.info("CheckState(): already deactivated") + } + } else if (userRequestedActive) { + logger.info("CheckState(): user requested that we activate") + + /* + * We only need to start the render thread if it's + * stopped. + */if (internalRenderingState == RenderingState.STOPPED) { + logger.info("CheckState(): activating viewport") + internalRenderingState = RenderingState.PAUSED + internalRenderingState = if (userRequestedPause) { + RenderingState.PAUSED + } else { + RenderingState.ACTIVE + } + } else { + logger.info("CheckState(): already activated") + } + } + if (internalRenderingState != RenderingState.STOPPED) { + if (userRequestedPause && internalRenderingState != RenderingState.PAUSED + || !userRequestedPause && internalRenderingState != RenderingState.ACTIVE) { + internalRenderingState = if (userRequestedPause) { + logger.info("CheckState(): pausing viewport") + RenderingState.PAUSED + } else { + logger.info("CheckState(): resuming viewport") + RenderingState.ACTIVE + } + + /* + * Interrupt him so that he's not stuck looking at his frame queue. + * (We stop filling the frame queue if the user requested pause so + * we aren't doing pointless memcpys) + */ + } + } + } + + + private fun renderCanvas(canvas: Canvas) { + when (internalRenderingState) { + RenderingState.ACTIVE -> { + shouldPaintOrange = true + + var mat: MatRecycler.RecyclableMat = try { + //Grab a Mat from the frame queue + val frame = visionPreviewFrameQueue.poll(10, TimeUnit.MILLISECONDS) ?: return; + + frame + } catch (e: InterruptedException) { + + //Note: we actually don't re-interrupt ourselves here, because interrupts are also + //used to simply make sure we properly pick up a transition to the PAUSED state, not + //just when we're trying to close. If we're trying to close, then exitRequested will + //be set, and since we break immediately right here, the close will be handled cleanly. + //Thread.currentThread().interrupt(); + return + } + + /* + * For some reason, the canvas will very occasionally be null upon closing. + * Stack Overflow seems to suggest this means the canvas has been destroyed. + * However, surfaceDestroyed(), which is called right before the surface is + * destroyed, calls checkState(), which *SHOULD* block until we die. This + * works most of the time, but not always? We don't yet understand... + */ + if (canvas != null) { + renderer.render(mat, canvas, renderHook, mat.context) + } else { + logger.info("Canvas was null") + } + + //We're done with that Mat object; return it to the Mat recycler so it can be used again later + framebufferRecycler!!.returnMat(mat) + } + + RenderingState.PAUSED -> { + if (shouldPaintOrange) { + shouldPaintOrange = false + + /* + * For some reason, the canvas will very occasionally be null upon closing. + * Stack Overflow seems to suggest this means the canvas has been destroyed. + * However, surfaceDestroyed(), which is called right before the surface is + * destroyed, calls checkState(), which *SHOULD* block until we die. This + * works most of the time, but not always? We don't yet understand... + */ + if (canvas != null) { + renderer.renderPaused(canvas) + } + } + } + + else -> {} + } + } + + override fun setRenderingPolicy(policy: ViewportRenderingPolicy) {} + override fun setRenderHook(renderHook: RenderHook) { + this.renderHook = renderHook + } + + companion object { + private const val VISION_PREVIEW_FRAME_QUEUE_CAPACITY = 2 + private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 2 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1). + } +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java b/Vision/src/main/java/io/github/deltacv/vision/gui/component/ImageX.java similarity index 86% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java rename to Vision/src/main/java/io/github/deltacv/vision/gui/component/ImageX.java index 7a79b711..2bf0a290 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/component/ImageX.java @@ -1,88 +1,87 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.gui.component; - -import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import org.opencv.core.Mat; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; - -public class ImageX extends JLabel { - - volatile ImageIcon icon; - - public ImageX() { - super(); - } - - public ImageX(ImageIcon img) { - this(); - setImage(img); - } - - public ImageX(BufferedImage img) { - this(); - setImage(img); - } - - public void setImage(ImageIcon img) { - if (icon != null) - icon.getImage().flush(); //flush old image :p - - icon = img; - - setIcon(icon); //set to the new image - } - - public synchronized void setImage(BufferedImage img) { - Graphics2D g2d = (Graphics2D) getGraphics(); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - setImage(new ImageIcon(img)); //set to the new image - } - - public synchronized void setImageMat(Mat m) { - setImage(CvUtil.matToBufferedImage(m)); - } - - public synchronized BufferedImage getImage() { - return (BufferedImage)icon.getImage(); - } - - @Override - public void setSize(int width, int height) { - super.setSize(width, height); - setImage(GuiUtil.scaleImage(icon, width, height)); //set to the new image - } - - @Override - public void setSize(Dimension dimension) { - super.setSize(dimension); - setImage(GuiUtil.scaleImage(icon, (int)dimension.getWidth(), (int)dimension.getHeight())); //set to the new image - } - -} +/* + * Copyright (c) 2021 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.vision.gui.component; + +import io.github.deltacv.vision.gui.util.ImgUtil; +import io.github.deltacv.vision.util.CvUtil; +import org.opencv.core.Mat; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class ImageX extends JLabel { + volatile ImageIcon icon; + + public ImageX() { + super(); + } + + public ImageX(ImageIcon img) { + this(); + setImage(img); + } + + public ImageX(BufferedImage img) { + this(); + setImage(img); + } + + public void setImage(ImageIcon img) { + if (icon != null) + icon.getImage().flush(); //flush old image :p + + icon = img; + + setIcon(icon); //set to the new image + } + + public synchronized void setImage(BufferedImage img) { + Graphics2D g2d = (Graphics2D) getGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + setImage(new ImageIcon(img)); //set to the new image + } + + public synchronized void setImageMat(Mat m) { + setImage(CvUtil.matToBufferedImage(m)); + } + + public synchronized BufferedImage getImage() { + return (BufferedImage)icon.getImage(); + } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + setImage(ImgUtil.scaleImage(icon, width, height)); //set to the new image + } + + @Override + public void setSize(Dimension dimension) { + super.setSize(dimension); + setImage(ImgUtil.scaleImage(icon, (int)dimension.getWidth(), (int)dimension.getHeight())); //set to the new image + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java b/Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java new file mode 100644 index 00000000..49199cbb --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java @@ -0,0 +1,28 @@ +package io.github.deltacv.vision.gui.util; + +import javax.swing.*; +import java.awt.*; + +public class ImgUtil { + + private ImgUtil() { } + + public static ImageIcon scaleImage(ImageIcon icon, int w, int h) { + + int nw = icon.getIconWidth(); + int nh = icon.getIconHeight(); + + if (icon.getIconWidth() > w) { + nw = w; + nh = (nw * icon.getIconHeight()) / icon.getIconWidth(); + } + + if (nh > h) { + nh = h; + nw = (icon.getIconWidth() * nh) / icon.getIconHeight(); + } + + return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_SMOOTH)); + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/source/Source.java b/Vision/src/main/java/io/github/deltacv/vision/source/Source.java new file mode 100644 index 00000000..ca81a70a --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/source/Source.java @@ -0,0 +1,18 @@ +package io.github.deltacv.vision.source; + +import org.opencv.core.Size; + +public interface Source { + + int init(); + + boolean start(Size requestedSize); + + boolean attach(Sourced sourced); + boolean remove(Sourced sourced); + + boolean stop(); + + boolean close(); + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java new file mode 100644 index 00000000..e73ed6ba --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java @@ -0,0 +1,7 @@ +package io.github.deltacv.vision.source; + +public interface SourceHander { + + Source hand(String name); + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java b/Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java new file mode 100644 index 00000000..a837565a --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java @@ -0,0 +1,7 @@ +package io.github.deltacv.vision.source; + +import org.opencv.core.Mat; + +public interface Sourced { + void onNewFrame(Mat frame, long timestamp); +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java b/Vision/src/main/java/io/github/deltacv/vision/util/CvUtil.java similarity index 98% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java rename to Vision/src/main/java/io/github/deltacv/vision/util/CvUtil.java index 491cc7cf..f1628a5c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java +++ b/Vision/src/main/java/io/github/deltacv/vision/util/CvUtil.java @@ -21,9 +21,9 @@ * */ -package com.github.serivesmejia.eocvsim.util; +package io.github.deltacv.vision.util; -import com.github.serivesmejia.eocvsim.util.extension.CvExt; +import io.github.deltacv.vision.util.extension.CvExt; import org.opencv.core.Mat; import org.opencv.core.MatOfByte; import org.opencv.core.Size; diff --git a/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java b/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java index 29682cd9..312bd60e 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java +++ b/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java @@ -1,2 +1,49 @@ -package io.github.deltacv.vision.util;public class FrameQueue { +package io.github.deltacv.vision.util; + +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; +import org.opencv.core.Mat; +import org.openftc.easyopencv.MatRecycler; + +import java.util.concurrent.ArrayBlockingQueue; + +public class FrameQueue { + + private final EvictingBlockingQueue viewportQueue; + private final MatRecycler matRecycler; + + public FrameQueue(int maxQueueItems) { + viewportQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); + matRecycler = new MatRecycler(maxQueueItems + 2); + + viewportQueue.setEvictAction(this::evict); + } + + public Mat takeMatAndPost() { + Mat mat = matRecycler.takeMat(); + viewportQueue.add(mat); + + return mat; + } + + + public Mat takeMat() { + return matRecycler.takeMat(); + } + + public Mat poll() { + Mat mat = viewportQueue.poll(); + + if(mat != null) { + evict(mat); + } + + return mat; + } + + private void evict(Mat mat) { + if(mat instanceof MatRecycler.RecyclableMat) { + matRecycler.returnMat((MatRecycler.RecyclableMat) mat); + } + } + } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt b/Vision/src/main/java/io/github/deltacv/vision/util/extension/CvExt.kt similarity index 94% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt rename to Vision/src/main/java/io/github/deltacv/vision/util/extension/CvExt.kt index c192ca07..91c59ee3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/util/extension/CvExt.kt @@ -1,52 +1,52 @@ -/* - * Copyright (c) 2021 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. - * - */ -@file:JvmName("CvExt") - -package com.github.serivesmejia.eocvsim.util.extension - -import com.qualcomm.robotcore.util.Range -import org.opencv.core.CvType -import org.opencv.core.Mat -import org.opencv.core.Scalar -import org.opencv.core.Size -import org.opencv.imgproc.Imgproc - -fun Scalar.cvtColor(code: Int): Scalar { - val mat = Mat(5, 5, CvType.CV_8UC3); - mat.setTo(this) - Imgproc.cvtColor(mat, mat, code); - - val newScalar = Scalar(mat.get(1, 1)) - mat.release() - - return newScalar -} - -fun Size.aspectRatio() = height / width -fun Mat.aspectRatio() = size().aspectRatio() - -fun Size.clipTo(size: Size): Size { - width = Range.clip(width, 0.0, size.width) - height = Range.clip(height, 0.0, size.height) - return this +/* + * Copyright (c) 2021 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. + * + */ +@file:JvmName("CvExt") + +package io.github.deltacv.vision.util.extension + +import com.qualcomm.robotcore.util.Range +import org.opencv.core.CvType +import org.opencv.core.Mat +import org.opencv.core.Scalar +import org.opencv.core.Size +import org.opencv.imgproc.Imgproc + +fun Scalar.cvtColor(code: Int): Scalar { + val mat = Mat(5, 5, CvType.CV_8UC3); + mat.setTo(this) + Imgproc.cvtColor(mat, mat, code); + + val newScalar = Scalar(mat.get(1, 1)) + mat.release() + + return newScalar +} + +fun Size.aspectRatio() = height / width +fun Mat.aspectRatio() = size().aspectRatio() + +fun Size.clipTo(size: Size): Size { + width = Range.clip(width, 0.0, size.width) + height = Range.clip(height, 0.0, size.height) + return this } \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java index 29f1f12b..649f56c1 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java @@ -66,13 +66,13 @@ public interface CameraCharacteristics *

The {@code format} should be a supported format (one of the formats returned by * {@link #getAndroidFormats}).

* - * @param androidFormat an image format from {@link ImageFormat} or {@link PixelFormat} + * @param androidFormat an image format from {link ImageFormat} or {link PixelFormat} * @return * an array of supported sizes, * or {@code null} if the {@code format} is not a supported output * - * @see ImageFormat - * @see PixelFormat + * see ImageFormat + * see PixelFormat * @see #getAndroidFormats */ Size[] getSizes(int androidFormat); @@ -86,7 +86,7 @@ public interface CameraCharacteristics *

{@code format} should be one of the ones returned by {@link #getAndroidFormats()}.

*

{@code size} should be one of the ones returned by {@link #getSizes(int)}.

* - * @param androidFormat an image format from {@link ImageFormat} or {@link PixelFormat} + * @param androidFormat an image format from {link ImageFormat} or {link PixelFormat} * @param size an output-compatible size * @return a minimum frame duration {@code >} 0 in nanoseconds, or * 0 if the minimum frame duration is not available. @@ -94,8 +94,8 @@ public interface CameraCharacteristics * @throws IllegalArgumentException if {@code format} or {@code size} was not supported * @throws NullPointerException if {@code size} was {@code null} * - * @see ImageFormat - * @see PixelFormat + * see ImageFormat + * see PixelFormat */ long getMinFrameDuration(int androidFormat, Size size); diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java index 86bd191c..42ffc7ed 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java @@ -30,15 +30,11 @@ are permitted (subject to the limitations in the disclaimer below) provided that 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 com.qualcomm.robotcore.external.hardware.camera; - -import android.content.Context; +package org.firstinspires.ftc.robotcore.external.hardware.camera; import com.qualcomm.robotcore.hardware.HardwareDevice; import org.firstinspires.ftc.robotcore.external.function.Consumer; -import org.firstinspires.ftc.robotcore.external.function.Continuation; -import org.firstinspires.ftc.robotcore.internal.system.Deadline; /** * {@link CameraName} identifies a {@link HardwareDevice} which is a camera. @@ -78,38 +74,6 @@ public interface CameraName */ boolean isUnknown(); - /** - * Requests from the user permission to use the camera if same has not already been granted. - * This may take a long time, as interaction with the user may be necessary. When the outcome - * is known, the reportResult continuation is called with the result. The report may occur either - * before or after the call to {@link #asyncRequestCameraPermission} has itself returned. The report will - * be delivered using the indicated {@link Continuation} - * - * @param context the context in which the permission request should run - * @param deadline the time by which the request must be honored or given up as ungranted. - * If this {@link Deadline} is cancelled while the request is outstanding, - * then the permission request will be aborted and false reported as - * the result of the request. - * @param continuation the dispatcher used to deliver results of the permission request - * - * @throws IllegalArgumentException if the cameraName does not match any known camera device. - * - * @see #requestCameraPermission - */ - void asyncRequestCameraPermission(Context context, Deadline deadline, final Continuation> continuation); - - /** - * Requests from the user permission to use the camera if same has not already been granted. - * This may take a long time, as interaction with the user may be necessary. The call is made - * synchronously: the calling thread blocks until an answer is obtained. - * - * @param deadline the time by which the request must be honored or given up as ungranted - * @return whether or not permission to use the camera has been granted. - * - * @see #asyncRequestCameraPermission - */ - boolean requestCameraPermission(Deadline deadline); - /** *

Query the capabilities of a camera device. These capabilities are * immutable for a given camera.

diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java index 84200276..bcd73251 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java @@ -1,4 +1,4 @@ -package com.qualcomm.robotcore.external.hardware.camera; +package org.firstinspires.ftc.robotcore.external.hardware.camera; public class WebcamName { } diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa new file mode 100644 index 00000000..8c8f17b2 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2023 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; + +import android.util.Size; + +import java.util.ArrayList; +import java.util.List; + +import org.firstinspires.ftc.robotcore.external.ClassFactory; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; +import org.openftc.easyopencv.OpenCvCameraFactory; +import org.openftc.easyopencv.OpenCvWebcam; + +public abstract class VisionPortal +{ + + private static final int DEFAULT_VIEW_CONTAINER_ID = 0; + + /** + * StreamFormat is only applicable if using a webcam + */ + public enum StreamFormat + { + /** The only format that was supported historically; it is uncompressed but + * chroma subsampled and uses lots of bandwidth - this limits frame rate + * at higher resolutions and also limits the ability to use two cameras + * on the same bus to lower resolutions + */ + YUY2(OpenCvWebcam.StreamFormat.YUY2), + + /** Compressed motion JPEG stream format; allows for higher resolutions at + * full frame rate, and better ability to use two cameras on the same bus. + * Requires extra CPU time to run decompression routine. + */ + MJPEG(OpenCvWebcam.StreamFormat.MJPEG); + + final OpenCvWebcam.StreamFormat eocvStreamFormat; + + StreamFormat(OpenCvWebcam.StreamFormat eocvStreamFormat) + { + this.eocvStreamFormat = eocvStreamFormat; + } + } + + /** + * If you are using multiple vision portals with live previews concurrently, + * you need to split up the screen to make room for both portals + */ + public enum MultiPortalLayout + { + /** + * Divides the screen vertically + */ + VERTICAL(OpenCvCameraFactory.ViewportSplitMethod.VERTICALLY), + + /** + * Divides the screen horizontally + */ + HORIZONTAL(OpenCvCameraFactory.ViewportSplitMethod.HORIZONTALLY); + + private final OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod; + + MultiPortalLayout(OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod) + { + this.viewportSplitMethod = viewportSplitMethod; + } + } + + /** + * Split up the screen for using multiple vision portals with live views simultaneously + * @param numPortals the number of portals to create space for on the screen + * @param mpl the methodology for laying out the multiple live views on the screen + * @return an array of view IDs, whose elements may be passed to {@link Builder#setCameraMonitorViewId(int)} + */ + public static int[] makeMultiPortalView(int numPortals, MultiPortalLayout mpl) + { + return OpenCvCameraFactory.getInstance().splitLayoutForMultipleViewports( + DEFAULT_VIEW_CONTAINER_ID, numPortals, mpl.viewportSplitMethod + ); + } + + /** + * Create a VisionPortal for an internal camera using default configuration parameters, and + * skipping the use of the {@link Builder} pattern. + * @param cameraDirection the internal camera to use + * @param processors all the processors you want to inject into the portal + * @return a configured, ready to use VisionPortal + */ + public static VisionPortal easyCreateWithDefaults(BuiltinCameraDirection cameraDirection, VisionProcessor... processors) + { + return new Builder() + .setCamera(cameraDirection) + .addProcessors(processors) + .build(); + } + + /** + * Create a VisionPortal for a webcam using default configuration parameters, and + * skipping the use of the {@link Builder} pattern. + * @param processors all the processors you want to inject into the portal + * @return a configured, ready to use VisionPortal + */ + public static VisionPortal easyCreateWithDefaults(CameraName cameraName, VisionProcessor... processors) + { + return new Builder() + .setCamera(cameraName) + .addProcessors(processors) + .build(); + } + + public static class Builder + { + // STATIC ! + private static final ArrayList attachedProcessors = new ArrayList<>(); + + private CameraName camera; + private int cameraMonitorViewId = DEFAULT_VIEW_CONTAINER_ID; // 0 == none + private boolean autoStopLiveView = true; + private Size cameraResolution = new Size(640, 480); + private StreamFormat streamFormat = null; + private StreamFormat STREAM_FORMAT_DEFAULT = StreamFormat.YUY2; + private final List processors = new ArrayList<>(); + + /** + * Configure the portal to use a webcam + * @param camera the WebcamName of the camera to use + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCamera(CameraName camera) + { + this.camera = camera; + return this; + } + + /** + * Configure the portal to use an internal camera + * @param cameraDirection the internal camera to use + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCamera(BuiltinCameraDirection cameraDirection) + { + this.camera = ClassFactory.getInstance().getCameraManager().nameFromCameraDirection(cameraDirection); + return this; + } + + /** + * Configure the vision portal to stream from the camera in a certain image format + * THIS APPLIES TO WEBCAMS ONLY! + * @param streamFormat the desired streaming format + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setStreamFormat(StreamFormat streamFormat) + { + this.streamFormat = streamFormat; + return this; + } + + /** + * Configure the vision portal to use (or not to use) a live camera preview + * @param enableLiveView whether or not to use a live preview + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder enableCameraMonitoring(boolean enableLiveView) + { + // dont care + l + ratio + + return setCameraMonitorViewId(viewId); + } + + /** + * Configure whether the portal should automatically pause the live camera + * view if all attached processors are disabled; this can save computational resources + * @param autoPause whether to enable this feature or not + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setAutoStopLiveView(boolean autoPause) + { + this.autoStopLiveView = autoPause; + return this; + } + + /** + * A more advanced version of {@link #enableCameraMonitoring(boolean)}; allows you + * to specify a specific view ID to use as a container, rather than just using the default one + * @param cameraMonitorViewId view ID of container for live view + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCameraMonitorViewId(int cameraMonitorViewId) + { + this.cameraMonitorViewId = cameraMonitorViewId; + return this; + } + + /** + * Specify the resolution in which to stream images from the camera. To find out what resolutions + * your camera supports, simply call this with some random numbers (e.g. new Size(4634, 11115)) + * and the error message will provide a list of supported resolutions. + * @param cameraResolution the resolution in which to stream images from the camera + * @return the {@link Builder} object, to allow for method chaining + */ + public Builder setCameraResolution(Size cameraResolution) + { + this.cameraResolution = cameraResolution; + return this; + } + + /** + * Send a {@link VisionProcessor} into this portal to allow it to process camera frames. + * @param processor the processor to attach + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if the specified processor is already inside another portal + */ + public Builder addProcessor(VisionProcessor processor) + { + synchronized (attachedProcessors) + { + if (attachedProcessors.contains(processor)) + { + throw new RuntimeException("This VisionProcessor has already been attached to a VisionPortal, either a different one or perhaps even this same portal."); + } + else + { + attachedProcessors.add(processor); + } + } + + processors.add(processor); + return this; + } + + /** + * Send multiple {@link VisionProcessor}s into this portal to allow them to process camera frames. + * @param processors the processors to attach + * @return the {@link Builder} object, to allow for method chaining + * @throws RuntimeException if the specified processor is already inside another portal + */ + public Builder addProcessors(VisionProcessor... processors) + { + for (VisionProcessor p : processors) + { + addProcessor(p); + } + + return this; + } + + /** + * Actually create the {@link VisionPortal} i.e. spool up the camera and live view + * and begin sending image data to any attached {@link VisionProcessor}s + * @return a configured, ready to use portal + * @throws RuntimeException if you didn't specify what camera to use + * @throws IllegalStateException if you tried to set the stream format when not using a webcam + */ + public VisionPortal build() + { + if (camera == null) + { + throw new RuntimeException("You can't build a vision portal without setting a camera!"); + } + + if (streamFormat != null) + { + if (!camera.isWebcam() && !camera.isSwitchable()) + { + throw new IllegalStateException("setStreamFormat() may only be used with a webcam"); + } + } + else + { + // Only used with webcams, will be ignored for internal camera + streamFormat = STREAM_FORMAT_DEFAULT; + } + + return new VisionPortalImpl( + camera, cameraMonitorViewId, autoStopLiveView, cameraResolution, streamFormat, + processors.toArray(new VisionProcessor[0])); + } + } + + /** + * Enable or disable a {@link VisionProcessor} that is attached to this portal. + * Disabled processors are not passed new image data and do not consume any computational + * resources. Of course, they also don't give you any useful data when disabled. + * This takes effect immediately (on the next frame interval) + * @param processor the processor to enable or disable + * @param enabled should it be enabled or disabled? + * @throws IllegalArgumentException if the processor specified isn't inside this portal + */ + public abstract void setProcessorEnabled(VisionProcessor processor, boolean enabled); + + /** + * Queries whether a given processor is enabled + * @param processor the processor in question + * @return whether the processor in question is enabled + * @throws IllegalArgumentException if the processor specified isn't inside this portal + */ + public abstract boolean getProcessorEnabled(VisionProcessor processor); + + /** + * The various states that the camera may be in at any given time + */ + public enum CameraState + { + /** + * The camera device handle is being opened + */ + OPENING_CAMERA_DEVICE, + + /** + * The camera device handle has been opened and the camera + * is now ready to start streaming + */ + CAMERA_DEVICE_READY, + + /** + * The camera stream is starting + */ + STARTING_STREAM, + + /** + * The camera streaming session is in flight and providing image data + * to any attached {@link VisionProcessor}s + */ + STREAMING, + + /** + * The camera stream is being shut down + */ + STOPPING_STREAM, + + /** + * The camera device handle is being closed + */ + CLOSING_CAMERA_DEVICE, + + /** + * The camera device handle has been closed; you must create a new + * portal if you wish to use the camera again + */ + CAMERA_DEVICE_CLOSED, + + /** + * The camera was having a bad day and refused to cooperate with configuration for either + * opening the device handle or starting the streaming session + */ + ERROR + } + + /** + * Query the current state of the camera (e.g. is a streaming session in flight?) + * @return the current state of the camera + */ + public abstract CameraState getCameraState(); + + public abstract void saveNextFrameRaw(String filename); + + /** + * Stop the streaming session. This is an asynchronous call which does not take effect + * immediately. You may use {@link #getCameraState()} to monitor for when this command + * has taken effect. If you call {@link #resumeStreaming()} before the operation is complete, + * it will SYNCHRONOUSLY await completion of the stop command + * + * Stopping the streaming session is a good way to save computational resources if there may + * be long (e.g. 10+ second) periods of match play in which vision processing is not required. + * When streaming is stopped, no new image data is acquired from the camera and any attached + * {@link VisionProcessor}s will lie dormant until such time as {@link #resumeStreaming()} is called. + * + * Stopping and starting the stream can take a second or two, and thus is not advised for use + * cases where instantaneously enabling/disabling vision processing is required. + */ + public abstract void stopStreaming(); + + /** + * Resume the streaming session if previously stopped by {@link #stopStreaming()}. This is + * an asynchronous call which does not take effect immediately. If you call {@link #stopStreaming()} + * before the operation is complete, it will SYNCHRONOUSLY await completion of the resume command. + * + * See notes about use case on {@link #stopStreaming()} + */ + public abstract void resumeStreaming(); + + /** + * Temporarily stop the live view on the RC screen. This DOES NOT affect the ability to get + * a camera frame on the Driver Station's "Camera Stream" feature. + * + * This has no effect if you didn't set up a live view. + * + * Stopping the live view is recommended during competition to save CPU resources when + * a live view is not required for debugging purposes. + */ + public abstract void stopLiveView(); + + /** + * Start the live view again, if it was previously stopped with {@link #stopLiveView()} + * + * This has no effect if you didn't set up a live view. + */ + public abstract void resumeLiveView(); + + /** + * Get the current rate at which frames are passing through the vision portal + * (and all processors therein) per second - frames per second + * @return the current vision frame rate in frames per second + */ + public abstract float getFps(); + + /** + * Get a camera control handle + * ONLY APPLICABLE TO WEBCAMS + * @param controlType the type of control to get + * @return the requested control + * @throws UnsupportedOperationException if you are not using a webcam + */ + public abstract T getCameraControl(Class controlType); + + /** + * Switches the active camera to the indicated camera. + * ONLY APPLICABLE IF USING A SWITCHABLE WEBCAM + * @param webcamName the name of the to-be-activated camera + * @throws UnsupportedOperationException if you are not using a switchable webcam + */ + public abstract void setActiveCamera(WebcamName webcamName); + + /** + * Returns the name of the currently active camera + * ONLY APPLIES IF USING A SWITCHABLE WEBCAM + * @return the name of the currently active camera + * @throws UnsupportedOperationException if you are not using a switchable webcam + */ + public abstract WebcamName getActiveCamera(); + + /** + * Teardown everything prior to the end of the OpMode (perhaps to save resources) at which point + * it will be torn down automagically anyway. + * + * This will stop all vision related processing, shut down the camera, and remove the live view. + * A closed portal may not be re-opened: if you wish to use the camera again, you must make a new portal + */ + public abstract void close(); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java deleted file mode 100644 index 4c1d560f..00000000 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java +++ /dev/null @@ -1,2 +0,0 @@ -package org.firstinspires.ftc.vision;public class VisionPortalImpl { -} diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa new file mode 100644 index 00000000..f4b55fae --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2023 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; + +import android.graphics.Canvas; +import android.util.Size; + +import com.qualcomm.robotcore.util.RobotLog; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationHelper; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationIdentity; +import org.firstinspires.ftc.robotcore.internal.camera.delegating.SwitchableCameraName; +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvCamera; +import org.openftc.easyopencv.OpenCvCameraFactory; +import org.openftc.easyopencv.OpenCvCameraRotation; +import org.openftc.easyopencv.OpenCvInternalCamera; +import org.openftc.easyopencv.OpenCvSwitchableWebcam; +import org.openftc.easyopencv.OpenCvWebcam; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +public class VisionPortalImpl extends VisionPortal +{ + protected OpenCvCamera camera; + protected volatile CameraState cameraState = CameraState.CAMERA_DEVICE_CLOSED; + protected VisionProcessor[] processors; + protected volatile boolean[] processorsEnabled; + protected volatile CameraCalibration calibration; + protected final boolean autoPauseCameraMonitor; + protected final Object userStateMtx = new Object(); + protected final Size cameraResolution; + protected final StreamFormat webcamStreamFormat; + protected static final OpenCvCameraRotation CAMERA_ROTATION = OpenCvCameraRotation.SENSOR_NATIVE; + protected String captureNextFrame; + protected final Object captureFrameMtx = new Object(); + + public VisionPortalImpl(CameraName camera, int cameraMonitorViewId, boolean autoPauseCameraMonitor, Size cameraResolution, StreamFormat webcamStreamFormat, VisionProcessor[] processors) + { + this.processors = processors; + this.cameraResolution = cameraResolution; + this.webcamStreamFormat = webcamStreamFormat; + processorsEnabled = new boolean[processors.length]; + + for (int i = 0; i < processors.length; i++) + { + processorsEnabled[i] = true; + } + + this.autoPauseCameraMonitor = autoPauseCameraMonitor; + + createCamera(camera, cameraMonitorViewId); + startCamera(); + } + + protected void startCamera() + { + if (camera == null) + { + throw new IllegalStateException("This should never happen"); + } + + if (cameraResolution == null) // was the user a silly silly + { + throw new IllegalArgumentException("parameters.cameraResolution == null"); + } + + camera.setViewportRenderer(OpenCvCamera.ViewportRenderer.NATIVE_VIEW); + + if(!(camera instanceof OpenCvWebcam)) + { + camera.setViewportRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW); + } + + cameraState = CameraState.OPENING_CAMERA_DEVICE; + camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() + { + @Override + public void onOpened() + { + cameraState = CameraState.CAMERA_DEVICE_READY; + cameraState = CameraState.STARTING_STREAM; + + if (camera instanceof OpenCvWebcam) + { + ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); + } + else + { + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); + } + + if (camera instanceof OpenCvWebcam) + { + CameraCalibrationIdentity identity = ((OpenCvWebcam) camera).getCalibrationIdentity(); + + if (identity != null) + { + calibration = CameraCalibrationHelper.getInstance().getCalibration(identity, cameraResolution.getWidth(), cameraResolution.getHeight()); + } + } + + camera.setPipeline(new ProcessingPipeline()); + cameraState = CameraState.STREAMING; + } + + @Override + public void onError(int errorCode) + { + cameraState = CameraState.ERROR; + RobotLog.ee("VisionPortalImpl", "Camera opening failed."); + } + }); + } + + protected void createCamera(CameraName cameraName, int cameraMonitorViewId) + { + if (cameraName == null) // was the user a silly silly + { + throw new IllegalArgumentException("parameters.camera == null"); + } + else if (cameraName.isWebcam()) // Webcams + { + if (cameraMonitorViewId != 0) + { + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); + } + else + { + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName); + } + } + else if (cameraName.isCameraDirection()) // Internal cameras + { + if (cameraMonitorViewId != 0) + { + camera = OpenCvCameraFactory.getInstance().createInternalCamera( + ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT, cameraMonitorViewId); + } + else + { + camera = OpenCvCameraFactory.getInstance().createInternalCamera( + ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT); + } + } + else if (cameraName.isSwitchable()) + { + SwitchableCameraName switchableCameraName = (SwitchableCameraName) cameraName; + if (switchableCameraName.allMembersAreWebcams()) { + CameraName[] members = switchableCameraName.getMembers(); + WebcamName[] webcamNames = new WebcamName[members.length]; + for (int i = 0; i < members.length; i++) + { + webcamNames[i] = (WebcamName) members[i]; + } + + if (cameraMonitorViewId != 0) + { + camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(cameraMonitorViewId, webcamNames); + } + else + { + camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(webcamNames); + } + } + else + { + throw new IllegalArgumentException("All members of a switchable camera name must be webcam names"); + } + } + else // ¯\_(ツ)_/¯ + { + throw new IllegalArgumentException("Unknown camera name"); + } + } + + @Override + public void setProcessorEnabled(VisionProcessor processor, boolean enabled) + { + int numProcessorsEnabled = 0; + boolean ok = false; + + for (int i = 0; i < processors.length; i++) + { + if (processor == processors[i]) + { + processorsEnabled[i] = enabled; + ok = true; + } + + if (processorsEnabled[i]) + { + numProcessorsEnabled++; + } + } + + if (ok) + { + if (autoPauseCameraMonitor) + { + if (numProcessorsEnabled == 0) + { + camera.pauseViewport(); + } + else + { + camera.resumeViewport(); + } + } + } + else + { + throw new IllegalArgumentException("Processor not attached to this helper!"); + } + } + + @Override + public boolean getProcessorEnabled(VisionProcessor processor) + { + for (int i = 0; i < processors.length; i++) + { + if (processor == processors[i]) + { + return processorsEnabled[i]; + } + } + + throw new IllegalArgumentException("Processor not attached to this helper!"); + } + + @Override + public CameraState getCameraState() + { + return cameraState; + } + + @Override + public void setActiveCamera(WebcamName webcamName) + { + if (camera instanceof OpenCvSwitchableWebcam) + { + ((OpenCvSwitchableWebcam) camera).setActiveCamera(webcamName); + } + else + { + throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); + } + } + + @Override + public WebcamName getActiveCamera() + { + if (camera instanceof OpenCvSwitchableWebcam) + { + return ((OpenCvSwitchableWebcam) camera).getActiveCamera(); + } + else + { + throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); + } + } + + @Override + public T getCameraControl(Class controlType) + { + if (cameraState == CameraState.STREAMING) + { + if (camera instanceof OpenCvWebcam) + { + return ((OpenCvWebcam) camera).getControl(controlType); + } + else + { + throw new UnsupportedOperationException("Getting controls is only supported for webcams"); + } + } + else + { + throw new IllegalStateException("You cannot use camera controls until the camera is streaming"); + } + } + + class ProcessingPipeline extends TimestampedOpenCvPipeline + { + @Override + public void init(Mat firstFrame) + { + for (VisionProcessor processor : processors) + { + processor.init(firstFrame.width(), firstFrame.height(), calibration); + } + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) + { + synchronized (captureFrameMtx) + { + if (captureNextFrame != null) + { + saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); + } + + captureNextFrame = null; + } + + Object[] processorDrawCtxes = new Object[processors.length]; // cannot re-use frome to frame + + for (int i = 0; i < processors.length; i++) + { + if (processorsEnabled[i]) + { + processorDrawCtxes[i] = processors[i].processFrame(input, captureTimeNanos); + } + } + + requestViewportDrawHook(processorDrawCtxes); + + return input; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) + { + Object[] ctx = (Object[]) userContext; + + for (int i = 0; i < processors.length; i++) + { + if (processorsEnabled[i]) + { + processors[i].onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, ctx[i]); + } + } + } + } + + @Override + public void saveNextFrameRaw(String filepath) + { + synchronized (captureFrameMtx) + { + captureNextFrame = filepath; + } + } + + @Override + public void stopStreaming() + { + synchronized (userStateMtx) + { + if (cameraState == CameraState.STREAMING || cameraState == CameraState.STARTING_STREAM) + { + cameraState = CameraState.STOPPING_STREAM; + new Thread(() -> + { + synchronized (userStateMtx) + { + camera.stopStreaming(); + cameraState = CameraState.CAMERA_DEVICE_READY; + } + }).start(); + } + else if (cameraState == CameraState.STOPPING_STREAM + || cameraState == CameraState.CAMERA_DEVICE_READY + || cameraState == CameraState.CLOSING_CAMERA_DEVICE) + { + // be idempotent + } + else + { + throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); + } + } + } + + @Override + public void resumeStreaming() + { + synchronized (userStateMtx) + { + if (cameraState == CameraState.CAMERA_DEVICE_READY || cameraState == CameraState.STOPPING_STREAM) + { + cameraState = CameraState.STARTING_STREAM; + new Thread(() -> + { + synchronized (userStateMtx) + { + if (camera instanceof OpenCvWebcam) + { + ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); + } + else + { + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); + } + cameraState = CameraState.STREAMING; + } + }).start(); + } + else if (cameraState == CameraState.STREAMING + || cameraState == CameraState.STARTING_STREAM + || cameraState == CameraState.OPENING_CAMERA_DEVICE) // we start streaming automatically after we open + { + // be idempotent + } + else + { + throw new RuntimeException("Illegal CameraState when calling stopStreaming()"); + } + } + } + + @Override + public void stopLiveView() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + camera.pauseViewport(); + } + } + + @Override + public void resumeLiveView() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + camera.resumeViewport(); + } + } + + @Override + public float getFps() + { + OpenCvCamera cameraSafe = camera; + + if (cameraSafe != null) + { + return cameraSafe.getFps(); + } + else + { + return 0; + } + } + + @Override + public void close() + { + synchronized (userStateMtx) + { + cameraState = CameraState.CLOSING_CAMERA_DEVICE; + + if (camera != null) + { + camera.closeCameraDeviceAsync(() -> cameraState = CameraState.CAMERA_DEVICE_CLOSED); + } + + camera = null; + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java index a76471b9..a7a0737f 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java @@ -193,8 +193,8 @@ public Builder setNumThreads(int threads) /** * Create a {@link VisionProcessor} object which may be attached to - * a {@link org.firstinspires.ftc.vision.VisionPortal} using - * {@link org.firstinspires.ftc.vision.VisionPortal.Builder#addProcessor(VisionProcessor)} + * a {link org.firstinspires.ftc.vision.VisionPortal} using + * {link org.firstinspires.ftc.vision.VisionPortal.Builder#addProcessor(VisionProcessor)} * @return a {@link VisionProcessor} object */ public AprilTagProcessor build() diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java index 92569ce6..7814fd46 100644 --- a/Vision/src/main/java/org/opencv/android/Utils.java +++ b/Vision/src/main/java/org/opencv/android/Utils.java @@ -1,2 +1,107 @@ -package org.opencv.android;public class Utils { +package org.opencv.android; + +import android.graphics.Bitmap; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.imgproc.Imgproc; + +public class Utils { + + /** + * Converts Android Bitmap to OpenCV Mat. + *

+ * This function converts an Android Bitmap image to the OpenCV Mat. + *
'ARGB_8888' and 'RGB_565' input Bitmap formats are supported. + *
The output Mat is always created of the same size as the input Bitmap and of the 'CV_8UC4' type, + * it keeps the image in RGBA format. + *
This function throws an exception if the conversion fails. + * @param bmp is a valid input Bitmap object of the type 'ARGB_8888' or 'RGB_565'. + * @param mat is a valid output Mat object, it will be reallocated if needed, so it may be empty. + * @param unPremultiplyAlpha is a flag, that determines, whether the bitmap needs to be converted from alpha premultiplied format (like Android keeps 'ARGB_8888' ones) to regular one; this flag is ignored for 'RGB_565' bitmaps. + */ + public static void bitmapToMat(Bitmap bmp, Mat mat, boolean unPremultiplyAlpha) { + if (bmp == null) + throw new IllegalArgumentException("bmp == null"); + if (mat == null) + throw new IllegalArgumentException("mat == null"); + nBitmapToMat2(bmp, mat, unPremultiplyAlpha); + } + + /** + * Short form of the bitmapToMat(bmp, mat, unPremultiplyAlpha=false). + * @param bmp is a valid input Bitmap object of the type 'ARGB_8888' or 'RGB_565'. + * @param mat is a valid output Mat object, it will be reallocated if needed, so Mat may be empty. + */ + public static void bitmapToMat(Bitmap bmp, Mat mat) { + bitmapToMat(bmp, mat, false); + } + + /** + * Converts OpenCV Mat to Android Bitmap. + *

+ *
This function converts an image in the OpenCV Mat representation to the Android Bitmap. + *
The input Mat object has to be of the types 'CV_8UC1' (gray-scale), 'CV_8UC3' (RGB) or 'CV_8UC4' (RGBA). + *
The output Bitmap object has to be of the same size as the input Mat and of the types 'ARGB_8888' or 'RGB_565'. + *
This function throws an exception if the conversion fails. + * + * @param mat is a valid input Mat object of types 'CV_8UC1', 'CV_8UC3' or 'CV_8UC4'. + * @param bmp is a valid Bitmap object of the same size as the Mat and of type 'ARGB_8888' or 'RGB_565'. + * @param premultiplyAlpha is a flag, that determines, whether the Mat needs to be converted to alpha premultiplied format (like Android keeps 'ARGB_8888' bitmaps); the flag is ignored for 'RGB_565' bitmaps. + */ + public static void matToBitmap(Mat mat, Bitmap bmp, boolean premultiplyAlpha) { + if (mat == null) + throw new IllegalArgumentException("mat == null"); + if (bmp == null) + throw new IllegalArgumentException("bmp == null"); + nMatToBitmap2(mat, bmp, premultiplyAlpha); + } + + /** + * Short form of the matToBitmap(mat, bmp, premultiplyAlpha=false) + * @param mat is a valid input Mat object of the types 'CV_8UC1', 'CV_8UC3' or 'CV_8UC4'. + * @param bmp is a valid Bitmap object of the same size as the Mat and of type 'ARGB_8888' or 'RGB_565'. + */ + public static void matToBitmap(Mat mat, Bitmap bmp) { + matToBitmap(mat, bmp, false); + } + + private static void nBitmapToMat2(Bitmap b, Mat mat, boolean unPremultiplyAlpha) { + + } + + private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { + Mat tmp; + + if(b.getConfig() == Bitmap.Config.ARGB_8888) { + tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC4); + + if(src.type() == CvType.CV_8UC1) + { + Imgproc.cvtColor(src, tmp, Imgproc.COLOR_GRAY2RGBA); + } else if(src.type() == CvType.CV_8UC3){ + Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGB2BGRA); + } else if(src.type() == CvType.CV_8UC4){ + if(premultiplyAlpha) Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2mRGBA); + else src.copyTo(tmp); + } + } else { + tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC2); + + if(src.type() == CvType.CV_8UC1) + { + Imgproc.cvtColor(src, tmp, Imgproc.COLOR_GRAY2BGR565); + } else if(src.type() == CvType.CV_8UC3){ + Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGB2BGR565); + } else if(src.type() == CvType.CV_8UC4){ + Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2BGR565); + } + } + + byte[] data = new byte[tmp.rows() * tmp.cols()]; + tmp.get(0, 0, data); + + b.theBitmap.installPixels(data); + + tmp.release(); + } } diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java index c1da5530..ca652252 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java @@ -1,2 +1,338 @@ -package org.openftc.easyopencv;public class OpenCvCamera { -} +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +public interface OpenCvCamera +{ + public static final int CAMERA_OPEN_ERROR_FAILURE_TO_OPEN_CAMERA_DEVICE = -1; + public static final int CAMERA_OPEN_ERROR_POSTMORTEM_OPMODE = -2; + + /*** + * Open the connection to the camera device. If the camera is + * already open, this will not do anything. + * + * You must call this before calling: + * {@link #startStreaming(int, int)} + * or {@link #startStreaming(int, int, OpenCvCameraRotation)} + * or {@link #stopStreaming()} + * + * See {@link #closeCameraDevice()} + */ + @Deprecated + int openCameraDevice(); + + /*** + * Performs the same thing as {@link #openCameraDevice()} except + * in a non-blocking fashion, with a callback delivered to you + * when the operation is complete. This can be particularly helpful + * if using a webcam, as opening/starting streaming on a webcam can + * be very expensive time-wise. + * + * It is reccommended to start streaming from your listener: + * + * camera = OpenCvCameraFactory.create.............. + * camera.setPipeline(new SomePipeline()); + * + * camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() + * { + * @Override + * public void onOpened() + * { + * camera.startStreaming(640, 480, OpenCvCameraRotation.UPRIGHT); + * } + * }); + * + * NOTE: the operation performed in the background thread is synchronized + * with the main lock, so any calls to camera.XYZ() will block until the + * callback has been completed. + * + * @param cameraOpenListener the listener to which a callback will be + * delivered when the camera has been opened + */ + void openCameraDeviceAsync(AsyncCameraOpenListener cameraOpenListener); + + interface AsyncCameraOpenListener + { + /** + * Called if the camera was successfully opened + */ + void onOpened(); + + /** + * Called if there was an error opening the camera + * @param errorCode reason for failure + */ + void onError(int errorCode); + } + + /*** + * Close the connection to the camera device. If the camera is + * already closed, this will not do anything. + */ + void closeCameraDevice(); + + /*** + * Performs the same this as {@link #closeCameraDevice()} except + * in a non-blocking fashion. + * + * NOTE: the operation performed in the background thread is synchronized + * with the main lock, so any calls to camera.XYZ() will block until the + * callback has been completed. + * + * @param cameraCloseListener the listener to which a callback will be + * delivered when the camera has been closed + */ + void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener); + + interface AsyncCameraCloseListener + { + void onClose(); + } + + /*** + * If a viewport container ID was passed to the constructor of + * the implementing class, this method controls whether or not + * to show some info/statistics on top of the camera feed. + * + * @param show whether to show some info on top of the camera feed + */ + void showFpsMeterOnViewport(boolean show); + + /*** + * If a viewport container ID was passed to the constructor of + * the implementing class, this method will "pause" the viewport + * rendering thread. This can reduce CPU, memory, and power load. + * For instance, this could be useful if you wish to see the live + * camera preview as you are initializing your robot, but you no + * longer require the live preview after you have finished your + * initialization process. See {@link #resumeViewport()} + */ + void pauseViewport(); + + /*** + * If a viewport container ID was passed to the constructor of + * the implementing class, and the viewport was previously paused + * by {@link #pauseViewport()}, this method will "unpause" the + * viewport rendering thread, so that you can see the live camera + * feed on the screen again. + */ + void resumeViewport(); + + /*** + * The way the viewport will render the live preview + * + * IMPORTANT NOTE: The policy you choose here has NO IMPACT on the + * frames passed to your pipeline. This ONLY affects how the frames + * you return from your pipeline are rendered to the viewport. + */ + enum ViewportRenderingPolicy + { + /* + * This policy will minimize the CPU load caused by the viewport + * rendering, at the expense of displaying a preview which is 90 + * or 180 out from what you might expect in some orientations. + * (Note: unlike when viewing a still picture which is taken sideways, + * simply rotating the phone physically does not correct the view + * because when doing so you also rotate the camera on the phone). + */ + MAXIMIZE_EFFICIENCY, + + /* + * This policy will ensure that the live view in the viewport is + * always displayed in a logical orientation, at the expense of + * additional CPU load. + */ + OPTIMIZE_VIEW + } + + /*** + * Set the viewport rendering policy for this camera + * + * @param policy see {@link ViewportRenderingPolicy} + */ + void setViewportRenderingPolicy(ViewportRenderingPolicy policy); + + /*** + * The renderer the viewport will use to render the live preview + * NOTE: this is different than {@link ViewportRenderingPolicy}. + * The rendering policy controls how the preview will look, but + * this controls how the rendering is *actually done* + */ + enum ViewportRenderer + { + /** + * Default, if not otherwise specified. Historically this was the only option + * (Well, technically there wasn't an option for this at all before, but you get the idea) + */ + SOFTWARE, + + /** + * Can provide a much smoother live preview at higher resolutions, especially if + * you're using {@link ViewportRenderingPolicy#OPTIMIZE_VIEW}. + * However, using GPU acceleration has been observed to occasionally cause crashes + * in libgles.so / libutils.so on some devices, if the activity orientation is changed + * (i.e. you rotate the device) while a streaming session is in flight. Caveat emptor. + * Deprecated in favor of NATIVE_VIEW + */ + @Deprecated + GPU_ACCELERATED, + + /** + * Renders using the native Android view (which is hardware accelerated). + */ + NATIVE_VIEW + } + + /*** + * Set the viewport renderer for this camera + * NOTE: This may ONLY be called if there is not currently a streaming session in + * flight for this camera. + * + * @param renderer see {@link ViewportRenderer} + * @throws IllegalStateException if called while a streaming session is in flight + */ + void setViewportRenderer(ViewportRenderer renderer); + + /*** + * Tell the camera to start streaming images to us! Note that you must make sure + * the resolution you specify is supported by the camera. If it is not, an exception + * will be thrown. + * + * Keep in mind that the SDK's UVC driver (what OpenCvWebcam uses under the hood) only + * supports streaming from the webcam in the uncompressed YUV image format. This means + * that the maximum resolution you can stream at and still get up to 30FPS is 480p (640x480). + * Streaming at e.g. 720p will limit you to up to 10FPS and so on and so forth. + * + * Also see the alternate {@link #startStreaming(int, int, OpenCvCameraRotation)} method. + * + * @param width the width of the resolution in which you would like the camera to stream + * @param height the height of the resolution in which you would like the camera to stream + */ + void startStreaming(int width, int height); + + /*** + * Same as {@link #startStreaming(int, int)} except for: + * + * @param rotation the rotation that the camera is being used in. This is so that + * the image from the camera sensor can be rotated such that it is always + * displayed with the image upright. For a front facing camera, rotation is + * defined assuming the user is looking at the screen. For a rear facing camera + * or a webcam, rotation is defined assuming the camera is facing away from the user. + */ + void startStreaming(int width, int height, OpenCvCameraRotation rotation); + + /*** + * Stops streaming images from the camera (and, by extension, stops invoking your vision + * pipeline), without closing ({@link #closeCameraDevice()}) the connection to the camera. + */ + void stopStreaming(); + + /*** + * Specify the image processing pipeline that you wish to be invoked upon receipt + * of each frame from the camera. Note that switching pipelines on-the-fly (while + * a streaming session is in flight) *IS* supported. + * + * @param pipeline the image processing pipeline that you wish to be invoked upon + * receipt of each frame from the camera. + */ + void setPipeline(OpenCvPipeline pipeline); + + /*** + * Get the number of frames that have been received from the camera and processed by + * your pipeline since {@link #startStreaming(int, int)} was called. + * + * @return the number of frames that have been received from the camera and processed + * by your pipeline since {@link #startStreaming(int, int)} was called. + */ + int getFrameCount(); + + /*** + * Get the current frame rate of the overall system (including your pipeline as well as + * overhead) averaged over the last 30 frames. + * + * @return the current frame rate of the overall system (including your pipeline as well + * as overhead) averaged over the last 30 frames. + */ + float getFps(); + + /*** + * Get the current execution time (in milliseconds) of your pipeline, averaged over the + * last 30 frames. + * + * @return the current execution time (in milliseconds) of your pipeline, averaged + * over the last 30 frames. + */ + int getPipelineTimeMs(); + + /*** + * Get the current system overhead time (in milliseconds) for each frame, averaged over + * the last 30 frames. + * + * @return the current system overhead time (in milliseconds) for each frame, averaged + * over the last 30 frames + */ + int getOverheadTimeMs(); + + /*** + * Get the current total processing time (in milliseconds) for each frame (including + * pipeline and overhead), averaged over the last 30 frames. + * + * @return the current total processing time (in milliseconds) for each frame (including + * pipeline and overhead), averaged over the last 30 frames. + */ + int getTotalFrameTimeMs(); + + /*** + * Get the current theoretically maximum frame rate that your pipeline (and overhead) + * could achieve. This is useful for identifying whether or not your pipeline is the + * bottleneck in the system. For instance, if {@link #getFps()} reports that the system + * is running at 10FPS, and this method reported that your theoretical maximum FPS is + * 12, then your pipeline is the bottleneck. Conversely, if {@link #getFps()} reported that + * the system was running at 25FPS, and this method reported that your theoretical maximum + * FPS is 100, then the camera would be the bottleneck. + * + * @return the current theoretically maximum frame rate that your pipeline (and overhead) + * could achieve. + */ + int getCurrentPipelineMaxFps(); + + /*** + * Start recording the output of the camera's current pipeline + * (If no pipeline is set, then the plain camera image is recorded) + * A streaming session must be in flight before this can be called. + * The recording will be automatically stopped when the streaming + * session is stopped (whether that be manually or automatically at + * the end of the OpMode), but can also be stopped independently by + * calling {@link #stopRecordingPipeline()} + * + * @param parameters the parameters which define how the recording should done + * @throws IllegalStateException if called before streaming is started + * @throws IllegalStateException if recording was started previously + */ + void startRecordingPipeline(PipelineRecordingParameters parameters); + + /*** + * Stops recording the output of the camera's current pipeline, + * if a recording session is currently active. + */ + void stopRecordingPipeline(); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java index 4865cf5f..b3df5e11 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java @@ -1,2 +1,294 @@ -package org.openftc.easyopencv;public class OpenCvCameraBase { +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.util.MovingStatistics; +import org.opencv.android.Utils; +import org.opencv.core.*; +import org.opencv.imgproc.Imgproc; + +public abstract class OpenCvCameraBase implements OpenCvCamera { + + private OpenCvPipeline pipeline = null; + + private MovingStatistics msFrameIntervalRollingAverage; + private MovingStatistics msUserPipelineRollingAverage; + private MovingStatistics msTotalFrameProcessingTimeRollingAverage; + private ElapsedTime timer; + + private OpenCvViewport viewport; + private OpenCvCameraRotation rotation; + + private final Object pipelineChangeLock = new Object(); + + private Mat rotatedMat = new Mat(); + private Mat matToUseIfPipelineReturnedCropped; + private Mat croppedColorCvtedMat = new Mat(); + + private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE; + ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; + boolean fpsMeterDesired = true; + + private Scalar brown = new Scalar(82, 61, 46, 255); + + private int frameCount = 0; + private float avgFps; + private int avgPipelineTime; + private int avgOverheadTime; + private int avgTotalFrameTime; + private long currentFrameStartTime; + + @Override + public void showFpsMeterOnViewport(boolean show) { + viewport.setFpsMeterEnabled(show); + } + + @Override + public void pauseViewport() { + viewport.pause(); + } + + @Override + public void resumeViewport() { + viewport.resume(); + } + + @Override + public void setViewportRenderingPolicy(ViewportRenderingPolicy policy) { + viewport.setRenderingPolicy(policy); + } + + @Override + public void setViewportRenderer(ViewportRenderer renderer) { + this.desiredViewportRenderer = renderer; + } + + @Override + public void setPipeline(OpenCvPipeline pipeline) { + this.pipeline = pipeline; + } + + @Override + public int getFrameCount() { + return 0; + } + + @Override + public float getFps() { + return 0; + } + + @Override + public int getPipelineTimeMs() { + return 0; + } + + @Override + public int getOverheadTimeMs() { + return 0; + } + + @Override + public int getTotalFrameTimeMs() { + return 0; + } + + @Override + public int getCurrentPipelineMaxFps() { + return 0; + } + + @Override + public void startRecordingPipeline(PipelineRecordingParameters parameters) { + + } + + @Override + public void stopRecordingPipeline() { + + } + + protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { + msFrameIntervalRollingAverage.add(timer.milliseconds()); + timer.reset(); + double secondsPerFrame = msFrameIntervalRollingAverage.getMean() / 1000d; + avgFps = (float) (1d / secondsPerFrame); + Mat userProcessedFrame = null; + + int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation); + + if (rotateCode != -1) { + /* + * Rotate onto another Mat rather than doing so in-place. + * + * This does two things: + * 1) It seems that rotating by 90 or 270 in-place + * causes the backing buffer to be re-allocated + * since the width/height becomes swapped. This + * causes a problem for user code which makes a + * submat from the input Mat, because after the + * parent Mat is re-allocated the submat is no + * longer tied to it. Thus, by rotating onto + * another Mat (which is never re-allocated) we + * remove that issue. + * + * 2) Since the backing buffer does need need to be + * re-allocated for each frame, we reduce overhead + * time by about 1ms. + */ + Core.rotate(frame, rotatedMat, rotateCode); + frame = rotatedMat; + } + + final OpenCvPipeline pipelineSafe; + + // Grab a safe reference to what the pipeline currently is, + // since the user is allowed to change it at any time + synchronized (pipelineChangeLock) { + pipelineSafe = pipeline; + } + + if (pipelineSafe != null) { + if (pipelineSafe instanceof TimestampedOpenCvPipeline) { + ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp); + } + + long pipelineStart = System.currentTimeMillis(); + userProcessedFrame = pipelineSafe.processFrameInternal(frame); + msUserPipelineRollingAverage.add(System.currentTimeMillis() - pipelineStart); + } + + // Will point to whatever mat we end up deciding to send to the screen + final Mat matForDisplay; + + if (pipelineSafe == null) { + matForDisplay = frame; + } else if (userProcessedFrame == null) { + throw new OpenCvCameraException("User pipeline returned null"); + } else if (userProcessedFrame.empty()) { + throw new OpenCvCameraException("User pipeline returned empty mat"); + } else if (userProcessedFrame.cols() != frame.cols() || userProcessedFrame.rows() != frame.rows()) { + /* + * The user didn't return the same size image from their pipeline as we gave them, + * ugh. This makes our lives interesting because we can't just send an arbitrary + * frame size to the viewport. It re-uses framebuffers that are of a fixed resolution. + * So, we copy the user's Mat onto a Mat of the correct size, and then send that other + * Mat to the viewport. + */ + + if (userProcessedFrame.cols() > frame.cols() || userProcessedFrame.rows() > frame.rows()) { + /* + * What on earth was this user thinking?! They returned a Mat that's BIGGER in + * a dimension than the one we gave them! + */ + + throw new OpenCvCameraException("User pipeline returned frame of unexpected size"); + } + + //We re-use this buffer, only create if needed + if (matToUseIfPipelineReturnedCropped == null) { + matToUseIfPipelineReturnedCropped = frame.clone(); + } + + //Set to brown to indicate to the user the areas which they cropped off + matToUseIfPipelineReturnedCropped.setTo(brown); + + int usrFrmTyp = userProcessedFrame.type(); + + if (usrFrmTyp == CvType.CV_8UC1) { + /* + * Handle 8UC1 returns (masks and single channels of images); + * + * We have to color convert onto a different mat (rather than + * doing so in place) to avoid breaking any of the user's submats + */ + Imgproc.cvtColor(userProcessedFrame, croppedColorCvtedMat, Imgproc.COLOR_GRAY2RGBA); + userProcessedFrame = croppedColorCvtedMat; //Doesn't affect user's handle, only ours + } else if (usrFrmTyp != CvType.CV_8UC4 && usrFrmTyp != CvType.CV_8UC3) { + /* + * Oof, we don't know how to handle the type they gave us + */ + throw new OpenCvCameraException("User pipeline returned a frame of an illegal type. Valid types are CV_8UC1, CV_8UC3, and CV_8UC4"); + } + + //Copy the user's frame onto a Mat of the correct size + userProcessedFrame.copyTo(matToUseIfPipelineReturnedCropped.submat( + new Rect(0, 0, userProcessedFrame.cols(), userProcessedFrame.rows()))); + + //Send that correct size Mat to the viewport + matForDisplay = matToUseIfPipelineReturnedCropped; + } else { + /* + * Yay, smart user! They gave us the frame size we were expecting! + * Go ahead and send it right on over to the viewport. + */ + matForDisplay = userProcessedFrame; + } + + if (viewport != null) { + viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null)); + } + + avgPipelineTime = (int) Math.round(msUserPipelineRollingAverage.getMean()); + avgTotalFrameTime = (int) Math.round(msTotalFrameProcessingTimeRollingAverage.getMean()); + avgOverheadTime = avgTotalFrameTime - avgPipelineTime; + + if (viewport != null) { + viewport.notifyStatistics(avgFps, avgPipelineTime, avgOverheadTime); + } + + frameCount++; + + msTotalFrameProcessingTimeRollingAverage.add(System.currentTimeMillis() - currentFrameStartTime); + } + + + protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCameraRotation streamRotation) { + if (!cameraOrientationIsTiedToDeviceOrientation()) { + return OpenCvViewport.OptimizedRotation.NONE; + } + + if (streamRotation == OpenCvCameraRotation.SIDEWAYS_LEFT || streamRotation == OpenCvCameraRotation.SENSOR_NATIVE) { + return OpenCvViewport.OptimizedRotation.ROT_90_COUNTERCLOCWISE; + } else if (streamRotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) { + return OpenCvViewport.OptimizedRotation.ROT_90_CLOCKWISE; + } else if (streamRotation == OpenCvCameraRotation.UPSIDE_DOWN) { + return OpenCvViewport.OptimizedRotation.ROT_180; + } else { + return OpenCvViewport.OptimizedRotation.NONE; + } + } + + protected abstract OpenCvViewport setupViewport(); + + protected abstract OpenCvCameraRotation getDefaultRotation(); + + protected abstract int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation); + + protected abstract boolean cameraOrientationIsTiedToDeviceOrientation(); + + protected abstract boolean isStreaming(); + } diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java new file mode 100644 index 00000000..bd85888c --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +public class OpenCvCameraException extends RuntimeException +{ + public OpenCvCameraException(String msg) + { + super(msg); + } +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java index af0365e4..d8db2572 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java @@ -1,2 +1,31 @@ -package org.openftc.easyopencv;public class OpenCvCameraRotation { -} +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +public enum OpenCvCameraRotation +{ + UPRIGHT, + UPSIDE_DOWN, + SIDEWAYS_LEFT, + SIDEWAYS_RIGHT, + SENSOR_NATIVE, +} \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java new file mode 100644 index 00000000..346bf806 --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java @@ -0,0 +1,74 @@ +package org.openftc.easyopencv; + +import android.graphics.Canvas; +import org.opencv.core.Mat; + +public abstract class OpenCvPipeline { + + private Object userContext; + private boolean isFirstFrame; + private long firstFrameTimestamp; + + public abstract Mat processFrame(Mat input); + + public void onViewportTapped() { } + + public void init(Mat mat) { } + + + public Object getUserContextForDrawHook() + { + return userContext; + } + + /** + * Call this during processFrame() to request a hook during the viewport's + * drawing operation of the current frame (which will happen asynchronously + * at some future time) using the Canvas API. + * + * If you call this more than once during processFrame(), the last call takes + * precedence. You will only get a single draw hook for a given frame. + * + * @param userContext anything you want :monkey: will be passed back to you + * in {@link #onDrawFrame(Canvas, int, int, float, float, Object)}. You can + * use this to store information about what you found in the frame, so that + * you know what to draw when it's time. (Otherwise how the heck would you + * know what to draw??). + */ + public void requestViewportDrawHook(Object userContext) + { + this.userContext = userContext; + } + + /** + * Called during the viewport's frame rendering operation at some later point after + * you called called {@link #requestViewportDrawHook(Object)} during processFrame(). + * Allows you to use the Canvas API to draw annotations on the frame, rather than + * using OpenCV calls. This allows for more eye-candy-y annotations since you've got + * a high resolution canvas to work with rather than, say, a 320x240 image. + * + * Note that this is NOT called from the same thread that calls processFrame()! + * And may actually be called from the UI thread depending on the viewport renderer. + * + * @param canvas the canvas that's being drawn on NOTE: Do NOT get dimensions from it, use below + * @param onscreenWidth the width of the canvas that corresponds to the image + * @param onscreenHeight the height of the canvas that corresponds to the image + * @param scaleBmpPxToCanvasPx multiply pixel coords by this to scale to canvas coords + * @param scaleCanvasDensity a scaling factor to adjust e.g. text size. Relative to Nexus5 DPI. + * @param userContext whatever you passed in when requesting the draw hook :monkey: + */ + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) {}; + + Mat processFrameInternal(Mat input) + { + if(isFirstFrame) + { + init(input); + firstFrameTimestamp = System.currentTimeMillis(); + isFirstFrame = false; + } + + return processFrame(input); + } + +} \ No newline at end of file diff --git a/Common/src/main/java/org/openftc/easyopencv/OpenCvTracker.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTracker.java similarity index 97% rename from Common/src/main/java/org/openftc/easyopencv/OpenCvTracker.java rename to Vision/src/main/java/org/openftc/easyopencv/OpenCvTracker.java index a1422e4f..a0042a68 100644 --- a/Common/src/main/java/org/openftc/easyopencv/OpenCvTracker.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTracker.java @@ -1,35 +1,35 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * 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 org.openftc.easyopencv; - -import org.opencv.core.Mat; - -public abstract class OpenCvTracker { - private final Mat mat = new Mat(); - - public abstract Mat processFrame(Mat input); - - protected final Mat processFrameInternal(Mat input) { - input.copyTo(mat); - return processFrame(mat); - } +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import org.opencv.core.Mat; + +public abstract class OpenCvTracker { + private final Mat mat = new Mat(); + + public abstract Mat processFrame(Mat input); + + protected final Mat processFrameInternal(Mat input) { + input.copyTo(mat); + return processFrame(mat); + } } \ No newline at end of file diff --git a/Common/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java similarity index 97% rename from Common/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java rename to Vision/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java index 1f4c4504..fb3bb12b 100644 --- a/Common/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java @@ -1,71 +1,73 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * 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 org.openftc.easyopencv; - -import org.opencv.core.Mat; - -import java.util.ArrayList; - -public class OpenCvTrackerApiPipeline extends OpenCvPipeline { - private final ArrayList trackers = new ArrayList<>(); - private int trackerDisplayIdx = 0; - - public synchronized void addTracker(OpenCvTracker tracker) { - trackers.add(tracker); - } - - public synchronized void removeTracker(OpenCvTracker tracker) { - trackers.remove(tracker); - - if (trackerDisplayIdx >= trackers.size()) { - trackerDisplayIdx--; - - if (trackerDisplayIdx < 0) { - trackerDisplayIdx = 0; - } - } - } - - @Override - public synchronized Mat processFrame(Mat input) { - if (trackers.size() == 0) { - return input; - } - - ArrayList returnMats = new ArrayList<>(trackers.size()); - - for (OpenCvTracker tracker : trackers) { - returnMats.add(tracker.processFrameInternal(input)); - } - - return returnMats.get(trackerDisplayIdx); - } - - @Override - public synchronized void onViewportTapped() { - trackerDisplayIdx++; - - if (trackerDisplayIdx >= trackers.size()) { - trackerDisplayIdx = 0; - } - } +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import org.opencv.core.Mat; + +import java.util.ArrayList; + +@Disabled +public class OpenCvTrackerApiPipeline extends OpenCvPipeline { + private final ArrayList trackers = new ArrayList<>(); + private int trackerDisplayIdx = 0; + + public synchronized void addTracker(OpenCvTracker tracker) { + trackers.add(tracker); + } + + public synchronized void removeTracker(OpenCvTracker tracker) { + trackers.remove(tracker); + + if (trackerDisplayIdx >= trackers.size()) { + trackerDisplayIdx--; + + if (trackerDisplayIdx < 0) { + trackerDisplayIdx = 0; + } + } + } + + @Override + public synchronized Mat processFrame(Mat input) { + if (trackers.size() == 0) { + return input; + } + + ArrayList returnMats = new ArrayList<>(trackers.size()); + + for (OpenCvTracker tracker : trackers) { + returnMats.add(tracker.processFrameInternal(input)); + } + + return returnMats.get(trackerDisplayIdx); + } + + @Override + public synchronized void onViewportTapped() { + trackerDisplayIdx++; + + if (trackerDisplayIdx >= trackers.size()) { + trackerDisplayIdx = 0; + } + } } \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java index f8ac842d..e1be7c3b 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -1,2 +1,331 @@ -package org.openftc.easyopencv;public class OpenCvViewRenderer { -} +/* + * Copyright (c) 2023 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; + +import org.opencv.android.Utils; +import org.opencv.core.Mat; + +public class OpenCvViewRenderer +{ + private final int statBoxW; + private final int statBoxH; + private final int statBoxTextLineSpacing; + private final int statBoxTextFirstLineYFromBottomOffset; + private final int statBoxLTxtMargin; + private static final float referenceDPI = 443; // Nexus 5 + private final float metricsScale; + private static final int OVERLAY_COLOR = Color.rgb(102, 20, 68); + private static final int PAUSED_COLOR = Color.rgb(255, 166, 0); + private static final int RC_ACTIVITY_BG_COLOR = Color.rgb(239,239,239); + private Paint fpsMeterNormalBgPaint; + private Paint fpsMeterRecordingPaint; + private Paint fpsMeterTextPaint; + private final float fpsMeterTextSize; + private Paint paintBlackBackground; + private double aspectRatio; + + private boolean fpsMeterEnabled = true; + private float fps = 0; + private int pipelineMs = 0; + private int overheadMs = 0; + + private int width; + private int height; + private final boolean offscreen; + + private volatile boolean isRecording; + + private volatile OpenCvViewport.OptimizedRotation optimizedViewRotation; + + private volatile OpenCvCamera.ViewportRenderingPolicy renderingPolicy = OpenCvCamera.ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; + + private Bitmap bitmapFromMat; + + public OpenCvViewRenderer(boolean renderingOffsceen) + { + offscreen = renderingOffsceen; + + metricsScale = 1.0f; + + fpsMeterTextSize = 30 * metricsScale; + statBoxW = (int) (450 * metricsScale); + statBoxH = (int) (120 * metricsScale); + statBoxTextLineSpacing = (int) (35 * metricsScale); + statBoxLTxtMargin = (int) (5 * metricsScale); + statBoxTextFirstLineYFromBottomOffset = (int) (80*metricsScale); + + fpsMeterNormalBgPaint = new Paint(); + fpsMeterNormalBgPaint.setColor(OVERLAY_COLOR); + fpsMeterNormalBgPaint.setStyle(Paint.Style.FILL); + + fpsMeterRecordingPaint = new Paint(); + fpsMeterRecordingPaint.setColor(Color.RED); + fpsMeterRecordingPaint.setStyle(Paint.Style.FILL); + + fpsMeterTextPaint = new Paint(); + fpsMeterTextPaint.setColor(Color.WHITE); + fpsMeterTextPaint.setTextSize(fpsMeterTextSize); + fpsMeterTextPaint.setAntiAlias(true); + + paintBlackBackground = new Paint(); + paintBlackBackground.setColor(Color.BLACK); + paintBlackBackground.setStyle(Paint.Style.FILL); + } + + private void unifiedDraw(Canvas canvas, int onscreenWidth, int onscreenHeight, OpenCvViewport.RenderHook userHook, Object userCtx) + { + int x_offset_statbox = 0; + int y_offset_statbox = 0; + + int topLeftX; + int topLeftY; + int scaledWidth; + int scaledHeight; + + double canvasAspect = (float) onscreenWidth/onscreenHeight; + + if(aspectRatio > canvasAspect) /* Image is WIDER than canvas */ + { + // Width: we use the max we have, since horizontal bounds are hit before vertical bounds + scaledWidth = onscreenWidth; + + // Height: calculate a scaled height assuming width is maxed for the canvas + scaledHeight = (int) Math.round(onscreenWidth / aspectRatio); + + // We want to center the image in the viewport + topLeftY = Math.abs(onscreenHeight-scaledHeight)/2; + topLeftX = 0; + y_offset_statbox = topLeftY; + } + else /* Image is TALLER than canvas */ + { + // Height: we use the max we have, since vertical bounds are hit before horizontal bounds + scaledHeight = onscreenHeight; + + // Width: calculate a scaled width assuming height is maxed for the canvas + scaledWidth = (int) Math.round(onscreenHeight * aspectRatio); + + // We want to center the image in the viewport + topLeftY = 0; + topLeftX = Math.abs(onscreenWidth - scaledWidth) / 2; + x_offset_statbox = topLeftX; + } + + //Draw the bitmap, scaling it to the maximum size that will fit in the viewport + Rect bmpRect = createRect( + topLeftX, + topLeftY, + scaledWidth, + scaledHeight); + + // Draw black behind the bitmap to avoid alpha issues if usercode tries to draw + // annotations and doesn't specify alpha 255. This wasn't an issue when we just + // painted black behind the entire view, but now that we paint the RC background + // color, it is an issue... + canvas.drawRect(bmpRect, paintBlackBackground); + + canvas.drawBitmap( + bitmapFromMat, + null, + bmpRect, + null + ); + + // We need to save the canvas translation/rotation and such before we hand off to the user, + // because if they don't put it back how they found it and then we go to draw the FPS meter, + // it's... well... not going to draw properly lol + int canvasSaveBeforeUserDraw = canvas.save(); + + // Allow the user to do some drawing if they want + if (userHook != null) + { + // Can either use width or height I guess ¯\_(ツ)_/¯ + float scaleBitmapPxToCanvasPx = (float) scaledWidth / bitmapFromMat.getWidth(); + + // To make the user's life easy, we teleport the origin to the top + // left corner of the bitmap we painted + canvas.translate(topLeftX, topLeftY); + userHook.onDrawFrame(canvas, scaledWidth, scaledHeight, scaleBitmapPxToCanvasPx, metricsScale, userCtx); + } + + // Make sure the canvas translation/rotation is what we expect (see comment when we save state) + canvas.restoreToCount(canvasSaveBeforeUserDraw); + + if (fpsMeterEnabled) + { + Rect statsRect = createRect( + x_offset_statbox, + onscreenHeight-y_offset_statbox-statBoxH, + statBoxW, + statBoxH + ); + + drawStats(canvas, statsRect); + } + } + + private void drawStats(Canvas canvas, Rect rect) + { + // Draw the purple rectangle + if(isRecording) + { + canvas.drawRect(rect, fpsMeterRecordingPaint); + } + else + { + canvas.drawRect(rect, fpsMeterNormalBgPaint); + } + + // Some formatting stuff + int statBoxLTxtStart = rect.left+statBoxLTxtMargin; + int textLine1Y = rect.bottom - statBoxTextFirstLineYFromBottomOffset; + int textLine2Y = textLine1Y + statBoxTextLineSpacing; + int textLine3Y = textLine2Y + statBoxTextLineSpacing; + + // Draw the 3 text lines + canvas.drawText(String.format("deltacv EOCV-Sim"), statBoxLTxtStart, textLine1Y, fpsMeterTextPaint); + canvas.drawText(String.format("FPS@%dx%d: %.2f", width, height, fps), statBoxLTxtStart, textLine2Y, fpsMeterTextPaint); + canvas.drawText(String.format("Pipeline: %dms - Overhead: %dms", pipelineMs, overheadMs), statBoxLTxtStart, textLine3Y, fpsMeterTextPaint); + } + + Rect createRect(int tlx, int tly, int w, int h) + { + return new Rect(tlx, tly, tlx+w, tly+h); + } + + public void setFpsMeterEnabled(boolean fpsMeterEnabled) + { + this.fpsMeterEnabled = fpsMeterEnabled; + } + + public void notifyStatistics(float fps, int pipelineMs, int overheadMs) + { + this.fps = fps; + this.pipelineMs = pipelineMs; + this.overheadMs = overheadMs; + } + + public void setRecording(boolean recording) + { + isRecording = recording; + } + + public void setOptimizedViewRotation(OpenCvViewport.OptimizedRotation optimizedViewRotation) + { + this.optimizedViewRotation = optimizedViewRotation; + } + + public void render(Mat mat, Canvas canvas, OpenCvViewport.RenderHook userHook, Object userCtx) + { + if (bitmapFromMat == null || bitmapFromMat.getWidth() != mat.width() || bitmapFromMat.getHeight() != mat.height()) + { + if (bitmapFromMat != null) + { + bitmapFromMat.recycle(); + } + + bitmapFromMat = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888); + + System.out.println("create bitmapFromMat"); + } + + //Convert that Mat to a bitmap we can render + // Utils.matToBitmap(mat, bitmapFromMat, false); + + width = bitmapFromMat.getWidth(); + height = bitmapFromMat.getHeight(); + aspectRatio = (float) width / height; + + if (!offscreen) + { + //Draw the background each time to prevent double buffering problems + canvas.drawColor(RC_ACTIVITY_BG_COLOR); + } + + // Cache current state, can change behind our backs + OpenCvViewport.OptimizedRotation optimizedRotationSafe = optimizedViewRotation; + + if(renderingPolicy == OpenCvCamera.ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY || optimizedRotationSafe == OpenCvViewport.OptimizedRotation.NONE) + { + unifiedDraw(canvas, canvas.getWidth(), canvas.getHeight(), userHook, userCtx); + } + else if(renderingPolicy == OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW) + { + if(optimizedRotationSafe == OpenCvViewport.OptimizedRotation.ROT_180) + { + // 180 is easy, just rotate canvas 180 about center and draw as usual + canvas.rotate(optimizedRotationSafe.val, canvas.getWidth()/2, canvas.getHeight()/2); + unifiedDraw(canvas, canvas.getWidth(), canvas.getHeight(), userHook, userCtx); + } + else // 90 either way + { + // Rotate the canvas +-90 about the center + canvas.rotate(optimizedRotationSafe.val, canvas.getWidth()/2, canvas.getHeight()/2); + + // Translate the canvas such that 0,0 is in the top left corner (for this perspective) ONSCREEN. + int origin_x = (canvas.getWidth()-canvas.getHeight())/2; + int origin_y = -origin_x; + canvas.translate(origin_x, origin_y); + + // Now draw as normal, but, the onscreen width and height are swapped + unifiedDraw(canvas, canvas.getHeight(), canvas.getWidth(), userHook, userCtx); + } + } + } + + public void setRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy policy) + { + renderingPolicy = policy; + } + + public void renderPaused(Canvas canvas) + { + canvas.drawColor(PAUSED_COLOR); + + Rect rect = createRect( + 0, + canvas.getHeight()-statBoxH, + statBoxW, + statBoxH + ); + + // Draw the purple rectangle + canvas.drawRect(rect, fpsMeterNormalBgPaint); + + // Some formatting stuff + int statBoxLTxtStart = rect.left+statBoxLTxtMargin; + int textLine1Y = rect.bottom - statBoxTextFirstLineYFromBottomOffset; + int textLine2Y = textLine1Y + statBoxTextLineSpacing; + int textLine3Y = textLine2Y + statBoxTextLineSpacing; + + // Draw the 3 text lines + canvas.drawText(String.format("deltacv EOCV-Sim"), statBoxLTxtStart, textLine1Y, fpsMeterTextPaint); + canvas.drawText("VIEWPORT PAUSED", statBoxLTxtStart, textLine2Y, fpsMeterTextPaint); + //canvas.drawText("Hi", statBoxLTxtStart, textLine3Y, fpsMeterTextPaint); + } +} \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java index ded7f587..24465e97 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java @@ -1,2 +1,74 @@ -package org.openftc.easyopencv;public class OpenCvViewport { -} +/* + * Copyright (c) 2023 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import android.graphics.Canvas; + +import org.opencv.core.Mat; + +public interface OpenCvViewport +{ + enum OptimizedRotation + { + NONE(0), + ROT_90_COUNTERCLOCWISE(90), + ROT_90_CLOCKWISE(-90), + ROT_180(180); + + int val; + + OptimizedRotation(int val) + { + this.val = val; + } + } + + interface RenderHook + { + void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float canvasDensityScale, Object userContext); + } + + void setFpsMeterEnabled(boolean enabled); + void pause(); + void resume(); + void activate(); + void deactivate(); + void setSize(int width, int height); + void setOptimizedViewRotation(OptimizedRotation rotation); + void notifyStatistics(float fps, int pipelineMs, int overheadMs); + void setRecording(boolean recording); + void post(Mat frame, Object userContext); + void setRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy policy); + void setRenderHook(RenderHook renderHook); + + class FrameContext + { + public OpenCvPipeline generatingPipeline; + public Object userContext; + + public FrameContext(OpenCvPipeline generatingPipeline, Object userContext) + { + this.generatingPipeline = generatingPipeline; + this.userContext = userContext; + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java b/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java index f7c6bbf9..646f9449 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java +++ b/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java @@ -1,2 +1,118 @@ -package org.openftc.easyopencv;public class PipelineRecordingParametes { -} +/* + * Copyright (c) 2020 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public class PipelineRecordingParameters +{ + public final String path; + public final Encoder encoder; + public final OutputFormat outputFormat; + public final int bitrate; + public final int frameRate; + + public enum Encoder + { + H264(), + H263(), + VP8(), + MPEG_4_SP(); + } + + public enum OutputFormat + { + MPEG_4(), + THREE_GPP(), + WEBM(); + } + + public enum BitrateUnits + { + bps(1), + Kbps(1000), + Mbps(1000000); + + final int scalar; + + BitrateUnits(int scalar) + { + this.scalar = scalar; + } + } + + public PipelineRecordingParameters(OutputFormat outputFormat, Encoder encoder, int frameRate, int bitrate, String path) + { + this.outputFormat = outputFormat; + this.encoder = encoder; + this.frameRate = frameRate; + this.bitrate = bitrate; + this.path = path; + } + + public static class Builder + { + private String path = "/sdcard/EasyOpenCV/pipeline_recording_"+new SimpleDateFormat("dd-MM-yyyy_HH:mm:ss", Locale.getDefault()).format(new Date())+".mp4"; + private Encoder encoder = Encoder.H264; + private OutputFormat outputFormat = OutputFormat.MPEG_4; + private int bitrate = 4000000; + private int frameRate = 30; + + public Builder setPath(String path) + { + this.path = path; + return this; + } + + public Builder setEncoder(Encoder encoder) + { + this.encoder = encoder; + return this; + } + + public Builder setOutputFormat(OutputFormat outputFormat) + { + this.outputFormat = outputFormat; + return this; + } + + public Builder setBitrate(int bitrate, BitrateUnits units) + { + this.bitrate = bitrate*units.scalar; + return this; + } + + public Builder setFrameRate(int frameRate) + { + this.frameRate = frameRate; + return this; + } + + public PipelineRecordingParameters build() + { + return new PipelineRecordingParameters(outputFormat, encoder, frameRate, bitrate, path); + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java b/Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java deleted file mode 100644 index b6c1832a..00000000 --- a/Vision/src/main/java/org/openftc/easyopencv/QueueOpenCvCamera.java +++ /dev/null @@ -1,2 +0,0 @@ -package org.openftc.easyopencv;public class QueueOpenCvPipeline { -} diff --git a/Common/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java similarity index 97% rename from Common/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java rename to Vision/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java index e2453bae..7281d245 100644 --- a/Common/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java +++ b/Vision/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java @@ -1,42 +1,42 @@ -/* - * Copyright (c) 2020 OpenFTC Team - * - * 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 org.openftc.easyopencv; - -import org.opencv.core.Mat; - -public abstract class TimestampedOpenCvPipeline extends OpenCvPipeline -{ - private long timestamp; - - @Override - public final Mat processFrame(Mat input) - { - return processFrame(input, timestamp); - } - - public abstract Mat processFrame(Mat input, long captureTimeNanos); - - protected void setTimestamp(long timestamp) - { - this.timestamp = timestamp; - } +/* + * Copyright (c) 2020 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import org.opencv.core.Mat; + +public abstract class TimestampedOpenCvPipeline extends OpenCvPipeline +{ + private long timestamp; + + @Override + public final Mat processFrame(Mat input) + { + return processFrame(input, timestamp); + } + + public abstract Mat processFrame(Mat input, long captureTimeNanos); + + protected void setTimestamp(long timestamp) + { + this.timestamp = timestamp; + } } \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/Util.java b/Vision/src/main/java/org/openftc/easyopencv/Util.java new file mode 100644 index 00000000..a8cf87a7 --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/Util.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import java.util.concurrent.CountDownLatch; + +public class Util +{ + public static void joinUninterruptibly(Thread thread) + { + boolean interrupted = false; + + while (true) + { + try + { + thread.join(); + break; + } + catch (InterruptedException e) + { + e.printStackTrace(); + interrupted = true; + } + } + + if (interrupted) + { + Thread.currentThread().interrupt(); + } + } + + public static void acquireUninterruptibly(CountDownLatch latch) + { + boolean interrupted = false; + + while (true) + { + try + { + latch.await(); + break; + } + catch (InterruptedException e) + { + e.printStackTrace(); + interrupted = true; + } + } + + if (interrupted) + { + Thread.currentThread().interrupt(); + } + } +} \ No newline at end of file diff --git a/Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java deleted file mode 100644 index 3b5c45fb..00000000 --- a/Vision/src/main/java/org/openftc/easyopencv/ViewportPipeline.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.deltacv.vision.util; - -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvCamera; -import org.openftc.easyopencv.TimestampedOpenCvPipeline; - -public abstract class ViewportPipeline extends TimestampedOpenCvPipeline implements OpenCvCamera { - - protected final FrameQueue queue; - - private final Mat emptyMat; - - protected ViewportPipeline(int maxQueueItems) { - queue = new FrameQueue(maxQueueItems + 3); - emptyMat = queue.takeMat(); - } - - protected ViewportPipeline() { - this(10); - } - - @Override - public Mat processFrame(Mat input, long captureTimeNanos) { - Mat output = queue.poll(); - if(output == null) { - output = emptyMat; - } - - return output; - } - - public FrameQueue getOutputQueue() { - return queue; - } - -} \ No newline at end of file diff --git a/build.common.gradle b/build.common.gradle index 828f6c35..808d5030 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -1,14 +1,13 @@ java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_10 + targetCompatibility = JavaVersion.VERSION_1_10 } if (project.getPluginManager().hasPlugin("org.jetbrains.kotlin.jvm")) { compileKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "10" freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - useIR = true } } } diff --git a/build.gradle b/build.gradle index 369c6678..6f6bea26 100644 --- a/build.gradle +++ b/build.gradle @@ -4,13 +4,13 @@ import java.time.format.DateTimeFormatter buildscript { ext { - kotlin_version = "1.5.31" + kotlin_version = "1.8.0" kotlinx_coroutines_version = "1.5.0-native-mt" slf4j_version = "1.7.32" log4j_version = "2.17.1" opencv_version = "4.5.5-1" apriltag_plugin_version = "2.0.0-A" - skija_version = "0.109.2" + skiko_version = "0.7.75" classgraph_version = "4.8.108" opencsv_version = "5.5.2" @@ -33,7 +33,7 @@ buildscript { allprojects { group 'com.github.deltacv' - version '3.4.4' + version '3.5.0' ext { standardVersion = version @@ -48,6 +48,7 @@ allprojects { maven { url "https://jitpack.io" } maven { url 'https://maven.openimaj.org/' } maven { url 'https://maven.ecs.soton.ac.uk/content/repositories/thirdparty/' } + maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } } tasks.withType(Jar) { From f1ad4c6030095805e2daafa90d544d4b36a9e785 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 11 Aug 2023 03:37:22 -0600 Subject: [PATCH 14/46] AprilTagAnnotator fully working and drawing to the Canvas --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 4 ++ .../serivesmejia/eocvsim/gui/Visualizer.java | 59 +++++-------------- .../eocvsim/pipeline/PipelineManager.kt | 35 +++++------ .../ProcessFrameInternalAccessor.kt | 5 ++ .../teamcode/AprilTagProcessorPipeline.java | 39 ++++++++++++ .../main/java/android/graphics/Canvas.java | 2 +- .../robotcore/eventloop/opmode/OpMode.java | 3 + .../deltacv/vision/PipelineRenderHook.kt | 19 ++++++ .../deltacv/vision/SourcedOpenCvCamera.java | 13 +--- .../deltacv/vision/gui/SwingOpenCvViewport.kt | 5 +- .../apriltag/AprilTagCanvasAnnotator.java | 2 + .../apriltag/AprilTagProcessorImpl.java | 8 +-- .../openftc/easyopencv/OpenCvPipeline.java | 3 +- build.common.gradle | 2 +- 14 files changed, 117 insertions(+), 82 deletions(-) create mode 100644 EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt 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 96d2a6d7..c40b6b62 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 @@ -47,6 +47,7 @@ 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.vision.PipelineRenderHook import nu.pattern.OpenCV import org.opencv.core.Size import org.openftc.easyopencv.TimestampedPipelineHandler @@ -223,8 +224,11 @@ class EOCVSim(val params: Parameters = Parameters()) { pipelineManager.onPipelineChange { if(pipelineManager.currentPipeline !is OpMode && pipelineManager.currentPipeline != null) { + // do some hand holding as pipelines cannot do this themselves... visualizer.viewport.activate() + visualizer.viewport.setRenderHook(PipelineRenderHook) // calls OpenCvPipeline#onDrawFrame on the viewport (UI) thread } else { + // opmodes are on their own, lol visualizer.viewport.deactivate() } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index b356f501..33f32d07 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -125,13 +125,15 @@ public void init(Theme theme) { frame = new JFrame(); viewport = new SwingOpenCvViewport(new Size(1080, 720)); - JLayeredPane panel = viewport.skiaPanel(); + JLayeredPane skiaPanel = viewport.skiaPanel(); + skiaPanel.setLayout(new BorderLayout()); - frame.add(panel); + frame.add(skiaPanel); menuBar = new TopMenuBar(this, eocvSim); tunerMenuPanel = new JPanel(); + skiaPanel.add(tunerMenuPanel, BorderLayout.SOUTH); pipelineSelectorPanel = new PipelineSelectorPanel(eocvSim); sourceSelectorPanel = new SourceSelectorPanel(eocvSim); @@ -149,59 +151,32 @@ public void init(Theme theme) { * IMG VISUALIZER & SCROLL PANE */ - imgScrollPane = new JScrollPane(); - - imgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); - imgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - - imgScrollPane.getHorizontalScrollBar().setUnitIncrement(16); - imgScrollPane.getVerticalScrollBar().setUnitIncrement(16); - rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS)); /* * PIPELINE SELECTOR */ - /* + pipelineSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(pipelineSelectorPanel); */ + rightContainer.add(pipelineSelectorPanel); /* * SOURCE SELECTOR */ -/* sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(sourceSelectorPanel);/* + sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); + rightContainer.add(sourceSelectorPanel); /* * TELEMETRY */ - /* telemetryPanel.setBorder(new EmptyBorder(0, 20, 20, 20)); - rightContainer.add(telemetryPanel);*/ - - /* - * SPLIT - */ - - /* - //left side, image scroll & tuner menu split panel - imageTunerSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, imgScrollPane, tunerMenuPanel); - - imageTunerSplitPane.setResizeWeight(1); - imageTunerSplitPane.setOneTouchExpandable(false); - imageTunerSplitPane.setContinuousLayout(true); + rightContainer.add(telemetryPanel); //global - globalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, imageTunerSplitPane, rightContainer); - - globalSplitPane.setResizeWeight(1); - globalSplitPane.setOneTouchExpandable(false); - globalSplitPane.setContinuousLayout(true); + frame.getContentPane().setDropTarget(new InputSourceDropTarget(eocvSim)); - globalSplitPane.setDropTarget(new InputSourceDropTarget(eocvSim)); - - frame.add(globalSplitPane, BorderLayout.CENTER); */ + frame.add(rightContainer, BorderLayout.EAST); //initialize other various stuff of the frame frame.setSize(780, 645); @@ -216,8 +191,6 @@ public void init(Theme theme) { frame.setExtendedState(JFrame.MAXIMIZED_BOTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - // globalSplitPane.setDividerLocation(1070); - // colorPicker = new ColorPicker(viewport.image); frame.setVisible(true); @@ -249,18 +222,18 @@ public void windowClosing(WindowEvent e) { //handling onViewportTapped evts viewport.getComponent().addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { - if(!colorPicker.isPicking()) +// if(!colorPicker.isPicking()) eocvSim.pipelineManager.callViewportTapped(); } }); //VIEWPORT RESIZE HANDLING - imgScrollPane.addMouseWheelListener(e -> { - if (isCtrlPressed) { //check if control key is pressed + // imgScrollPane.addMouseWheelListener(e -> { + // if (isCtrlPressed) { //check if control key is pressed // double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation()); // viewport.setViewportScale(scale); - } - }); + // } + // }); //listening for keyboard presses and releases, to check if ctrl key was pressed or released (handling zoom) KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ke -> { 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 4cba9b69..975452e5 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 @@ -42,6 +42,7 @@ import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl import org.opencv.core.Mat import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.OpenCvViewport +import org.openftc.easyopencv.processFrameInternal import java.lang.reflect.Constructor import java.lang.reflect.Field import java.util.* @@ -268,26 +269,16 @@ class PipelineManager(var eocvSim: EOCVSim) { //a different pipeline at this point. we also call init if we //haven't done so. - if(!hasInitCurrentPipeline && inputMat != null) { - for(pipeHandler in pipelineHandlers) { - pipeHandler.preInit(); - } - - currentPipeline?.init(inputMat) - - for(pipeHandler in pipelineHandlers) { - pipeHandler.init(); - } - - logger.info("Initialized pipeline $currentPipelineName") - - hasInitCurrentPipeline = true - } - //check if we're still active (not timeouted) //after initialization if(inputMat != null) { - currentPipeline?.processFrame(inputMat)?.let { outputMat -> + if(!hasInitCurrentPipeline) { + for(pipeHandler in pipelineHandlers) { + pipeHandler.preInit(); + } + } + + currentPipeline?.processFrameInternal(inputMat)?.let { outputMat -> if (isActive) { pipelineFpsCounter.update() @@ -303,6 +294,16 @@ class PipelineManager(var eocvSim: EOCVSim) { } } } + + if(!hasInitCurrentPipeline) { + for(pipeHandler in pipelineHandlers) { + pipeHandler.init(); + } + + logger.info("Initialized pipeline $currentPipelineName") + + hasInitCurrentPipeline = true + } } if(!isActive) { diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt b/EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt new file mode 100644 index 00000000..eaba57f7 --- /dev/null +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt @@ -0,0 +1,5 @@ +package org.openftc.easyopencv + +import org.opencv.core.Mat + +fun OpenCvPipeline.processFrameInternal(frame: Mat): Mat? = processFrameInternal(frame) \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java new file mode 100644 index 00000000..2ade7b7a --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java @@ -0,0 +1,39 @@ +package org.firstinspires.ftc.teamcode; + +import android.graphics.Canvas; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvPipeline; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +public class AprilTagProcessorPipeline extends TimestampedOpenCvPipeline { + + AprilTagProcessor processor = new AprilTagProcessor.Builder() + .setOutputUnits(DistanceUnit.METER, AngleUnit.DEGREES) + .setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) + .setDrawAxes(true) + .setDrawTagOutline(true) + .setDrawCubeProjection(true) + .build(); + + @Override + public void init(Mat firstFrame) { + processor.init(firstFrame.width(), firstFrame.height(), null); + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) { + requestViewportDrawHook(processor.processFrame(input, captureTimeNanos)); + + return input; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { + processor.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, userContext); + } + +} diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java index 573484e6..86ba87a4 100644 --- a/Vision/src/main/java/android/graphics/Canvas.java +++ b/Vision/src/main/java/android/graphics/Canvas.java @@ -75,7 +75,7 @@ public Canvas drawText(String text, float x, float y, Paint paint) { } public Canvas rotate(float degrees, float xCenter, float yCenter) { - theCanvas.rotate(degrees); + theCanvas.rotate(degrees, xCenter, yCenter); return this; } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java index 660c5739..efbddee1 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -108,6 +108,7 @@ public void requestOpModeStop() { @Override public final void init(Mat mat) { init(); + telemetry.update(); } private boolean startCalled = false; @@ -117,9 +118,11 @@ public final Mat processFrame(Mat input, long captureTimeNanos) { if(!startCalled) { start(); startCalled = true; + telemetry.update(); } loop(); + telemetry.update(); return null; // OpModes don't actually show anything to the viewport, we'll delegate that } diff --git a/Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt b/Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt new file mode 100644 index 00000000..c490d7ae --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt @@ -0,0 +1,19 @@ +package io.github.deltacv.vision + +import android.graphics.Canvas +import org.openftc.easyopencv.OpenCvViewport +import org.openftc.easyopencv.OpenCvViewport.RenderHook + +object PipelineRenderHook : RenderHook { + override fun onDrawFrame(canvas: Canvas, onscreenWidth: Int, onscreenHeight: Int, scaleBmpPxToCanvasPx: Float, canvasDensityScale: Float, userContext: Any) { + val frameContext = userContext as OpenCvViewport.FrameContext + + // We must make sure that we call onDrawFrame() for the same pipeline which set the + // context object when requesting a draw hook. (i.e. we can't just call onDrawFrame() + // for whatever pipeline happens to be currently attached; it might have an entirely + // different notion of what to expect in the context object) + if (frameContext.generatingPipeline != null) { + frameContext.generatingPipeline.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, canvasDensityScale, frameContext.userContext) + } + } +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java index 7863cbc5..aed2acf3 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java +++ b/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java @@ -74,18 +74,7 @@ public void stopStreaming() { @Override protected OpenCvViewport setupViewport() { - handedViewport.setRenderHook((canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, context) -> { - OpenCvViewport.FrameContext frameContext = (OpenCvViewport.FrameContext) context; - - // We must make sure that we call onDrawFrame() for the same pipeline which set the - // context object when requesting a draw hook. (i.e. we can't just call onDrawFrame() - // for whatever pipeline happens to be currently attached; it might have an entirely - // different notion of what to expect in the context object) - if (frameContext.generatingPipeline != null) - { - frameContext.generatingPipeline.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, frameContext.userContext); - } - }); + handedViewport.setRenderHook(PipelineRenderHook.INSTANCE); return handedViewport; } diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt index 01498879..cfdfdfe7 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt @@ -96,12 +96,11 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { skiaLayer.skikoView = GenericSkikoView(skiaLayer, object: SkikoView { override fun onRender(canvas: org.jetbrains.skia.Canvas, width: Int, height: Int, nanoTime: Long) { - // renderCanvas(Canvas(canvas, width, height)) - - canvas.clear(Color.BLUE) + renderCanvas(Canvas(canvas, width, height)) } }) + setSize(size.width.toInt(), size.height.toInt()) } diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java index 484aca1e..3a697a30 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java @@ -276,6 +276,8 @@ void drawTagID(AprilTagDetection detection, Canvas canvas) float tag_id_text_x = id_x + 10*canvasDensityScale; float tag_id_text_y = id_y + 40*canvasDensityScale; + System.out.println(id_x + " " + id_y + " - " + tag_id_width + " " + tag_id_height + " - " + tag_id_text_x + " " + tag_id_text_y); + Point lowerLeft = detection.corners[0]; Point lowerRight = detection.corners[1]; diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java index c7728504..ea82e745 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java @@ -56,14 +56,14 @@ import org.openftc.apriltag.AprilTagDetectorJNI; import org.openftc.apriltag.ApriltagDetectionJNI; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; public class AprilTagProcessorImpl extends AprilTagProcessor { public static final String TAG = "AprilTagProcessorImpl"; - Logger logger = org.slf4j.LoggerFactory.getLogger(TAG); - + Logger logger = LoggerFactory.getLogger(TAG); private long nativeApriltagPtr; private Mat grey = new Mat(); private ArrayList detections = new ArrayList<>(); @@ -126,7 +126,7 @@ protected void finalize() } else { - System.out.println("AprilTagDetectionPipeline.finalize(): nativeApriltagPtr was NULL"); + logger.warn("AprilTagDetectionPipeline.finalize(): nativeApriltagPtr was NULL"); } } @@ -151,7 +151,7 @@ else if (fx == 0 && fy == 0 && cx == 0 && cy == 0) // set it to *something* so we don't crash the native code String warning = "User did not provide a camera calibration, nor was a built-in calibration found for this camera; 6DOF pose data will likely be inaccurate."; - logger.warn(TAG, warning); + logger.warn(warning); // RobotLog.addGlobalWarningMessage(warning); fx = 578.272; diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java index 346bf806..53a2c53e 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java @@ -6,7 +6,7 @@ public abstract class OpenCvPipeline { private Object userContext; - private boolean isFirstFrame; + private boolean isFirstFrame = true; private long firstFrameTimestamp; public abstract Mat processFrame(Mat input); @@ -64,6 +64,7 @@ Mat processFrameInternal(Mat input) if(isFirstFrame) { init(input); + firstFrameTimestamp = System.currentTimeMillis(); isFirstFrame = false; } diff --git a/build.common.gradle b/build.common.gradle index 808d5030..e67f6f18 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -7,7 +7,7 @@ if (project.getPluginManager().hasPlugin("org.jetbrains.kotlin.jvm")) { compileKotlin { kotlinOptions { jvmTarget = "10" - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } } } From 0ff618995d175b967b791da7f83ac144ac816de8 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 11 Aug 2023 15:19:29 -0600 Subject: [PATCH 15/46] Utils.matToBitmap kinda working --- .../handling/EOCVSimUncaughtExceptionHandler.kt | 3 ++- .../src/main/java/android/graphics/Bitmap.java | 2 +- .../deltacv/vision/gui/SwingOpenCvViewport.kt | 2 +- .../apriltag/AprilTagCanvasAnnotator.java | 2 -- .../src/main/java/org/opencv/android/Utils.java | 17 +++++++++++++++-- .../openftc/easyopencv/OpenCvViewRenderer.java | 6 ++---- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt index dd995bb4..82926c38 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt @@ -2,6 +2,7 @@ package com.github.serivesmejia.eocvsim.util.exception.handling import com.github.serivesmejia.eocvsim.currentMainThread import com.github.serivesmejia.eocvsim.util.loggerForThis +import javax.swing.SwingUtilities import kotlin.system.exitProcess class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExceptionHandler { @@ -33,7 +34,7 @@ class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExc //Exit if uncaught exception happened in the main thread //since we would be basically in a deadlock state if that happened //or if we have a lotta uncaught exceptions. - if(t == currentMainThread || e !is Exception || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { + if(t == currentMainThread || SwingUtilities.isEventDispatchThread() || e !is Exception || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { CrashReport(e).saveCrashReport() logger.warn("If this error persists, open an issue on EOCV-Sim's GitHub attaching the crash report file.") diff --git a/Vision/src/main/java/android/graphics/Bitmap.java b/Vision/src/main/java/android/graphics/Bitmap.java index de7c3513..1a762765 100644 --- a/Vision/src/main/java/android/graphics/Bitmap.java +++ b/Vision/src/main/java/android/graphics/Bitmap.java @@ -196,7 +196,7 @@ public static Bitmap createBitmap(int width, int height) { public static Bitmap createBitmap(int width, int height, Config config) { Bitmap bm = new Bitmap(); - bm.theBitmap.allocPixels(new ImageInfo(width, height, configToColorType(config), ColorAlphaType.OPAQUE)); + bm.theBitmap.allocPixels(new ImageInfo(width, height, configToColorType(config), ColorAlphaType.PREMUL)); bm.theBitmap.erase(0); diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt index cfdfdfe7..a0642e0c 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt @@ -96,7 +96,7 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { skiaLayer.skikoView = GenericSkikoView(skiaLayer, object: SkikoView { override fun onRender(canvas: org.jetbrains.skia.Canvas, width: Int, height: Int, nanoTime: Long) { - renderCanvas(Canvas(canvas, width, height)) + // renderCanvas(Canvas(canvas, width, height)) } }) diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java index 3a697a30..484aca1e 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java @@ -276,8 +276,6 @@ void drawTagID(AprilTagDetection detection, Canvas canvas) float tag_id_text_x = id_x + 10*canvasDensityScale; float tag_id_text_y = id_y + 40*canvasDensityScale; - System.out.println(id_x + " " + id_y + " - " + tag_id_width + " " + tag_id_height + " - " + tag_id_text_x + " " + tag_id_text_y); - Point lowerLeft = detection.corners[0]; Point lowerRight = detection.corners[1]; diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java index 7814fd46..15108f47 100644 --- a/Vision/src/main/java/org/opencv/android/Utils.java +++ b/Vision/src/main/java/org/opencv/android/Utils.java @@ -1,10 +1,13 @@ package org.opencv.android; import android.graphics.Bitmap; +import org.jetbrains.skia.impl.BufferUtil; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.imgproc.Imgproc; +import java.nio.ByteBuffer; + public class Utils { /** @@ -69,6 +72,8 @@ private static void nBitmapToMat2(Bitmap b, Mat mat, boolean unPremultiplyAlpha) } + private static byte[] data = new byte[0]; + private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { Mat tmp; @@ -97,10 +102,18 @@ private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { } } - byte[] data = new byte[tmp.rows() * tmp.cols()]; + int size = tmp.rows() * tmp.cols(); + + if(data.length != tmp.rows() * tmp.cols()) { + data = new byte[size]; + } + tmp.get(0, 0, data); - b.theBitmap.installPixels(data); + long addr = b.theBitmap.peekPixels().getAddr(); + ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, tmp.cols() * tmp.rows()); + + buffer.put(data); tmp.release(); } diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java index e1be7c3b..6a7a1dcc 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -250,13 +250,11 @@ public void render(Mat mat, Canvas canvas, OpenCvViewport.RenderHook userHook, O } bitmapFromMat = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888); - - System.out.println("create bitmapFromMat"); } //Convert that Mat to a bitmap we can render - // Utils.matToBitmap(mat, bitmapFromMat, false); - + Utils.matToBitmap(mat, bitmapFromMat, false); + width = bitmapFromMat.getWidth(); height = bitmapFromMat.getHeight(); aspectRatio = (float) width / height; From 9997c7146c32c745df1377091ae0a656b729e4b3 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 12 Aug 2023 07:25:11 -0600 Subject: [PATCH 16/46] Fully working SwingOpenCvViewport implementation --- .../org/openftc/easyopencv/MatRecycler.java | 7 +++ .../github/serivesmejia/eocvsim/EOCVSim.kt | 25 +++++++- .../serivesmejia/eocvsim/gui/Visualizer.java | 2 +- .../eocvsim/input/source/VideoSource.java | 19 +++--- .../eocvsim/pipeline/PipelineManager.kt | 16 ++++- .../util/PipelineStatisticsCalculator.kt | 61 +++++++++++++++++++ .../eocvsim/util/event/EventHandler.kt | 4 +- .../eocvsim/util/event/EventListener.kt | 7 +++ .../main/java/android/graphics/Bitmap.java | 4 ++ .../main/java/android/graphics/Canvas.java | 21 +++++-- .../deltacv/vision/gui/SwingOpenCvViewport.kt | 27 +++++--- .../main/java/org/opencv/android/Utils.java | 8 +-- .../easyopencv/OpenCvViewRenderer.java | 14 +++-- 13 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt diff --git a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java index 1ea1c7f9..30b7e4d2 100644 --- a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java +++ b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java @@ -133,5 +133,12 @@ public void returnMat() { public boolean isCheckedOut() { return checkedOut; } + @Override + public void copyTo(Mat mat) { + super.copyTo(mat); + if(mat instanceof RecyclableMat) { + ((RecyclableMat) mat).setContext(getContext()); + } + } } } 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 c40b6b62..6af8139f 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 @@ -32,6 +32,7 @@ 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 com.github.serivesmejia.eocvsim.pipeline.util.PipelineStatisticsCalculator import com.github.serivesmejia.eocvsim.tuner.TunerManager import com.github.serivesmejia.eocvsim.util.ClasspathScan import com.github.serivesmejia.eocvsim.util.FileFilters @@ -128,7 +129,10 @@ class EOCVSim(val params: Parameters = Parameters()) { val inputSourceManager = InputSourceManager(this) @JvmField - val pipelineManager = PipelineManager(this) + val pipelineStatisticsCalculator = PipelineStatisticsCalculator() + + @JvmField + val pipelineManager = PipelineManager(this, pipelineStatisticsCalculator) @JvmField val tunerManager = TunerManager(this) @@ -222,9 +226,14 @@ class EOCVSim(val params: Parameters = Parameters()) { //post output mats from the pipeline to the visualizer viewport pipelineManager.pipelineOutputPosters.add(visualizer.viewport) + // now that we have two different runnable units (OpenCvPipeline and OpMode) + // we have to give a more special treatment to the OpenCvPipeline + // OpModes can take care of themselves, setting up their own stuff + // but we need to do some hand holding for OpenCvPipelines... pipelineManager.onPipelineChange { + pipelineStatisticsCalculator.init() + if(pipelineManager.currentPipeline !is OpMode && pipelineManager.currentPipeline != null) { - // do some hand holding as pipelines cannot do this themselves... visualizer.viewport.activate() visualizer.viewport.setRenderHook(PipelineRenderHook) // calls OpenCvPipeline#onDrawFrame on the viewport (UI) thread } else { @@ -233,6 +242,16 @@ class EOCVSim(val params: Parameters = Parameters()) { } } + pipelineManager.onUpdate { + if(pipelineManager.currentPipeline !is OpMode && pipelineManager.currentPipeline != null) { + visualizer.viewport.notifyStatistics( + pipelineStatisticsCalculator.avgFps, + pipelineStatisticsCalculator.avgPipelineTime, + pipelineStatisticsCalculator.avgOverheadTime + ) + } + } + start() } @@ -245,6 +264,8 @@ class EOCVSim(val params: Parameters = Parameters()) { updateVisualizerTitle() + pipelineStatisticsCalculator.newInputFrameStart() + inputSourceManager.update(pipelineManager.paused) tunerManager.update() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 33f32d07..433f625a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -124,7 +124,7 @@ public void init(Theme theme) { //instantiate all swing elements after theme installation frame = new JFrame(); - viewport = new SwingOpenCvViewport(new Size(1080, 720)); + viewport = new SwingOpenCvViewport(new Size(1080, 720), "deltacv EOCV-Sim v" + Build.standardVersionString); JLayeredPane skiaPanel = viewport.skiaPanel(); skiaPanel.setLayout(new BorderLayout()); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index 300b8e61..b8a883e9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -25,6 +25,7 @@ import com.github.serivesmejia.eocvsim.input.InputSource; import com.github.serivesmejia.eocvsim.util.FileFilters; +import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter; import com.google.gson.annotations.Expose; import org.opencv.core.Mat; import org.opencv.core.Size; @@ -44,6 +45,8 @@ public class VideoSource extends InputSource { private transient VideoCapture video; + private transient FpsLimiter fpsLimiter = new FpsLimiter(30); + private transient MatRecycler.RecyclableMat lastFramePaused; private transient MatRecycler.RecyclableMat lastFrame; @@ -70,7 +73,6 @@ public VideoSource(String videoPath, Size size) { @Override public boolean init() { - if (initialized) return false; initialized = true; @@ -94,16 +96,16 @@ public boolean init() { return false; } + fpsLimiter.setMaxFPS(video.get(Videoio.CAP_PROP_FPS)); + newFrame.release(); matRecycler.returnMat(newFrame); return true; - } @Override public void reset() { - if (!initialized) return; if (video != null && video.isOpened()) video.release(); @@ -117,7 +119,6 @@ public void reset() { video = null; initialized = false; - } @Override @@ -135,7 +136,6 @@ public void close() { @Override public Mat update() { - if (isPaused) { return lastFramePaused; } else if (lastFramePaused != null) { @@ -143,6 +143,12 @@ public Mat update() { lastFramePaused = null; } + try { + fpsLimiter.sync(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (lastFrame == null) lastFrame = matRecycler.takeMat(); if (video == null) return lastFrame; @@ -171,12 +177,10 @@ public Mat update() { matRecycler.returnMat(newFrame); return lastFrame; - } @Override public void onPause() { - if (lastFrame != null) lastFrame.release(); if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); @@ -191,7 +195,6 @@ public void onPause() { video.release(); video = null; - } @Override 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 975452e5..8bf3e652 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 @@ -29,12 +29,12 @@ import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot +import com.github.serivesmejia.eocvsim.pipeline.util.PipelineStatisticsCalculator import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import com.github.serivesmejia.eocvsim.util.loggerForThis -import com.qualcomm.robotcore.eventloop.opmode.OpMode import io.github.deltacv.common.image.MatPoster import kotlinx.coroutines.* import org.firstinspires.ftc.robotcore.external.Telemetry @@ -50,7 +50,7 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.roundToLong @OptIn(DelicateCoroutinesApi::class) -class PipelineManager(var eocvSim: EOCVSim) { +class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: PipelineStatisticsCalculator) { companion object { const val MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS = 5 @@ -257,6 +257,8 @@ class PipelineManager(var eocvSim: EOCVSim) { "processFrame" } + pipelineStatisticsCalculator.newPipelineFrameStart() + //run our pipeline in the background until it finishes or gets cancelled val pipelineJob = GlobalScope.launch(currentPipelineContext!!) { try { @@ -278,7 +280,13 @@ class PipelineManager(var eocvSim: EOCVSim) { } } - currentPipeline?.processFrameInternal(inputMat)?.let { outputMat -> + pipelineStatisticsCalculator.beforeProcessFrame() + + val pipelineResult =currentPipeline?.processFrameInternal(inputMat) + + pipelineStatisticsCalculator.afterProcessFrame() + + pipelineResult?.let { outputMat -> if (isActive) { pipelineFpsCounter.update() @@ -328,6 +336,8 @@ class PipelineManager(var eocvSim: EOCVSim) { updateExceptionTracker(ex) } } + + pipelineStatisticsCalculator.endFrame() } runBlocking { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt new file mode 100644 index 00000000..e4b86620 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt @@ -0,0 +1,61 @@ +package com.github.serivesmejia.eocvsim.pipeline.util + +import com.qualcomm.robotcore.util.ElapsedTime +import com.qualcomm.robotcore.util.MovingStatistics +import kotlin.math.roundToInt + +class PipelineStatisticsCalculator { + + private lateinit var msFrameIntervalRollingAverage: MovingStatistics + private lateinit var msUserPipelineRollingAverage: MovingStatistics + private lateinit var msTotalFrameProcessingTimeRollingAverage: MovingStatistics + private lateinit var timer: ElapsedTime + + private var currentFrameStartTime = 0L + private var pipelineStart = 0L + + var avgFps = 0f + private set + var avgPipelineTime = 0 + private set + var avgOverheadTime = 0 + private set + var avgTotalFrameTime = 0 + private set + + fun init() { + msFrameIntervalRollingAverage = MovingStatistics(30) + msUserPipelineRollingAverage = MovingStatistics(30) + msTotalFrameProcessingTimeRollingAverage = MovingStatistics(30) + timer = ElapsedTime() + } + + fun newInputFrameStart() { + currentFrameStartTime = System.currentTimeMillis(); + } + + fun newPipelineFrameStart() { + msFrameIntervalRollingAverage.add(timer.milliseconds()) + timer.reset() + + val secondsPerFrame = msFrameIntervalRollingAverage.mean / 1000.0 + avgFps = (1.0 / secondsPerFrame).toFloat() + } + + fun beforeProcessFrame() { + pipelineStart = System.currentTimeMillis() + } + + fun afterProcessFrame() { + msUserPipelineRollingAverage.add((System.currentTimeMillis() - pipelineStart).toDouble()) + avgPipelineTime = msUserPipelineRollingAverage.mean.roundToInt() + } + + fun endFrame() { + msTotalFrameProcessingTimeRollingAverage.add((System.currentTimeMillis() - currentFrameStartTime).toDouble()) + + avgTotalFrameTime = msTotalFrameProcessingTimeRollingAverage.mean.roundToInt() + avgOverheadTime = avgTotalFrameTime - avgPipelineTime + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt index e0dbed6b..9fb0062d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt @@ -101,12 +101,14 @@ class EventHandler(val name: String) : Runnable { fun doOnce(runnable: Runnable) = doOnce { runnable.run() } - fun doPersistent(listener: EventListener) { + fun doPersistent(listener: EventListener): EventListenerRemover { synchronized(lock) { internalListeners.add(listener) } if(callRightAway) runListener(listener, false) + + return EventListenerRemover(this, listener, false) } fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt index 532802f9..015d81e8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt @@ -32,6 +32,9 @@ class EventListenerRemover( val listener: EventListener, val isOnceListener: Boolean ) { + + private val attached = mutableListOf() + fun removeThis() { if(isOnceListener) handler.removeOnceListener(listener) @@ -39,4 +42,8 @@ class EventListenerRemover( handler.removePersistentListener(listener) } + fun attach(remover: EventListenerRemover) { + + } + } \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Bitmap.java b/Vision/src/main/java/android/graphics/Bitmap.java index 1a762765..c9539372 100644 --- a/Vision/src/main/java/android/graphics/Bitmap.java +++ b/Vision/src/main/java/android/graphics/Bitmap.java @@ -221,6 +221,10 @@ public int getHeight() { return theBitmap.getHeight(); } + public Rect getBounds() { + return new Rect(0, 0, getWidth(), getHeight()); + } + public Config getConfig() { return colorTypeToConfig(theBitmap.getColorType()); } diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java index 86ba87a4..563a0429 100644 --- a/Vision/src/main/java/android/graphics/Canvas.java +++ b/Vision/src/main/java/android/graphics/Canvas.java @@ -23,6 +23,7 @@ package android.graphics; +import org.jetbrains.annotations.NotNull; import org.jetbrains.skia.*; public class Canvas { @@ -98,11 +99,12 @@ public Canvas drawLines(float[] points, Paint paint) { return this; } - public void drawRect(Rect rect, Paint paint) { + public Canvas drawRect(Rect rect, Paint paint) { theCanvas.drawRect(rect.toSkijaRect(), paint.thePaint); + return this; } - public void drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { + public Canvas drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { int left, top, right, bottom; if (src == null) { left = top = 0; @@ -126,18 +128,27 @@ public void drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { new org.jetbrains.skia.Rect(left, top, right, bottom), rect.toSkijaRect(), thePaint ); + + return this; } - public void translate(int dx, int dy) { + public Canvas translate(int dx, int dy) { theCanvas.translate(dx, dy); + return this; } - public void restoreToCount(int saveCount) { + public Canvas restoreToCount(int saveCount) { theCanvas.restoreToCount(saveCount); + return this; + } + + public boolean readPixels(@NotNull Bitmap lastFrame, int srcX, int srcY) { + return theCanvas.readPixels(lastFrame.theBitmap, srcX, srcY); } - public void drawColor(int color) { + public Canvas drawColor(int color) { theCanvas.clear(color); + return this; } public int getWidth() { diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt index a0642e0c..9111e472 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt @@ -50,7 +50,7 @@ import javax.swing.JComponent import javax.swing.JPanel import javax.swing.SwingUtilities -class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { +class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Vision") : OpenCvViewport, MatPoster { private val syncObj = Any() @@ -76,7 +76,7 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { @Volatile private var internalRenderingState = RenderingState.STOPPED - val renderer: OpenCvViewRenderer = OpenCvViewRenderer(false) + val renderer: OpenCvViewRenderer = OpenCvViewRenderer(false, fpsMeterDescriptor) private val skiaLayer = SkiaLayer() val component: JComponent get() = skiaLayer @@ -96,7 +96,7 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { skiaLayer.skikoView = GenericSkikoView(skiaLayer, object: SkikoView { override fun onRender(canvas: org.jetbrains.skia.Canvas, width: Int, height: Int, nanoTime: Long) { - // renderCanvas(Canvas(canvas, width, height)) + renderCanvas(Canvas(canvas, width, height)) } }) @@ -278,15 +278,20 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { } } + private lateinit var lastFrame: MatRecycler.RecyclableMat private fun renderCanvas(canvas: Canvas) { + if(!::lastFrame.isInitialized) { + lastFrame = framebufferRecycler!!.takeMat() + } + when (internalRenderingState) { RenderingState.ACTIVE -> { shouldPaintOrange = true - var mat: MatRecycler.RecyclableMat = try { + val mat: MatRecycler.RecyclableMat = try { //Grab a Mat from the frame queue - val frame = visionPreviewFrameQueue.poll(10, TimeUnit.MILLISECONDS) ?: return; + val frame = visionPreviewFrameQueue.poll(10, TimeUnit.MILLISECONDS) ?: lastFrame frame } catch (e: InterruptedException) { @@ -299,6 +304,12 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { return } + mat.copyTo(lastFrame) + + if(mat.empty()) { + return // nope out + } + /* * For some reason, the canvas will very occasionally be null upon closing. * Stack Overflow seems to suggest this means the canvas has been destroyed. @@ -313,7 +324,9 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { } //We're done with that Mat object; return it to the Mat recycler so it can be used again later - framebufferRecycler!!.returnMat(mat) + if(mat != lastFrame) { + framebufferRecycler!!.returnMat(mat) + } } RenderingState.PAUSED -> { @@ -344,6 +357,6 @@ class SwingOpenCvViewport(size: Size) : OpenCvViewport, MatPoster { companion object { private const val VISION_PREVIEW_FRAME_QUEUE_CAPACITY = 2 - private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 2 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1). + private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 3 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1). } } diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java index 15108f47..7e8634fc 100644 --- a/Vision/src/main/java/org/opencv/android/Utils.java +++ b/Vision/src/main/java/org/opencv/android/Utils.java @@ -87,7 +87,7 @@ private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGB2BGRA); } else if(src.type() == CvType.CV_8UC4){ if(premultiplyAlpha) Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2mRGBA); - else src.copyTo(tmp); + else Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2BGRA); } } else { tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC2); @@ -102,16 +102,16 @@ private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { } } - int size = tmp.rows() * tmp.cols(); + int size = tmp.rows() * tmp.cols() * tmp.channels(); - if(data.length != tmp.rows() * tmp.cols()) { + if(data.length != size) { data = new byte[size]; } tmp.get(0, 0, data); long addr = b.theBitmap.peekPixels().getAddr(); - ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, tmp.cols() * tmp.rows()); + ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, size); buffer.put(data); diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java index 6a7a1dcc..737930f3 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -50,9 +50,10 @@ public class OpenCvViewRenderer private double aspectRatio; private boolean fpsMeterEnabled = true; - private float fps = 0; - private int pipelineMs = 0; - private int overheadMs = 0; + private String fpsMeterDescriptor; + private volatile float fps = 0; + private volatile int pipelineMs = 0; + private volatile int overheadMs = 0; private int width; private int height; @@ -66,9 +67,10 @@ public class OpenCvViewRenderer private Bitmap bitmapFromMat; - public OpenCvViewRenderer(boolean renderingOffsceen) + public OpenCvViewRenderer(boolean renderingOffsceen, String fpsMeterDescriptor) { offscreen = renderingOffsceen; + this.fpsMeterDescriptor = fpsMeterDescriptor; metricsScale = 1.0f; @@ -208,7 +210,7 @@ private void drawStats(Canvas canvas, Rect rect) int textLine3Y = textLine2Y + statBoxTextLineSpacing; // Draw the 3 text lines - canvas.drawText(String.format("deltacv EOCV-Sim"), statBoxLTxtStart, textLine1Y, fpsMeterTextPaint); + canvas.drawText(fpsMeterDescriptor, statBoxLTxtStart, textLine1Y, fpsMeterTextPaint); canvas.drawText(String.format("FPS@%dx%d: %.2f", width, height, fps), statBoxLTxtStart, textLine2Y, fpsMeterTextPaint); canvas.drawText(String.format("Pipeline: %dms - Overhead: %dms", pipelineMs, overheadMs), statBoxLTxtStart, textLine3Y, fpsMeterTextPaint); } @@ -322,7 +324,7 @@ public void renderPaused(Canvas canvas) int textLine3Y = textLine2Y + statBoxTextLineSpacing; // Draw the 3 text lines - canvas.drawText(String.format("deltacv EOCV-Sim"), statBoxLTxtStart, textLine1Y, fpsMeterTextPaint); + canvas.drawText(fpsMeterDescriptor, statBoxLTxtStart, textLine1Y, fpsMeterTextPaint); canvas.drawText("VIEWPORT PAUSED", statBoxLTxtStart, textLine2Y, fpsMeterTextPaint); //canvas.drawText("Hi", statBoxLTxtStart, textLine3Y, fpsMeterTextPaint); } From a610a161e1ecda0390b5b66f72344d210ae35459 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 12 Aug 2023 09:55:16 -0600 Subject: [PATCH 17/46] OpModes/VisionPortal initial work, currently rendering --- .../main/java/android/hardware/Camera.java | 58 +++++++ .../java/androidx/annotation/Nullable.java | 42 ++++++ .../qualcomm/robotcore/util/SerialNumber.java | 106 +++++++++++++ .../util/PipelineStatisticsCalculator.kt | 2 +- .../github/serivesmejia/eocvsim/EOCVSim.kt | 7 +- .../github/serivesmejia/eocvsim/gui/Icons.kt | 2 +- .../serivesmejia/eocvsim/gui/Visualizer.java | 2 +- .../gui/component/tuner/ColorPicker.kt | 2 +- .../tuner/TunableFieldPanelOptions.kt | 2 +- .../eocvsim/gui/dialog/About.java | 2 +- .../gui/dialog/source/CreateImageSource.java | 2 +- .../gui/dialog/source/CreateVideoSource.java | 2 +- .../eocvsim/gui/util/GuiUtil.java | 2 +- .../eocvsim/input/InputSource.java | 3 + .../eocvsim/input/source/CameraSource.java | 5 + .../eocvsim/output/VideoRecordingSession.kt | 4 +- .../eocvsim/pipeline/PipelineManager.kt | 2 +- .../pipeline/util/PipelineExceptionTracker.kt | 6 +- .../eventloop/opmode/OpModePipelineHandler.kt | 13 +- .../eocvsim/input/VisionInputSource.kt | 39 +++++ .../eocvsim/input/VisionInputSourceHander.kt | 18 +++ .../external/samples/ConceptAprilTagEasy.java | 141 ++++++++++++++++++ .../ftc/teamcode/AprilTagTestOpMode.java | 17 --- .../eventloop/opmode/LinearOpMode.java | 62 ++++++++ .../robotcore/eventloop/opmode/OpMode.java | 2 +- .../robotcore/hardware/HardwareMap.java | 10 +- .../{ => external}/PipelineRenderHook.kt | 2 +- .../{ => external}/SourcedOpenCvCamera.java | 53 +++++-- .../vision/{ => external}/gui/SkiaPanel.kt | 2 +- .../{ => external}/gui/SwingOpenCvViewport.kt | 2 +- .../{ => external}/gui/component/ImageX.java | 6 +- .../{ => external}/gui/util/ImgUtil.java | 2 +- .../external/source/ThreadSourceHander.java | 27 ++++ .../source/ViewportAndSourceHander.java | 9 ++ .../vision/external/source/VisionSource.java | 18 +++ .../external/source/VisionSourceBase.java | 81 ++++++++++ .../external/source/VisionSourceHander.java | 7 + .../vision/external/source/VisionSourced.java | 7 + .../source/ftc/SourcedCameraNameImpl.java | 71 +++++++++ .../vision/{ => external}/util/CvUtil.java | 4 +- .../{ => external}/util/FrameQueue.java | 2 +- .../vision/external/util/Timestamped.java | 21 +++ .../{ => external}/util/extension/CvExt.kt | 2 +- .../source/ftc/SourcedCameraName.java | 37 +++++ .../github/deltacv/vision/source/Source.java | 18 --- .../deltacv/vision/source/SourceHander.java | 7 - .../github/deltacv/vision/source/Sourced.java | 7 - .../camera/BuiltinCameraDirection.java | 7 + .../hardware/camera/CameraControls.java | 46 ++++++ .../external/hardware/camera/WebcamName.java | 63 +++++++- .../camera/controls/CameraControl.java | 42 ++++++ .../{VisionPortal.javaa => VisionPortal.java} | 12 +- ...PortalImpl.javaa => VisionPortalImpl.java} | 105 +------------ .../main/java/org/opencv/android/Utils.java | 2 +- .../openftc/easyopencv/OpenCvCameraBase.java | 42 +++--- .../easyopencv/OpenCvCameraFactory.java | 38 +++++ .../easyopencv/OpenCvInternalCamera.java | 40 +++++ .../easyopencv/OpenCvInternalCamera2.java | 40 +++++ .../org/openftc/easyopencv/OpenCvWebcam.java | 44 ++++++ .../SourcedOpenCvCameraFactoryImpl.java | 63 ++++++++ 60 files changed, 1252 insertions(+), 230 deletions(-) create mode 100644 Common/src/main/java/android/hardware/Camera.java create mode 100644 Common/src/main/java/androidx/annotation/Nullable.java create mode 100644 Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java rename {EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim => Common/src/main/java/io/github/deltacv/common}/pipeline/util/PipelineStatisticsCalculator.kt (97%) create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt create mode 100644 EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java delete mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/PipelineRenderHook.kt (95%) rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/SourcedOpenCvCamera.java (61%) rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/gui/SkiaPanel.kt (94%) rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/gui/SwingOpenCvViewport.kt (99%) rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/gui/component/ImageX.java (93%) rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/gui/util/ImgUtil.java (92%) create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/util/CvUtil.java (98%) rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/util/FrameQueue.java (95%) create mode 100644 Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java rename Vision/src/main/java/io/github/deltacv/vision/{ => external}/util/extension/CvExt.kt (96%) create mode 100644 Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java delete mode 100644 Vision/src/main/java/io/github/deltacv/vision/source/Source.java delete mode 100644 Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java delete mode 100644 Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java create mode 100644 Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java rename Vision/src/main/java/org/firstinspires/ftc/vision/{VisionPortal.javaa => VisionPortal.java} (97%) rename Vision/src/main/java/org/firstinspires/ftc/vision/{VisionPortalImpl.javaa => VisionPortalImpl.java} (73%) create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java create mode 100644 Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java diff --git a/Common/src/main/java/android/hardware/Camera.java b/Common/src/main/java/android/hardware/Camera.java new file mode 100644 index 00000000..09e43e5e --- /dev/null +++ b/Common/src/main/java/android/hardware/Camera.java @@ -0,0 +1,58 @@ +package android.hardware; + +public class Camera { + + + /** + * Information about a camera + */ + public static class CameraInfo { + /** + * The facing of the camera is opposite to that of the screen. + */ + public static final int CAMERA_FACING_BACK = 0; + /** + * The facing of the camera is the same as that of the screen. + */ + public static final int CAMERA_FACING_FRONT = 1; + /** + * The direction that the camera faces. It should be + * CAMERA_FACING_BACK or CAMERA_FACING_FRONT. + */ + public int facing; + /** + *

The orientation of the camera image. The value is the angle that the + * camera image needs to be rotated clockwise so it shows correctly on + * the display in its natural orientation. It should be 0, 90, 180, or 270.

+ * + *

For example, suppose a device has a naturally tall screen. The + * back-facing camera sensor is mounted in landscape. You are looking at + * the screen. If the top side of the camera sensor is aligned with the + * right edge of the screen in natural orientation, the value should be + * 90. If the top side of a front-facing camera sensor is aligned with + * the right of the screen, the value should be 270.

+ * + * see #setDisplayOrientation(int) + * see Parameters#setRotation(int) + * see Parameters#setPreviewSize(int, int) + * see Parameters#setPictureSize(int, int) + * see Parameters#setJpegThumbnailSize(int, int) + */ + public int orientation; + /** + *

Whether the shutter sound can be disabled.

+ * + *

On some devices, the camera shutter sound cannot be turned off + * through {link #enableShutterSound enableShutterSound}. This field + * can be used to determine whether a call to disable the shutter sound + * will succeed.

+ * + *

If this field is set to true, then a call of + * {@code enableShutterSound(false)} will be successful. If set to + * false, then that call will fail, and the shutter sound will be played + * when {link Camera#takePicture takePicture} is called.

+ */ + public boolean canDisableShutterSound; + }; + +} diff --git a/Common/src/main/java/androidx/annotation/Nullable.java b/Common/src/main/java/androidx/annotation/Nullable.java new file mode 100644 index 00000000..059eaf5d --- /dev/null +++ b/Common/src/main/java/androidx/annotation/Nullable.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 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 java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; +/** + * Denotes that a parameter, field or method return value can be null. + *

+ * When decorating a method call parameter, this denotes that the parameter can + * legitimately be null and the method will gracefully deal with it. Typically + * used on optional parameters. + *

+ * When decorating a method, this denotes the method might legitimately return + * null. + *

+ * This is a marker annotation and it has no specific attributes. + * + * @paramDoc This value may be {@code null}. + * @returnDoc This value may be {@code null}. + * @hide + */ +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface Nullable { +} \ No newline at end of file diff --git a/Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java b/Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java new file mode 100644 index 00000000..0456d28d --- /dev/null +++ b/Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java @@ -0,0 +1,106 @@ +/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc + +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 Qualcomm Technologies Inc 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 com.qualcomm.robotcore.util; + +public class SerialNumber { + + + protected final String serialNumberString; + + //------------------------------------------------------------------------------------------------ + // Construction + //------------------------------------------------------------------------------------------------ + + /** + * Constructs a serial number using the supplied initialization string. If the initialization + * string is a legacy form of fake serial number, a unique fake serial number is created. + * + * @param serialNumberString the initialization string for the serial number. + */ + protected SerialNumber(String serialNumberString) { + this.serialNumberString = serialNumberString; + } + + + /** + * Returns the string contents of the serial number. Result is not intended to be + * displayed to humans. + * @see #toString() + */ + public String getString() { + return serialNumberString; + } + + + /** + * Returns the {@link SerialNumber} of the device associated with this one that would appear + * in a {link ScannedDevices}. + */ + public SerialNumber getScannableDeviceSerialNumber() { + return this; + } + + //------------------------------------------------------------------------------------------------ + // Comparison + //------------------------------------------------------------------------------------------------ + + public boolean matches(Object pattern) { + return this.equals(pattern); + } + + @Override + public boolean equals(Object object) { + if (object == null) return false; + if (object == this) return true; + + if (object instanceof SerialNumber) { + return serialNumberString.equals(((SerialNumber) object).serialNumberString); + } + + if (object instanceof String) { + return this.equals((String)object); + } + + return false; + } + + // separate method to avoid annoying Android Studio inspection warnings when comparing SerialNumber against String + public boolean equals(String string) { + return serialNumberString.equals(string); + } + + @Override + public int hashCode() { + return serialNumberString.hashCode() ^ 0xabcd9873; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt b/Common/src/main/java/io/github/deltacv/common/pipeline/util/PipelineStatisticsCalculator.kt similarity index 97% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt rename to Common/src/main/java/io/github/deltacv/common/pipeline/util/PipelineStatisticsCalculator.kt index e4b86620..b59d5d7d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineStatisticsCalculator.kt +++ b/Common/src/main/java/io/github/deltacv/common/pipeline/util/PipelineStatisticsCalculator.kt @@ -1,4 +1,4 @@ -package com.github.serivesmejia.eocvsim.pipeline.util +package io.github.deltacv.common.pipeline.util import com.qualcomm.robotcore.util.ElapsedTime import com.qualcomm.robotcore.util.MovingStatistics 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 6af8139f..79c487d9 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 @@ -32,7 +32,7 @@ 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 com.github.serivesmejia.eocvsim.pipeline.util.PipelineStatisticsCalculator +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 @@ -48,7 +48,7 @@ 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.vision.PipelineRenderHook +import io.github.deltacv.vision.external.PipelineRenderHook import nu.pattern.OpenCV import org.opencv.core.Size import org.openftc.easyopencv.TimestampedPipelineHandler @@ -196,7 +196,6 @@ class EOCVSim(val params: Parameters = Parameters()) { pipelineManager.init() //init pipeline manager (scan for pipelines) pipelineManager.subscribePipelineHandler(TimestampedPipelineHandler()) - pipelineManager.subscribePipelineHandler(OpModePipelineHandler()) tunerManager.init() //init tunable variables manager @@ -216,6 +215,8 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.waitForFinishingInit() + pipelineManager.subscribePipelineHandler(OpModePipelineHandler(visualizer.viewport)) + visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0 visualizer.sourceSelectorPanel.allowSourceSwitching = true diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt index 69ead2c8..cc0697c4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt @@ -25,7 +25,7 @@ package com.github.serivesmejia.eocvsim.gui import com.github.serivesmejia.eocvsim.gui.util.GuiUtil import com.github.serivesmejia.eocvsim.util.loggerForThis -import io.github.deltacv.vision.gui.util.ImgUtil +import io.github.deltacv.vision.external.gui.util.ImgUtil import java.awt.image.BufferedImage import java.util.NoSuchElementException import javax.swing.ImageIcon diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 433f625a..27475102 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -26,7 +26,7 @@ import com.formdev.flatlaf.FlatLaf; import com.github.serivesmejia.eocvsim.Build; import com.github.serivesmejia.eocvsim.EOCVSim; -import io.github.deltacv.vision.gui.SwingOpenCvViewport; +import io.github.deltacv.vision.external.gui.SwingOpenCvViewport; import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.gui.component.visualizer.InputSourceDropTarget; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt index 519318fe..d13994eb 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt @@ -25,7 +25,7 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.gui.Icons -import io.github.deltacv.vision.gui.component.ImageX +import io.github.deltacv.vision.external.gui.component.ImageX import com.github.serivesmejia.eocvsim.util.event.EventHandler import org.opencv.core.Scalar import java.awt.Color 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 30b02da3..a00d2cdb 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 @@ -26,7 +26,7 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.Icons import com.github.serivesmejia.eocvsim.gui.component.PopupX -import io.github.deltacv.vision.util.extension.cvtColor +import io.github.deltacv.vision.external.util.extension.cvtColor import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero import java.awt.FlowLayout import java.awt.GridLayout diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java index f9bc955b..46554282 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java @@ -25,7 +25,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.Icons; -import io.github.deltacv.vision.gui.component.ImageX; +import io.github.deltacv.vision.external.gui.component.ImageX; import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; import com.github.serivesmejia.eocvsim.util.StrUtil; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java index dfe8dc62..e5c44d0f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java @@ -27,7 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; import com.github.serivesmejia.eocvsim.input.source.ImageSource; -import io.github.deltacv.vision.util.CvUtil; +import io.github.deltacv.vision.external.util.CvUtil; import com.github.serivesmejia.eocvsim.util.FileFilters; import com.github.serivesmejia.eocvsim.util.StrUtil; import org.opencv.core.Size; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java index c4e3d165..1c9f5464 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java @@ -27,7 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; import com.github.serivesmejia.eocvsim.input.source.VideoSource; -import io.github.deltacv.vision.util.CvUtil; +import io.github.deltacv.vision.external.util.CvUtil; import com.github.serivesmejia.eocvsim.util.FileFilters; import com.github.serivesmejia.eocvsim.util.StrUtil; import org.opencv.core.Mat; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java index a94e653d..9f5c7653 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java @@ -26,7 +26,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.DialogFactory; import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists; -import io.github.deltacv.vision.util.CvUtil; +import io.github.deltacv.vision.external.util.CvUtil; import com.github.serivesmejia.eocvsim.util.SysUtil; import org.opencv.core.Mat; import org.slf4j.Logger; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java index 95ff57a8..c9db10b8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java @@ -25,6 +25,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import org.opencv.core.Mat; +import org.opencv.core.Size; import javax.swing.filechooser.FileFilter; @@ -49,6 +50,8 @@ public abstract class InputSource implements Comparable { public abstract void onResume(); + public void setSize(Size size) {} + public Mat update() { return null; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java index c0565c6b..f822e253 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java @@ -100,6 +100,11 @@ public CameraSource(int webcamIndex, Size size) { isLegacyByIndex = true; } + @Override + public void setSize(Size size) { + this.size = size; + } + @Override public boolean init() { if (initialized) return false; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt index a31c6737..7d15e27d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt @@ -25,8 +25,8 @@ package com.github.serivesmejia.eocvsim.output import com.github.serivesmejia.eocvsim.gui.util.MatPosterImpl import com.github.serivesmejia.eocvsim.util.StrUtil -import io.github.deltacv.vision.util.extension.aspectRatio -import io.github.deltacv.vision.util.extension.clipTo +import io.github.deltacv.vision.external.util.extension.aspectRatio +import io.github.deltacv.vision.external.util.extension.clipTo import com.github.serivesmejia.eocvsim.util.fps.FpsCounter import org.opencv.core.* import org.opencv.imgproc.Imgproc 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 8bf3e652..8ac7efe7 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 @@ -29,7 +29,7 @@ import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot -import com.github.serivesmejia.eocvsim.pipeline.util.PipelineStatisticsCalculator +import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt index 953232ed..788c919a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt @@ -136,12 +136,8 @@ class PipelineExceptionTracker(private val pipelineManager: PipelineManager) { val expiresIn = millisExceptionExpire - (System.currentTimeMillis() - data.millisThrown) val expiresInSecs = String.format("%.1f", expiresIn.toDouble() / 1000.0) - val shortStacktrace = StrUtil.cutStringBy( - data.stacktrace, "\n", cutStacktraceLines - ).trim() - messageBuilder - .appendLine("> $shortStacktrace") + .appendLine("> ${data.stacktrace}") .appendLine() .appendLine("! It has been thrown ${data.count} times, and will expire in $expiresInSecs seconds !") .appendLine() diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index a4ad3425..9ec38168 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -3,20 +3,25 @@ package com.qualcomm.robotcore.eventloop.opmode import com.github.serivesmejia.eocvsim.input.InputSource import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler import com.qualcomm.robotcore.hardware.HardwareMap +import io.github.deltacv.eocvsim.input.VisionInputSourceHander +import io.github.deltacv.vision.external.source.ThreadSourceHander import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline +import org.openftc.easyopencv.OpenCvViewport -class OpModePipelineHandler : SpecificPipelineHandler( +class OpModePipelineHandler(private val viewport: OpenCvViewport) : SpecificPipelineHandler( { it is OpMode } ) { override fun preInit() { + ThreadSourceHander.register(VisionInputSourceHander(viewport)) + pipeline?.telemetry = telemetry - pipeline?.hardwareMap = HardwareMap(null, null) + pipeline?.hardwareMap = HardwareMap() } - override fun init() { - } + override fun init() { } + override fun processFrame(currentInputSource: InputSource?) { } diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt new file mode 100644 index 00000000..38693b0b --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt @@ -0,0 +1,39 @@ +package io.github.deltacv.eocvsim.input + +import com.github.serivesmejia.eocvsim.input.InputSource +import io.github.deltacv.vision.external.source.VisionSource +import io.github.deltacv.vision.external.source.VisionSourceBase +import io.github.deltacv.vision.external.source.VisionSourced +import io.github.deltacv.vision.external.util.Timestamped +import org.opencv.core.Mat +import org.opencv.core.Size + +class VisionInputSource(private val inputSource: InputSource) : VisionSourceBase() { + + override fun init(): Int { + return 0 + } + + override fun close(): Boolean { + inputSource.close() + inputSource.reset() + return true + } + + override fun startSource(size: Size?): Boolean { + inputSource.setSize(size) + inputSource.init() + return true + } + + override fun stopSource(): Boolean { + inputSource.close() + return true; + } + + override fun pullFrame(): Timestamped { + val frame = inputSource.update(); + return Timestamped(frame, inputSource.captureTimeNanos) + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt new file mode 100644 index 00000000..91a75f09 --- /dev/null +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt @@ -0,0 +1,18 @@ +package io.github.deltacv.eocvsim.input + +import com.github.serivesmejia.eocvsim.input.source.CameraSource +import io.github.deltacv.vision.external.source.ViewportAndSourceHander +import io.github.deltacv.vision.external.source.VisionSource +import io.github.deltacv.vision.external.source.VisionSourceHander +import org.opencv.core.Size +import org.openftc.easyopencv.OpenCvViewport + +class VisionInputSourceHander(val viewport: OpenCvViewport) : ViewportAndSourceHander { + + override fun hand(name: String): VisionSource { + return VisionInputSource(CameraSource(0, Size(640.0, 480.0))) + } + + override fun viewport() = viewport + +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java new file mode 100644 index 00000000..ce8a6789 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java @@ -0,0 +1,141 @@ +/* Copyright (c) 2023 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.robotcontroller.external.samples; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +// import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import java.util.List; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +/** + * This 2023-2024 OpMode illustrates the basics of AprilTag recognition and pose estimation, using + * the easy way. + * + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. + */ +// @TeleOp(name = "Concept: AprilTag Easy", group = "Concept") +// @Disabled +public class ConceptAprilTagEasy extends LinearOpMode { + + private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera + + /** + * {@link #aprilTag} is the variable to store our instance of the AprilTag processor. + */ + private AprilTagProcessor aprilTag; + + /** + * {@link #visionPortal} is the variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch Play to start OpMode"); + telemetry.update(); + waitForStart(); + + if (opModeIsActive()) { + while (opModeIsActive()) { + + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Share the CPU. + sleep(20); + } + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end method runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor the easy way. + aprilTag = AprilTagProcessor.easyCreateWithDefaults(); + + // Create the vision portal the easy way. + if (USE_WEBCAM) { + visionPortal = VisionPortal.easyCreateWithDefaults( + hardwareMap.get(WebcamName.class, "Webcam 1"), aprilTag); + } else { + visionPortal = VisionPortal.easyCreateWithDefaults( + BuiltinCameraDirection.BACK, aprilTag); + } + + } // end method initAprilTag() + + /** + * Function to add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + List currentDetections = aprilTag.getDetections(); + telemetry.addData("# AprilTags Detected", currentDetections.size()); + + // Step through the list of detections and display info for each one. + for (AprilTagDetection detection : currentDetections) { + if (detection.metadata != null) { + telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name)); + telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z)); + telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw)); + telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation)); + } else { + telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id)); + telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y)); + } + } // end for() loop + + // Add "key" information to telemetry + telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist."); + telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)"); + telemetry.addLine("RBE = Range, Bearing & Elevation"); + + } // end method telemetryAprilTag() + +} // end class \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java deleted file mode 100644 index bd003c57..00000000 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagTestOpMode.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.firstinspires.ftc.teamcode; - -import com.qualcomm.robotcore.eventloop.opmode.OpMode; - -public class AprilTagTestOpMode extends OpMode { - - @Override - public void init() { - telemetry.addData("Status", "Initialized"); - } - - @Override - public void loop() { - - } - -} diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java index 594d7daf..51388ceb 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java @@ -1,7 +1,15 @@ package com.qualcomm.robotcore.eventloop.opmode; +import io.github.deltacv.vision.external.source.ThreadSourceHander; + public abstract class LinearOpMode extends OpMode { + protected final Object lock = new Object(); + + private LinearOpModeHelperThread helper = new LinearOpModeHelperThread(this); + private RuntimeException catchedException = null; + + public LinearOpMode() { } @@ -110,4 +118,58 @@ public final boolean isStopRequested() { return this.stopRequested || Thread.currentThread().isInterrupted(); } + + //------------------------------------------------------------------------------------------------ + // OpMode inheritance + //------------------------------------------------------------------------------------------------ + + @Override + public final void init() { + ThreadSourceHander.register(helper, ThreadSourceHander.threadHander()); + + helper.start(); + } + + @Override + public final void init_loop() { } + + @Override + public final void loop() { + synchronized (lock) { + if (catchedException != null) { + throw catchedException; + } + } + } + + @Override + public final void stop() { + helper.interrupt(); + } + + private static class LinearOpModeHelperThread extends Thread { + + LinearOpMode opMode; + + public LinearOpModeHelperThread(LinearOpMode opMode) { + super("Thread-LinearOpModeHelper-" + opMode.getClass().getSimpleName()); + + this.opMode = opMode; + } + + @Override + public void run() { + try { + opMode.runOpMode(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (RuntimeException e) { + synchronized (opMode.lock) { + opMode.catchedException = e; + } + } + } + + } + } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java index efbddee1..e1f8165f 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -32,7 +32,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE package com.qualcomm.robotcore.eventloop.opmode; import com.qualcomm.robotcore.hardware.HardwareMap; -import io.github.deltacv.vision.util.FrameQueue; +import io.github.deltacv.vision.external.util.FrameQueue; import org.openftc.easyopencv.TimestampedOpenCvPipeline; import org.firstinspires.ftc.robotcore.external.Telemetry; import org.opencv.core.Mat; diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java index 2af68ca1..5f095ab1 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java @@ -1,15 +1,13 @@ package com.qualcomm.robotcore.hardware; -import io.github.deltacv.vision.source.SourceHander; +import io.github.deltacv.vision.external.source.ThreadSourceHander; +import io.github.deltacv.vision.external.source.ftc.SourcedCameraNameImpl; import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; import org.openftc.easyopencv.OpenCvViewport; public class HardwareMap { - public HardwareMap(SourceHander sourceHander, OpenCvViewport viewport) { - } - - public static boolean hasSuperclass(Class clazz, Class superClass) { + private static boolean hasSuperclass(Class clazz, Class superClass) { try { clazz.asSubclass(superClass); return true; @@ -21,7 +19,7 @@ public static boolean hasSuperclass(Class clazz, Class superClass) { @SuppressWarnings("unchecked") public T get(Class classType, String deviceName) { if(hasSuperclass(classType, CameraName.class)) { - // return (T) new QueueOpenCvCamera(cameraFramesQueue, viewportOutputQueue); + return (T) new SourcedCameraNameImpl(ThreadSourceHander.hand(deviceName)); } return null; diff --git a/Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt b/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt similarity index 95% rename from Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt rename to Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt index c490d7ae..c3a9d035 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/PipelineRenderHook.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt @@ -1,4 +1,4 @@ -package io.github.deltacv.vision +package io.github.deltacv.vision.external import android.graphics.Canvas import org.openftc.easyopencv.OpenCvViewport diff --git a/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java similarity index 61% rename from Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java rename to Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java index aed2acf3..60f40a66 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/SourcedOpenCvCamera.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java @@ -1,20 +1,22 @@ -package io.github.deltacv.vision; +package io.github.deltacv.vision.external; -import android.graphics.Canvas; -import io.github.deltacv.vision.source.Source; -import io.github.deltacv.vision.source.Sourced; +import io.github.deltacv.vision.external.source.VisionSource; +import io.github.deltacv.vision.external.source.VisionSourced; +import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.Size; import org.openftc.easyopencv.*; -public class SourcedOpenCvCamera extends OpenCvCameraBase implements Sourced { +public class SourcedOpenCvCamera extends OpenCvCameraBase implements OpenCvWebcam, VisionSourced { - private final Source source; - OpenCvViewport handedViewport = null; + private final VisionSource source; + OpenCvViewport handedViewport; boolean streaming = false; - public SourcedOpenCvCamera(Source source, OpenCvViewport handedViewport) { + public SourcedOpenCvCamera(VisionSource source, OpenCvViewport handedViewport) { + super(handedViewport); + this.source = source; this.handedViewport = handedViewport; } @@ -54,6 +56,8 @@ public void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener) @Override public void startStreaming(int width, int height) { + setupViewport(); + synchronized (source) { source.start(new Size(width, height)); source.attach(this); @@ -70,11 +74,13 @@ public void startStreaming(int width, int height, OpenCvCameraRotation rotation) @Override public void stopStreaming() { source.stop(); + handedViewport.deactivate(); } @Override protected OpenCvViewport setupViewport() { handedViewport.setRenderHook(PipelineRenderHook.INSTANCE); + handedViewport.activate(); return handedViewport; } @@ -85,8 +91,35 @@ protected OpenCvCameraRotation getDefaultRotation() { } @Override - protected int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation) { - return 0; + protected int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation) + { + /* + * The camera sensor in a webcam is mounted in the logical manner, such + * that the raw image is upright when the webcam is used in its "normal" + * orientation. However, if the user is using it in any other orientation, + * we need to manually rotate the image. + */ + + if(rotation == OpenCvCameraRotation.SENSOR_NATIVE) + { + return -1; + } + else if(rotation == OpenCvCameraRotation.SIDEWAYS_LEFT) + { + return Core.ROTATE_90_COUNTERCLOCKWISE; + } + else if(rotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) + { + return Core.ROTATE_90_CLOCKWISE; + } + else if(rotation == OpenCvCameraRotation.UPSIDE_DOWN) + { + return Core.ROTATE_180; + } + else + { + return -1; + } } @Override diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SkiaPanel.kt similarity index 94% rename from Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt rename to Vision/src/main/java/io/github/deltacv/vision/external/gui/SkiaPanel.kt index da75e32a..16fd8991 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/SkiaPanel.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SkiaPanel.kt @@ -1,4 +1,4 @@ -package io.github.deltacv.vision.gui +package io.github.deltacv.vision.external.gui import org.jetbrains.skiko.ClipComponent import org.jetbrains.skiko.SkiaLayer diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt similarity index 99% rename from Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt rename to Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt index 9111e472..da6a76d4 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt @@ -20,7 +20,7 @@ * SOFTWARE. * */ -package io.github.deltacv.vision.gui +package io.github.deltacv.vision.external.gui import android.graphics.Bitmap import android.graphics.Canvas diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/component/ImageX.java b/Vision/src/main/java/io/github/deltacv/vision/external/gui/component/ImageX.java similarity index 93% rename from Vision/src/main/java/io/github/deltacv/vision/gui/component/ImageX.java rename to Vision/src/main/java/io/github/deltacv/vision/external/gui/component/ImageX.java index 2bf0a290..4fcb214d 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/component/ImageX.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/component/ImageX.java @@ -21,10 +21,10 @@ * */ -package io.github.deltacv.vision.gui.component; +package io.github.deltacv.vision.external.gui.component; -import io.github.deltacv.vision.gui.util.ImgUtil; -import io.github.deltacv.vision.util.CvUtil; +import io.github.deltacv.vision.external.gui.util.ImgUtil; +import io.github.deltacv.vision.external.util.CvUtil; import org.opencv.core.Mat; import javax.swing.*; diff --git a/Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java b/Vision/src/main/java/io/github/deltacv/vision/external/gui/util/ImgUtil.java similarity index 92% rename from Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java rename to Vision/src/main/java/io/github/deltacv/vision/external/gui/util/ImgUtil.java index 49199cbb..d8e3da2b 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/gui/util/ImgUtil.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/util/ImgUtil.java @@ -1,4 +1,4 @@ -package io.github.deltacv.vision.gui.util; +package io.github.deltacv.vision.external.gui.util; import javax.swing.*; import java.awt.*; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java new file mode 100644 index 00000000..9cd22e00 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java @@ -0,0 +1,27 @@ +package io.github.deltacv.vision.external.source; + +import java.util.HashMap; + +public class ThreadSourceHander { + + private static HashMap handlers = new HashMap<>(); + + private ThreadSourceHander() {} // No instantiation + + public static void register(Thread thread, VisionSourceHander handler) { + handlers.put(thread, handler); + } + + public static void register(VisionSourceHander hander) { + register(Thread.currentThread(), hander); + } + + public static VisionSourceHander threadHander() { + return handlers.get(Thread.currentThread()); + } + + public static VisionSource hand(String name) { + return threadHander().hand(name); + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java new file mode 100644 index 00000000..3c85f630 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java @@ -0,0 +1,9 @@ +package io.github.deltacv.vision.external.source; + +import org.openftc.easyopencv.OpenCvViewport; + +public interface ViewportAndSourceHander extends VisionSourceHander { + + OpenCvViewport viewport(); + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java new file mode 100644 index 00000000..af17d9ce --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java @@ -0,0 +1,18 @@ +package io.github.deltacv.vision.external.source; + +import org.opencv.core.Size; + +public interface VisionSource { + + int init(); + + boolean start(Size requestedSize); + + boolean attach(VisionSourced sourced); + boolean remove(VisionSourced sourced); + + boolean stop(); + + boolean close(); + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java new file mode 100644 index 00000000..1bbb7e85 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java @@ -0,0 +1,81 @@ +package io.github.deltacv.vision.external.source; + +import io.github.deltacv.vision.external.util.Timestamped; +import org.opencv.core.Mat; +import org.opencv.core.Size; + +import java.util.ArrayList; + +public abstract class VisionSourceBase implements VisionSource { + + private final Object lock = new Object(); + + ArrayList sourceds = new ArrayList<>(); + + SourceBaseHelperThread helperThread = new SourceBaseHelperThread(this); + + @Override + public final boolean start(Size size) { + boolean result = startSource(size); + + helperThread.start(); + + return result; + } + + public abstract boolean startSource(Size size); + + @Override + public boolean attach(VisionSourced sourced) { + synchronized (lock) { + return sourceds.add(sourced); + } + } + + @Override + public boolean remove(VisionSourced sourced) { + synchronized (lock) { + return sourceds.remove(sourced); + } + } + + @Override + public final boolean stop() { + helperThread.interrupt(); + + return stopSource(); + } + + public abstract boolean stopSource(); + + public abstract Timestamped pullFrame(); + + private static class SourceBaseHelperThread extends Thread { + + VisionSourceBase sourceBase; + + public SourceBaseHelperThread(VisionSourceBase sourcedBase) { + super("Thread-SourceBaseHelper-" + sourcedBase.getClass().getSimpleName()); + + this.sourceBase = sourcedBase; + } + @Override + public void run() { + while (!isInterrupted()) { + Timestamped frame = sourceBase.pullFrame(); + + VisionSourced[] sourceds; + + synchronized (sourceBase.lock) { + sourceds = sourceBase.sourceds.toArray(new VisionSourced[0]); + } + + for(VisionSourced sourced : sourceds) { + sourced.onNewFrame(frame.getValue(), frame.getTimestamp()); + } + } + } + + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java new file mode 100644 index 00000000..35d5166f --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java @@ -0,0 +1,7 @@ +package io.github.deltacv.vision.external.source; + +public interface VisionSourceHander { + + VisionSource hand(String name); + +} \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java new file mode 100644 index 00000000..0377f043 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java @@ -0,0 +1,7 @@ +package io.github.deltacv.vision.external.source; + +import org.opencv.core.Mat; + +public interface VisionSourced { + void onNewFrame(Mat frame, long timestamp); +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java new file mode 100644 index 00000000..a4de3009 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java @@ -0,0 +1,71 @@ +package io.github.deltacv.vision.external.source.ftc; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.qualcomm.robotcore.util.SerialNumber; +import io.github.deltacv.vision.external.source.VisionSource; +import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName; +import org.jetbrains.annotations.NotNull; + +public class SourcedCameraNameImpl extends SourcedCameraName { + + private VisionSource source; + + public SourcedCameraNameImpl(VisionSource source) { + this.source = source; + } + + @Override + public VisionSource getSource() { + return source; + } + + @Override + public Manufacturer getManufacturer() { + return null; + } + + @Override + public String getDeviceName() { + return null; + } + + @Override + public String getConnectionInfo() { + return null; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public void resetDeviceConfigurationForOpMode() { + + } + + @Override + public void close() { + + } + + @NonNull + @NotNull + @Override + public SerialNumber getSerialNumber() { + return null; + } + + @Nullable + @org.jetbrains.annotations.Nullable + @Override + public String getUsbDeviceNameIfAttached() { + return null; + } + + @Override + public boolean isAttached() { + return false; + } +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/util/CvUtil.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/CvUtil.java similarity index 98% rename from Vision/src/main/java/io/github/deltacv/vision/util/CvUtil.java rename to Vision/src/main/java/io/github/deltacv/vision/external/util/CvUtil.java index f1628a5c..b132a8bd 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/util/CvUtil.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/CvUtil.java @@ -21,9 +21,9 @@ * */ -package io.github.deltacv.vision.util; +package io.github.deltacv.vision.external.util; -import io.github.deltacv.vision.util.extension.CvExt; +import io.github.deltacv.vision.external.util.extension.CvExt; import org.opencv.core.Mat; import org.opencv.core.MatOfByte; import org.opencv.core.Size; diff --git a/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java similarity index 95% rename from Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java rename to Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java index 312bd60e..ce909b5d 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/util/FrameQueue.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java @@ -1,4 +1,4 @@ -package io.github.deltacv.vision.util; +package io.github.deltacv.vision.external.util; import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; import org.opencv.core.Mat; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java new file mode 100644 index 00000000..76c5094f --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java @@ -0,0 +1,21 @@ +package io.github.deltacv.vision.external.util; + +public class Timestamped { + + private final T value; + private final long timestamp; + + public Timestamped(T value, long timestamp) { + this.value = value; + this.timestamp = timestamp; + } + + public T getValue() { + return value; + } + + public long getTimestamp() { + return timestamp; + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/util/extension/CvExt.kt b/Vision/src/main/java/io/github/deltacv/vision/external/util/extension/CvExt.kt similarity index 96% rename from Vision/src/main/java/io/github/deltacv/vision/util/extension/CvExt.kt rename to Vision/src/main/java/io/github/deltacv/vision/external/util/extension/CvExt.kt index 91c59ee3..4e11ae9b 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/util/extension/CvExt.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/extension/CvExt.kt @@ -22,7 +22,7 @@ */ @file:JvmName("CvExt") -package io.github.deltacv.vision.util.extension +package io.github.deltacv.vision.external.util.extension import com.qualcomm.robotcore.util.Range import org.opencv.core.CvType diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java new file mode 100644 index 00000000..7233bcf1 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java @@ -0,0 +1,37 @@ +package io.github.deltacv.vision.internal.source.ftc; + +import io.github.deltacv.vision.external.source.VisionSource; +import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCharacteristics; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; + +public abstract class SourcedCameraName implements WebcamName { + + public abstract VisionSource getSource(); + + @Override + public boolean isWebcam() { + return false; + } + + @Override + public boolean isCameraDirection() { + return false; + } + + @Override + public boolean isSwitchable() { + return false; + } + + @Override + public boolean isUnknown() { + return false; + } + + @Override + public CameraCharacteristics getCameraCharacteristics() { + return null; + } + +} diff --git a/Vision/src/main/java/io/github/deltacv/vision/source/Source.java b/Vision/src/main/java/io/github/deltacv/vision/source/Source.java deleted file mode 100644 index ca81a70a..00000000 --- a/Vision/src/main/java/io/github/deltacv/vision/source/Source.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.deltacv.vision.source; - -import org.opencv.core.Size; - -public interface Source { - - int init(); - - boolean start(Size requestedSize); - - boolean attach(Sourced sourced); - boolean remove(Sourced sourced); - - boolean stop(); - - boolean close(); - -} diff --git a/Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java deleted file mode 100644 index e73ed6ba..00000000 --- a/Vision/src/main/java/io/github/deltacv/vision/source/SourceHander.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.deltacv.vision.source; - -public interface SourceHander { - - Source hand(String name); - -} diff --git a/Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java b/Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java deleted file mode 100644 index a837565a..00000000 --- a/Vision/src/main/java/io/github/deltacv/vision/source/Sourced.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.deltacv.vision.source; - -import org.opencv.core.Mat; - -public interface Sourced { - void onNewFrame(Mat frame, long timestamp); -} diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java new file mode 100644 index 00000000..2e2eb352 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java @@ -0,0 +1,7 @@ +package org.firstinspires.ftc.robotcore.external.hardware.camera; + +public enum BuiltinCameraDirection +{ + BACK, + FRONT +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java new file mode 100644 index 00000000..35ec8635 --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java @@ -0,0 +1,46 @@ +/* +Copyright (c) 2017 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.external.hardware.camera; + +import androidx.annotation.Nullable; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; + +public interface CameraControls +{ + //---------------------------------------------------------------------------------------------- + // Controls + //---------------------------------------------------------------------------------------------- + + @Nullable T getControl(Class controlType); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java index bcd73251..f8de2587 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java @@ -1,4 +1,63 @@ +/* +Copyright (c) 2018 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.external.hardware.camera; -public class WebcamName { -} +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.qualcomm.robotcore.hardware.HardwareDevice; +import com.qualcomm.robotcore.util.SerialNumber; + +public interface WebcamName extends CameraName, HardwareDevice +{ + /** + * Returns the USB serial number of the webcam + * @return the USB serial number of the webcam + */ + @NonNull SerialNumber getSerialNumber(); + + /** + * Returns the USB device path currently associated with this webcam. + * May be null if the webcam is not presently attached. + * + * @return returns the USB device path associated with this name. + * see UsbManager#getDeviceList() + */ + @Nullable String getUsbDeviceNameIfAttached(); + + /** + * Returns whether this camera currently attached to the robot controller + * @return whether this camera currently attached to the robot controller + */ + boolean isAttached(); +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java new file mode 100644 index 00000000..499d683f --- /dev/null +++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java @@ -0,0 +1,42 @@ +/* +Copyright (c) 2018 Robert Atkinson + +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 Robert Atkinson nor the names of his 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.robotcore.external.hardware.camera.controls; + +/** + * A {@link CameraControl} can be thought of as a knob or setting or dial on a {link Camera} + * that can be adjusted by the user + */ +@SuppressWarnings("WeakerAccess") +public interface CameraControl +{ +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java similarity index 97% rename from Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa rename to Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java index 8c8f17b2..79973bbe 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.javaa +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java @@ -38,7 +38,8 @@ import java.util.ArrayList; import java.util.List; -import org.firstinspires.ftc.robotcore.external.ClassFactory; +import io.github.deltacv.vision.external.source.ThreadSourceHander; +import io.github.deltacv.vision.external.source.ftc.SourcedCameraNameImpl; import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; @@ -109,9 +110,7 @@ public enum MultiPortalLayout */ public static int[] makeMultiPortalView(int numPortals, MultiPortalLayout mpl) { - return OpenCvCameraFactory.getInstance().splitLayoutForMultipleViewports( - DEFAULT_VIEW_CONTAINER_ID, numPortals, mpl.viewportSplitMethod - ); + throw new UnsupportedOperationException("Split views not supported yet"); } /** @@ -174,7 +173,7 @@ public Builder setCamera(CameraName camera) */ public Builder setCamera(BuiltinCameraDirection cameraDirection) { - this.camera = ClassFactory.getInstance().getCameraManager().nameFromCameraDirection(cameraDirection); + this.camera = new SourcedCameraNameImpl(ThreadSourceHander.hand("default")); return this; } @@ -199,7 +198,8 @@ public Builder enableCameraMonitoring(boolean enableLiveView) { // dont care + l + ratio - return setCameraMonitorViewId(viewId); + // return setCameraMonitorViewId(viewId); + return this; } /** diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java similarity index 73% rename from Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa rename to Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java index f4b55fae..5f6dbcd6 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.javaa +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java @@ -38,21 +38,15 @@ import com.qualcomm.robotcore.util.RobotLog; -import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraName; +import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName; import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl; import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationHelper; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibrationIdentity; -import org.firstinspires.ftc.robotcore.internal.camera.delegating.SwitchableCameraName; import org.opencv.core.Mat; import org.openftc.easyopencv.OpenCvCamera; import org.openftc.easyopencv.OpenCvCameraFactory; import org.openftc.easyopencv.OpenCvCameraRotation; -import org.openftc.easyopencv.OpenCvInternalCamera; -import org.openftc.easyopencv.OpenCvSwitchableWebcam; import org.openftc.easyopencv.OpenCvWebcam; import org.openftc.easyopencv.TimestampedOpenCvPipeline; @@ -117,24 +111,7 @@ public void onOpened() cameraState = CameraState.CAMERA_DEVICE_READY; cameraState = CameraState.STARTING_STREAM; - if (camera instanceof OpenCvWebcam) - { - ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat); - } - else - { - camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); - } - - if (camera instanceof OpenCvWebcam) - { - CameraCalibrationIdentity identity = ((OpenCvWebcam) camera).getCalibrationIdentity(); - - if (identity != null) - { - calibration = CameraCalibrationHelper.getInstance().getCalibration(identity, cameraResolution.getWidth(), cameraResolution.getHeight()); - } - } + camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION); camera.setPipeline(new ProcessingPipeline()); cameraState = CameraState.STREAMING; @@ -155,54 +132,9 @@ protected void createCamera(CameraName cameraName, int cameraMonitorViewId) { throw new IllegalArgumentException("parameters.camera == null"); } - else if (cameraName.isWebcam()) // Webcams - { - if (cameraMonitorViewId != 0) - { - camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); - } - else - { - camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName); - } - } - else if (cameraName.isCameraDirection()) // Internal cameras + else if (cameraName instanceof SourcedCameraName) // Webcams { - if (cameraMonitorViewId != 0) - { - camera = OpenCvCameraFactory.getInstance().createInternalCamera( - ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT, cameraMonitorViewId); - } - else - { - camera = OpenCvCameraFactory.getInstance().createInternalCamera( - ((BuiltinCameraName) cameraName).getCameraDirection() == BuiltinCameraDirection.BACK ? OpenCvInternalCamera.CameraDirection.BACK : OpenCvInternalCamera.CameraDirection.FRONT); - } - } - else if (cameraName.isSwitchable()) - { - SwitchableCameraName switchableCameraName = (SwitchableCameraName) cameraName; - if (switchableCameraName.allMembersAreWebcams()) { - CameraName[] members = switchableCameraName.getMembers(); - WebcamName[] webcamNames = new WebcamName[members.length]; - for (int i = 0; i < members.length; i++) - { - webcamNames[i] = (WebcamName) members[i]; - } - - if (cameraMonitorViewId != 0) - { - camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(cameraMonitorViewId, webcamNames); - } - else - { - camera = OpenCvCameraFactory.getInstance().createSwitchableWebcam(webcamNames); - } - } - else - { - throw new IllegalArgumentException("All members of a switchable camera name must be webcam names"); - } + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName); } else // ¯\_(ツ)_/¯ { @@ -273,27 +205,13 @@ public CameraState getCameraState() @Override public void setActiveCamera(WebcamName webcamName) { - if (camera instanceof OpenCvSwitchableWebcam) - { - ((OpenCvSwitchableWebcam) camera).setActiveCamera(webcamName); - } - else - { - throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); - } + throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams"); } @Override public WebcamName getActiveCamera() { - if (camera instanceof OpenCvSwitchableWebcam) - { - return ((OpenCvSwitchableWebcam) camera).getActiveCamera(); - } - else - { - throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); - } + throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams"); } @Override @@ -301,14 +219,7 @@ public T getCameraControl(Class controlType) { if (cameraState == CameraState.STREAMING) { - if (camera instanceof OpenCvWebcam) - { - return ((OpenCvWebcam) camera).getControl(controlType); - } - else - { - throw new UnsupportedOperationException("Getting controls is only supported for webcams"); - } + throw new UnsupportedOperationException("Getting controls is not yet supported in EOCV-Sim"); } else { @@ -334,7 +245,7 @@ public Mat processFrame(Mat input, long captureTimeNanos) { if (captureNextFrame != null) { - saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); + // saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png"); } captureNextFrame = null; diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java index 7e8634fc..feb7a13f 100644 --- a/Vision/src/main/java/org/opencv/android/Utils.java +++ b/Vision/src/main/java/org/opencv/android/Utils.java @@ -82,7 +82,7 @@ private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { if(src.type() == CvType.CV_8UC1) { - Imgproc.cvtColor(src, tmp, Imgproc.COLOR_GRAY2RGBA); + Imgproc.cvtColor(src, tmp, Imgproc.COLOR_GRAY2BGRA); } else if(src.type() == CvType.CV_8UC3){ Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGB2BGRA); } else if(src.type() == CvType.CV_8UC4){ diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java index b3df5e11..3e8cffac 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java @@ -25,6 +25,7 @@ import android.graphics.Canvas; import com.qualcomm.robotcore.util.ElapsedTime; import com.qualcomm.robotcore.util.MovingStatistics; +import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator; import org.opencv.android.Utils; import org.opencv.core.*; import org.opencv.imgproc.Imgproc; @@ -33,10 +34,7 @@ public abstract class OpenCvCameraBase implements OpenCvCamera { private OpenCvPipeline pipeline = null; - private MovingStatistics msFrameIntervalRollingAverage; - private MovingStatistics msUserPipelineRollingAverage; - private MovingStatistics msTotalFrameProcessingTimeRollingAverage; - private ElapsedTime timer; + private OpenCvViewport viewport; private OpenCvCameraRotation rotation; @@ -54,11 +52,15 @@ public abstract class OpenCvCameraBase implements OpenCvCamera { private Scalar brown = new Scalar(82, 61, 46, 255); private int frameCount = 0; - private float avgFps; - private int avgPipelineTime; - private int avgOverheadTime; - private int avgTotalFrameTime; - private long currentFrameStartTime; + + private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator(); + + public OpenCvCameraBase(OpenCvViewport viewport) { + this.viewport = viewport; + this.rotation = getDefaultRotation(); + + statistics.init(); + } @Override public void showFpsMeterOnViewport(boolean show) { @@ -122,19 +124,15 @@ public int getCurrentPipelineMaxFps() { @Override public void startRecordingPipeline(PipelineRecordingParameters parameters) { - } @Override public void stopRecordingPipeline() { - } protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { - msFrameIntervalRollingAverage.add(timer.milliseconds()); - timer.reset(); - double secondsPerFrame = msFrameIntervalRollingAverage.getMean() / 1000d; - avgFps = (float) (1d / secondsPerFrame); + statistics.newPipelineFrameStart(); + Mat userProcessedFrame = null; int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation); @@ -175,9 +173,11 @@ protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp); } - long pipelineStart = System.currentTimeMillis(); + statistics.beforeProcessFrame(); + userProcessedFrame = pipelineSafe.processFrameInternal(frame); - msUserPipelineRollingAverage.add(System.currentTimeMillis() - pipelineStart); + + statistics.afterProcessFrame(); } // Will point to whatever mat we end up deciding to send to the screen @@ -251,17 +251,13 @@ protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null)); } - avgPipelineTime = (int) Math.round(msUserPipelineRollingAverage.getMean()); - avgTotalFrameTime = (int) Math.round(msTotalFrameProcessingTimeRollingAverage.getMean()); - avgOverheadTime = avgTotalFrameTime - avgPipelineTime; + statistics.endFrame(); if (viewport != null) { - viewport.notifyStatistics(avgFps, avgPipelineTime, avgOverheadTime); + viewport.notifyStatistics(statistics.getAvgFps(), statistics.getAvgPipelineTime(), statistics.getAvgOverheadTime()); } frameCount++; - - msTotalFrameProcessingTimeRollingAverage.add(System.currentTimeMillis() - currentFrameStartTime); } diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java new file mode 100644 index 00000000..f417bd0c --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java @@ -0,0 +1,38 @@ +package org.openftc.easyopencv; + +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; + +public abstract class OpenCvCameraFactory { + + private static OpenCvCameraFactory instance = new SourcedOpenCvCameraFactoryImpl(); + + public static OpenCvCameraFactory getInstance() { + return instance; + } + + + /* + * Internal + */ + public abstract OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction); + public abstract OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction, int viewportContainerId); + + /* + * Internal2 + */ + public abstract OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction); + public abstract OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction, int viewportContainerId); + + /* + * Webcam + */ + public abstract OpenCvWebcam createWebcam(WebcamName cameraName); + public abstract OpenCvWebcam createWebcam(WebcamName cameraName, int viewportContainerId); + + public enum ViewportSplitMethod + { + VERTICALLY, + HORIZONTALLY + } + +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java new file mode 100644 index 00000000..17f698fe --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import android.hardware.Camera; + +public interface OpenCvInternalCamera extends OpenCvCamera +{ + enum CameraDirection + { + FRONT(Camera.CameraInfo.CAMERA_FACING_FRONT), + BACK(Camera.CameraInfo.CAMERA_FACING_BACK); + + public int id; + + CameraDirection(int id) + { + this.id = id; + } + } +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java new file mode 100644 index 00000000..0650622a --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +import android.hardware.Camera; + +public interface OpenCvInternalCamera2 extends OpenCvCamera +{ + enum CameraDirection + { + FRONT(Camera.CameraInfo.CAMERA_FACING_FRONT), + BACK(Camera.CameraInfo.CAMERA_FACING_BACK); + + public int id; + + CameraDirection(int id) + { + this.id = id; + } + } +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java new file mode 100644 index 00000000..924116cc --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 OpenFTC Team + * + * 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 org.openftc.easyopencv; + +public interface OpenCvWebcam extends OpenCvCamera { + + default void startStreaming(int width, int height, OpenCvCameraRotation cameraRotation, StreamFormat eocvStreamFormat) { + startStreaming(width, height, cameraRotation); + } + + enum StreamFormat + { + // The only format that was supported historically; it is uncompressed but + // chroma subsampled and uses lots of bandwidth - this limits frame rate + // at higher resolutions and also limits the ability to use two cameras + // on the same bus to lower resolutions + YUY2, + + // Compressed motion JPEG stream format; allows for higher resolutions at + // full frame rate, and better ability to use two cameras on the same bus. + // Requires extra CPU time to run decompression routine. + MJPEG; + } + +} diff --git a/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java new file mode 100644 index 00000000..f6c5671e --- /dev/null +++ b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java @@ -0,0 +1,63 @@ +package org.openftc.easyopencv; + +import io.github.deltacv.vision.external.SourcedOpenCvCamera; +import io.github.deltacv.vision.external.source.VisionSource; +import io.github.deltacv.vision.external.source.ThreadSourceHander; +import io.github.deltacv.vision.external.source.ViewportAndSourceHander; +import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; + +public class SourcedOpenCvCameraFactoryImpl extends OpenCvCameraFactory { + + + private OpenCvViewport viewport() { + if(ThreadSourceHander.threadHander() instanceof ViewportAndSourceHander) { + return ((ViewportAndSourceHander) ThreadSourceHander.threadHander()).viewport(); + } + + return null; + } + + private VisionSource source(String name) { + if(ThreadSourceHander.threadHander() instanceof ViewportAndSourceHander) { + return ThreadSourceHander.threadHander().hand(name); + } + + return null; + } + + @Override + public OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction) { + return new SourcedOpenCvCamera(source("default"), viewport()); + } + + @Override + public OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction, int viewportContainerId) { + return createInternalCamera(direction); + } + + @Override + public OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction) { + return new SourcedOpenCvCamera(source("default"), viewport()); + } + + @Override + public OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction, int viewportContainerId) { + return createInternalCamera2(direction); + } + + @Override + public OpenCvWebcam createWebcam(WebcamName cameraName) { + if(cameraName instanceof SourcedCameraName) { + return new SourcedOpenCvCamera(((SourcedCameraName) cameraName).getSource(), viewport()); + } else { + throw new IllegalArgumentException("cameraName is not compatible with SourcedOpenCvCamera"); + } + } + + @Override + public OpenCvWebcam createWebcam(WebcamName cameraName, int viewportContainerId) { + return createWebcam(cameraName); + } + +} From 17a68c7910c0b24b7879da0766d32761f19f073f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 12 Aug 2023 15:04:42 -0600 Subject: [PATCH 18/46] Better handling of OpMode stopping with cameras --- .../collections/EvictingBlockingQueue.java | 11 +++ .../github/serivesmejia/eocvsim/EOCVSim.kt | 3 +- .../eocvsim/input/InputSourceManager.java | 4 + .../eocvsim/input/source/ImageSource.java | 5 + .../eocvsim/input/source/VideoSource.java | 5 + .../eventloop/opmode/OpModePipelineHandler.kt | 12 ++- .../eocvsim/input/VisionInputSource.kt | 18 +++- .../eocvsim/input/VisionInputSourceHander.kt | 39 +++++++- .../eventloop/opmode/LinearOpMode.java | 19 +++- .../robotcore/eventloop/opmode/OpMode.java | 4 +- .../vision/external/SourcedOpenCvCamera.java | 49 ++++++---- .../external/gui/SwingOpenCvViewport.kt | 92 +++++++++++-------- .../external/source/VisionSourceBase.java | 23 ++++- .../vision/external/source/VisionSourced.java | 6 ++ .../ftc/vision/VisionPortalImpl.java | 2 +- .../openftc/easyopencv/OpenCvCameraBase.java | 87 ++++++++++++++++-- .../openftc/easyopencv/OpenCvViewport.java | 4 + .../SourcedOpenCvCameraFactoryImpl.java | 29 ++++-- 18 files changed, 322 insertions(+), 90 deletions(-) diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java index e64f66a6..895a2004 100644 --- a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java +++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java @@ -201,4 +201,15 @@ public int drainTo(Collection c, int maxElements) { return targetQueue.drainTo(c, maxElements); } } + + @Override + public void clear() { + synchronized (theLock) { + for(E e : targetQueue) + if (evictAction != null) + evictAction.accept(e); + + targetQueue.clear(); + } + } } \ No newline at end of file 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 79c487d9..5c405ec7 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 @@ -215,7 +215,7 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.waitForFinishingInit() - pipelineManager.subscribePipelineHandler(OpModePipelineHandler(visualizer.viewport)) + pipelineManager.subscribePipelineHandler(OpModePipelineHandler(inputSourceManager, visualizer.viewport)) visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0 @@ -240,6 +240,7 @@ class EOCVSim(val params: Parameters = Parameters()) { } else { // opmodes are on their own, lol visualizer.viewport.deactivate() + visualizer.viewport.clearViewport() } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index f3c56159..bb8a0aaa 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -302,6 +302,10 @@ public Visualizer.AsyncPleaseWaitDialog showApwdIfNeeded(String sourceName) { return apwd; } + public String getDefaultInputSource() { + return defaultSource; + } + public SourceType getSourceType(String sourceName) { if(sourceName == null) { return SourceType.UNKNOWN; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java index 39187cd6..efc985e1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java @@ -159,6 +159,11 @@ protected InputSource internalCloneSource() { return new ImageSource(imgPath, size); } + @Override + public void setSize(Size size) { + this.size = size; + } + @Override public FileFilter getFileFilters() { return FileFilters.imagesFilter; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index b8a883e9..b310f1be 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -209,6 +209,11 @@ protected InputSource internalCloneSource() { return new VideoSource(videoPath, size); } + @Override + public void setSize(Size size) { + this.size = size; + } + @Override public FileFilter getFileFilters() { return FileFilters.videoMediaFilter; diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index 9ec38168..9209c8d3 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -1,7 +1,10 @@ package com.qualcomm.robotcore.eventloop.opmode +import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.input.InputSource +import com.github.serivesmejia.eocvsim.input.InputSourceManager import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler +import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.qualcomm.robotcore.hardware.HardwareMap import io.github.deltacv.eocvsim.input.VisionInputSourceHander import io.github.deltacv.vision.external.source.ThreadSourceHander @@ -9,12 +12,16 @@ import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.OpenCvViewport -class OpModePipelineHandler(private val viewport: OpenCvViewport) : SpecificPipelineHandler( +class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private val viewport: OpenCvViewport) : SpecificPipelineHandler( { it is OpMode } ) { + private val onStop = EventHandler("OpModePipelineHandler-onStop") + override fun preInit() { - ThreadSourceHander.register(VisionInputSourceHander(viewport)) + inputSourceManager.setInputSource(inputSourceManager.defaultInputSource) + + ThreadSourceHander.register(VisionInputSourceHander(onStop, viewport)) pipeline?.telemetry = telemetry pipeline?.hardwareMap = HardwareMap() @@ -28,6 +35,7 @@ class OpModePipelineHandler(private val viewport: OpenCvViewport) : SpecificPipe override fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) { this.pipeline?.requestOpModeStop() + onStop.run() super.onChange(pipeline, telemetry) } diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt index 38693b0b..c0ac643d 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt @@ -1,6 +1,7 @@ package io.github.deltacv.eocvsim.input import com.github.serivesmejia.eocvsim.input.InputSource +import com.github.serivesmejia.eocvsim.util.loggerForThis import io.github.deltacv.vision.external.source.VisionSource import io.github.deltacv.vision.external.source.VisionSourceBase import io.github.deltacv.vision.external.source.VisionSourced @@ -8,7 +9,11 @@ import io.github.deltacv.vision.external.util.Timestamped import org.opencv.core.Mat import org.opencv.core.Size -class VisionInputSource(private val inputSource: InputSource) : VisionSourceBase() { +class VisionInputSource( + private val inputSource: InputSource +) : VisionSourceBase() { + + val logger by loggerForThis() override fun init(): Int { return 0 @@ -31,9 +36,16 @@ class VisionInputSource(private val inputSource: InputSource) : VisionSourceBase return true; } + private val emptyMat = Mat(); + override fun pullFrame(): Timestamped { - val frame = inputSource.update(); - return Timestamped(frame, inputSource.captureTimeNanos) + try { + val frame = inputSource.update(); + return Timestamped(frame, inputSource.captureTimeNanos) + } catch(e: Exception) { + logger.warn("Exception thrown while pulling frame from input source", e) + return Timestamped(emptyMat, 0) + } } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt index 91a75f09..ec6fb78d 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt @@ -1,16 +1,51 @@ package io.github.deltacv.eocvsim.input import com.github.serivesmejia.eocvsim.input.source.CameraSource +import com.github.serivesmejia.eocvsim.input.source.ImageSource +import com.github.serivesmejia.eocvsim.input.source.VideoSource +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.qualcomm.robotcore.eventloop.opmode.OpMode import io.github.deltacv.vision.external.source.ViewportAndSourceHander import io.github.deltacv.vision.external.source.VisionSource import io.github.deltacv.vision.external.source.VisionSourceHander import org.opencv.core.Size import org.openftc.easyopencv.OpenCvViewport +import java.io.File +import java.lang.IllegalArgumentException +import java.net.URLConnection -class VisionInputSourceHander(val viewport: OpenCvViewport) : ViewportAndSourceHander { +class VisionInputSourceHander(val stopNotifier: EventHandler, val viewport: OpenCvViewport) : ViewportAndSourceHander { + + private fun isImage(path: String): Boolean { + val mimeType: String = URLConnection.getFileNameMap().getContentTypeFor(path) + return mimeType.startsWith("image") + } + + private fun isVideo(path: String): Boolean { + val mimeType: String = URLConnection.getFileNameMap().getContentTypeFor(path) + return mimeType.startsWith("video") + } override fun hand(name: String): VisionSource { - return VisionInputSource(CameraSource(0, Size(640.0, 480.0))) + val source = VisionInputSource(if(File(name).exists()) { + if(isImage(name)) { + ImageSource(name) + } else if(isVideo(name)) { + VideoSource(name, null) + } else throw IllegalArgumentException("File is not an image nor a video") + } else { + val index = name.toIntOrNull() + ?: if(name == "default" || name == "Webcam 1") 0 + else throw IllegalArgumentException("Unknown source $name") + + CameraSource(index, Size(640.0, 480.0)) + }) + + stopNotifier.doOnce { + source.stop() + } + + return source } override fun viewport() = viewport diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java index 51388ceb..c89a49c8 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java @@ -3,13 +3,10 @@ import io.github.deltacv.vision.external.source.ThreadSourceHander; public abstract class LinearOpMode extends OpMode { - protected final Object lock = new Object(); - private LinearOpModeHelperThread helper = new LinearOpModeHelperThread(this); private RuntimeException catchedException = null; - public LinearOpMode() { } @@ -125,6 +122,9 @@ public final boolean isStopRequested() { @Override public final void init() { + isStarted = false; + stopRequested = false; + ThreadSourceHander.register(helper, ThreadSourceHander.threadHander()); helper.start(); @@ -133,6 +133,12 @@ public final void init() { @Override public final void init_loop() { } + @Override + public final void start() { + stopRequested = false; + isStarted = true; + } + @Override public final void loop() { synchronized (lock) { @@ -144,6 +150,13 @@ public final void loop() { @Override public final void stop() { + /* + * Get out of dodge. Been here, done this. + */ + if(stopRequested) { return; } + + stopRequested = true; + helper.interrupt(); } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java index e1f8165f..990103c3 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -97,7 +97,7 @@ public OpMode() { *

* The stop method is optional. By default this method takes no action. */ - public void stop() {}; + public void stop() {}; // normally called by OpModePipelineHandler public void requestOpModeStop() { stop(); @@ -124,7 +124,7 @@ public final Mat processFrame(Mat input, long captureTimeNanos) { loop(); telemetry.update(); - return null; // OpModes don't actually show anything to the viewport, we'll delegate that + return null; // OpModes don't actually show anything to the viewport, we'll delegate that to OpenCvCamera-s } @Override diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java index 60f40a66..995b0947 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java @@ -14,8 +14,8 @@ public class SourcedOpenCvCamera extends OpenCvCameraBase implements OpenCvWebca boolean streaming = false; - public SourcedOpenCvCamera(VisionSource source, OpenCvViewport handedViewport) { - super(handedViewport); + public SourcedOpenCvCamera(VisionSource source, OpenCvViewport handedViewport, boolean viewportEnabled) { + super(handedViewport, viewportEnabled); this.source = source; this.handedViewport = handedViewport; @@ -23,6 +23,8 @@ public SourcedOpenCvCamera(VisionSource source, OpenCvViewport handedViewport) { @Override public int openCameraDevice() { + prepareForOpenCameraDevice(); + return source.init(); } @@ -56,7 +58,12 @@ public void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener) @Override public void startStreaming(int width, int height) { - setupViewport(); + startStreaming(width, height, getDefaultRotation()); + } + + @Override + public void startStreaming(int width, int height, OpenCvCameraRotation rotation) { + prepareForStartStreaming(width, height, rotation); synchronized (source) { source.start(new Size(width, height)); @@ -66,23 +73,10 @@ public void startStreaming(int width, int height) { streaming = true; } - @Override - public void startStreaming(int width, int height, OpenCvCameraRotation rotation) { - startStreaming(width, height); - } - @Override public void stopStreaming() { source.stop(); - handedViewport.deactivate(); - } - - @Override - protected OpenCvViewport setupViewport() { - handedViewport.setRenderHook(PipelineRenderHook.INSTANCE); - handedViewport.activate(); - - return handedViewport; + cleanupForEndStreaming(); } @Override @@ -129,11 +123,30 @@ protected boolean cameraOrientationIsTiedToDeviceOrientation() { @Override protected boolean isStreaming() { - return false; + return streaming; } + @Override + public void onFrameStart() { + if(!isStreaming()) return; + + notifyStartOfFrameProcessing(); + } + + // + // Inheritance from Sourced + // + @Override public void onNewFrame(Mat frame, long timestamp) { + if(!isStreaming()) return; + if(frame.empty() || frame == null) return; + handleFrameUserCrashable(frame, timestamp); } + + @Override + public void stop() { + closeCameraDevice(); + } } \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt index da6a76d4..40483edc 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt @@ -125,7 +125,10 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi visionPreviewFrameQueue.clear() framebufferRecycler = MatRecycler(FRAMEBUFFER_RECYCLER_CAPACITY) - skiaLayer.setSize(width, height) + SwingUtilities.invokeLater { + skiaLayer.setSize(width, height) + skiaLayer.repaint() + } surfaceExistsAndIsReady = true checkState() @@ -278,6 +281,7 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi } } + private val canvasLock = Any() private lateinit var lastFrame: MatRecycler.RecyclableMat private fun renderCanvas(canvas: Canvas) { @@ -285,68 +289,78 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi lastFrame = framebufferRecycler!!.takeMat() } - when (internalRenderingState) { - RenderingState.ACTIVE -> { - shouldPaintOrange = true + synchronized(canvasLock) { + when (internalRenderingState) { + RenderingState.ACTIVE -> { + shouldPaintOrange = true - val mat: MatRecycler.RecyclableMat = try { - //Grab a Mat from the frame queue - val frame = visionPreviewFrameQueue.poll(10, TimeUnit.MILLISECONDS) ?: lastFrame + val mat: MatRecycler.RecyclableMat = try { + //Grab a Mat from the frame queue + val frame = visionPreviewFrameQueue.poll(10, TimeUnit.MILLISECONDS) ?: lastFrame - frame - } catch (e: InterruptedException) { + frame + } catch (e: InterruptedException) { - //Note: we actually don't re-interrupt ourselves here, because interrupts are also - //used to simply make sure we properly pick up a transition to the PAUSED state, not - //just when we're trying to close. If we're trying to close, then exitRequested will - //be set, and since we break immediately right here, the close will be handled cleanly. - //Thread.currentThread().interrupt(); - return - } + //Note: we actually don't re-interrupt ourselves here, because interrupts are also + //used to simply make sure we properly pick up a transition to the PAUSED state, not + //just when we're trying to close. If we're trying to close, then exitRequested will + //be set, and since we break immediately right here, the close will be handled cleanly. + //Thread.currentThread().interrupt(); + return + } - mat.copyTo(lastFrame) + mat.copyTo(lastFrame) - if(mat.empty()) { - return // nope out - } + if (mat.empty()) { + return // nope out + } - /* + /* * For some reason, the canvas will very occasionally be null upon closing. * Stack Overflow seems to suggest this means the canvas has been destroyed. * However, surfaceDestroyed(), which is called right before the surface is * destroyed, calls checkState(), which *SHOULD* block until we die. This * works most of the time, but not always? We don't yet understand... */ - if (canvas != null) { - renderer.render(mat, canvas, renderHook, mat.context) - } else { - logger.info("Canvas was null") - } + if (canvas != null) { + renderer.render(mat, canvas, renderHook, mat.context) + } else { + logger.info("Canvas was null") + } - //We're done with that Mat object; return it to the Mat recycler so it can be used again later - if(mat != lastFrame) { - framebufferRecycler!!.returnMat(mat) + //We're done with that Mat object; return it to the Mat recycler so it can be used again later + if (mat !== lastFrame) { + framebufferRecycler!!.returnMat(mat) + } } - } - RenderingState.PAUSED -> { - if (shouldPaintOrange) { - shouldPaintOrange = false + RenderingState.PAUSED -> { + if (shouldPaintOrange) { + shouldPaintOrange = false - /* + /* * For some reason, the canvas will very occasionally be null upon closing. * Stack Overflow seems to suggest this means the canvas has been destroyed. * However, surfaceDestroyed(), which is called right before the surface is * destroyed, calls checkState(), which *SHOULD* block until we die. This * works most of the time, but not always? We don't yet understand... */ - if (canvas != null) { - renderer.renderPaused(canvas) + if (canvas != null) { + renderer.renderPaused(canvas) + } } } + + else -> {} } + } + } + + fun clearViewport() { + visionPreviewFrameQueue.clear() - else -> {} + synchronized(canvasLock) { + lastFrame.release() } } @@ -357,6 +371,6 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi companion object { private const val VISION_PREVIEW_FRAME_QUEUE_CAPACITY = 2 - private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 3 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1). + private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 4 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1). } -} +} \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java index 1bbb7e85..298d23c6 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java @@ -43,6 +43,12 @@ public boolean remove(VisionSourced sourced) { public final boolean stop() { helperThread.interrupt(); + for(VisionSourced sourced : sourceds) { + synchronized (sourced) { + sourced.stop(); + } + } + return stopSource(); } @@ -50,6 +56,16 @@ public final boolean stop() { public abstract Timestamped pullFrame(); + private Timestamped pullFrameInternal() { + for(VisionSourced sourced : sourceds) { + synchronized (sourced) { + sourced.onFrameStart(); + } + } + + return pullFrame(); + } + private static class SourceBaseHelperThread extends Thread { VisionSourceBase sourceBase; @@ -62,7 +78,7 @@ public SourceBaseHelperThread(VisionSourceBase sourcedBase) { @Override public void run() { while (!isInterrupted()) { - Timestamped frame = sourceBase.pullFrame(); + Timestamped frame = sourceBase.pullFrameInternal(); VisionSourced[] sourceds; @@ -71,11 +87,12 @@ public void run() { } for(VisionSourced sourced : sourceds) { - sourced.onNewFrame(frame.getValue(), frame.getTimestamp()); + synchronized (sourced) { + sourced.onNewFrame(frame.getValue(), frame.getTimestamp()); + } } } } - } } diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java index 0377f043..8f3bb489 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java @@ -3,5 +3,11 @@ import org.opencv.core.Mat; public interface VisionSourced { + + default void onFrameStart() {} + + void stop(); + void onNewFrame(Mat frame, long timestamp); + } diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java index 5f6dbcd6..a0455ccd 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java @@ -134,7 +134,7 @@ protected void createCamera(CameraName cameraName, int cameraMonitorViewId) } else if (cameraName instanceof SourcedCameraName) // Webcams { - camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName); + camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId); } else // ¯\_(ツ)_/¯ { diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java index 3e8cffac..8c0ce412 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java @@ -26,6 +26,7 @@ import com.qualcomm.robotcore.util.ElapsedTime; import com.qualcomm.robotcore.util.MovingStatistics; import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator; +import io.github.deltacv.vision.external.PipelineRenderHook; import org.opencv.android.Utils; import org.opencv.core.*; import org.opencv.imgproc.Imgproc; @@ -34,8 +35,6 @@ public abstract class OpenCvCameraBase implements OpenCvCamera { private OpenCvPipeline pipeline = null; - - private OpenCvViewport viewport; private OpenCvCameraRotation rotation; @@ -45,6 +44,9 @@ public abstract class OpenCvCameraBase implements OpenCvCamera { private Mat matToUseIfPipelineReturnedCropped; private Mat croppedColorCvtedMat = new Mat(); + private boolean isStreaming = false; + private boolean viewportEnabled = true; + private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE; ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY; boolean fpsMeterDesired = true; @@ -55,11 +57,14 @@ public abstract class OpenCvCameraBase implements OpenCvCamera { private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator(); - public OpenCvCameraBase(OpenCvViewport viewport) { + private double width; + private double height; + + public OpenCvCameraBase(OpenCvViewport viewport, boolean viewportEnabled) { this.viewport = viewport; this.rotation = getDefaultRotation(); - statistics.init(); + this.viewportEnabled = viewportEnabled; } @Override @@ -99,22 +104,22 @@ public int getFrameCount() { @Override public float getFps() { - return 0; + return statistics.getAvgFps(); } @Override public int getPipelineTimeMs() { - return 0; + return statistics.getAvgPipelineTime(); } @Override public int getOverheadTimeMs() { - return 0; + return statistics.getAvgOverheadTime(); } @Override public int getTotalFrameTimeMs() { - return 0; + return getTotalFrameTimeMs(); } @Override @@ -130,6 +135,46 @@ public void startRecordingPipeline(PipelineRecordingParameters parameters) { public void stopRecordingPipeline() { } + protected void notifyStartOfFrameProcessing() { + statistics.newInputFrameStart(); + } + + public synchronized final void prepareForOpenCameraDevice() + { + if (viewportEnabled) + { + setupViewport(); + viewport.setRenderingPolicy(desiredRenderingPolicy); + } + } + + public synchronized final void prepareForStartStreaming(int width, int height, OpenCvCameraRotation rotation) + { + this.rotation = rotation; + this.statistics = new PipelineStatisticsCalculator(); + this.statistics.init(); + + Size sizeAfterRotation = getFrameSizeAfterRotation(width, height, rotation); + + this.width = sizeAfterRotation.width; + this.height = sizeAfterRotation.height; + + if(viewport != null) + { + // viewport.setSize(width, height); + viewport.setOptimizedViewRotation(getOptimizedViewportRotation(rotation)); + viewport.activate(); + } + } + + public synchronized final void cleanupForEndStreaming() { + matToUseIfPipelineReturnedCropped = null; + + if (viewport != null) { + viewport.deactivate(); + } + } + protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) { statistics.newPipelineFrameStart(); @@ -277,7 +322,10 @@ protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCa } } - protected abstract OpenCvViewport setupViewport(); + protected void setupViewport() { + viewport.setFpsMeterEnabled(fpsMeterDesired); + viewport.setRenderHook(PipelineRenderHook.INSTANCE); + } protected abstract OpenCvCameraRotation getDefaultRotation(); @@ -287,4 +335,25 @@ protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCa protected abstract boolean isStreaming(); + protected Size getFrameSizeAfterRotation(int width, int height, OpenCvCameraRotation rotation) + { + int screenRenderedWidth, screenRenderedHeight; + int openCvRotateCode = mapRotationEnumToOpenCvRotateCode(rotation); + + if(openCvRotateCode == Core.ROTATE_90_CLOCKWISE || openCvRotateCode == Core.ROTATE_90_COUNTERCLOCKWISE) + { + //noinspection SuspiciousNameCombination + screenRenderedWidth = height; + //noinspection SuspiciousNameCombination + screenRenderedHeight = width; + } + else + { + screenRenderedWidth = width; + screenRenderedHeight = height; + } + + return new Size(screenRenderedWidth, screenRenderedHeight); + } + } diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java index 24465e97..9f49b525 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java @@ -50,12 +50,16 @@ interface RenderHook void setFpsMeterEnabled(boolean enabled); void pause(); void resume(); + void activate(); void deactivate(); + void setSize(int width, int height); void setOptimizedViewRotation(OptimizedRotation rotation); + void notifyStatistics(float fps, int pipelineMs, int overheadMs); void setRecording(boolean recording); + void post(Mat frame, Object userContext); void setRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy policy); void setRenderHook(RenderHook renderHook); diff --git a/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java index f6c5671e..c252c815 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java +++ b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java @@ -9,7 +9,6 @@ public class SourcedOpenCvCameraFactoryImpl extends OpenCvCameraFactory { - private OpenCvViewport viewport() { if(ThreadSourceHander.threadHander() instanceof ViewportAndSourceHander) { return ((ViewportAndSourceHander) ThreadSourceHander.threadHander()).viewport(); @@ -28,28 +27,36 @@ private VisionSource source(String name) { @Override public OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction) { - return new SourcedOpenCvCamera(source("default"), viewport()); + return new SourcedOpenCvCamera(source("default"), viewport(), false); } @Override public OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction, int viewportContainerId) { - return createInternalCamera(direction); + if(viewportContainerId <= 0) { + return createInternalCamera(direction); + } + + return new SourcedOpenCvCamera(source("default"), viewport(), true); } @Override public OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction) { - return new SourcedOpenCvCamera(source("default"), viewport()); + return new SourcedOpenCvCamera(source("default"), viewport(), false); } @Override public OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction, int viewportContainerId) { - return createInternalCamera2(direction); + if(viewportContainerId <= 0) { + return createInternalCamera2(direction); + } + + return new SourcedOpenCvCamera(source("default"), viewport(), true); } @Override public OpenCvWebcam createWebcam(WebcamName cameraName) { if(cameraName instanceof SourcedCameraName) { - return new SourcedOpenCvCamera(((SourcedCameraName) cameraName).getSource(), viewport()); + return new SourcedOpenCvCamera(((SourcedCameraName) cameraName).getSource(), viewport(), false); } else { throw new IllegalArgumentException("cameraName is not compatible with SourcedOpenCvCamera"); } @@ -57,7 +64,15 @@ public OpenCvWebcam createWebcam(WebcamName cameraName) { @Override public OpenCvWebcam createWebcam(WebcamName cameraName, int viewportContainerId) { - return createWebcam(cameraName); + if(viewportContainerId <= 0) { + return createWebcam(cameraName); + } + + if(cameraName instanceof SourcedCameraName) { + return new SourcedOpenCvCamera(((SourcedCameraName) cameraName).getSource(), viewport(), true); + } else { + throw new IllegalArgumentException("cameraName is not compatible with SourcedOpenCvCamera"); + } } } From fcbd0b795fe967f8cdaf8f3455ec59b50f9662d9 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 16 Aug 2023 12:07:36 -0600 Subject: [PATCH 19/46] Adding OpMode controls to GUI --- Common/build.gradle | 1 - .../eventloop/opmode/Autonomous.java | 75 +++++++ .../robotcore/eventloop/opmode/TeleOp.java | 66 +++++++ EOCV-Sim/build.gradle | 3 +- .../github/serivesmejia/eocvsim/EOCVSim.kt | 7 +- .../serivesmejia/eocvsim/gui/Visualizer.java | 37 ++-- .../PipelineOpModeSwitchablePanel.kt | 59 ++++++ .../visualizer/opmode/OpModeControlsPanel.kt | 17 ++ .../visualizer/opmode/OpModeSelectorPanel.kt | 134 +++++++++++++ .../pipeline/PipelineSelectorPanel.kt | 21 +- .../eocvsim/pipeline/PipelineManager.kt | 5 +- .../eventloop/opmode/OpModePipelineHandler.kt | 2 + .../eocvsim/input/VisionInputSourceHander.kt | 23 ++- TeamCode/build.gradle | 3 +- .../external/samples/ConceptAprilTag.java | 185 ++++++++++++++++++ .../external/samples/ConceptAprilTagEasy.java | 7 +- Vision/build.gradle | 5 +- .../robotcore/hardware/HardwareMap.java | 3 +- .../source/ftc/SourcedCameraNameImpl.java | 3 +- .../ftc/vision/VisionPortal.java | 6 +- build.common.gradle | 18 +- build.gradle | 10 +- gradle/wrapper/gradle-wrapper.properties | 4 +- settings.gradle | 4 + 24 files changed, 629 insertions(+), 69 deletions(-) create mode 100644 Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java create mode 100644 Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java rename Vision/src/main/java/io/github/deltacv/vision/{external => internal}/source/ftc/SourcedCameraNameImpl.java (91%) diff --git a/Common/build.gradle b/Common/build.gradle index b57da9d7..ba521628 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'java' id 'kotlin' id 'maven-publish' } diff --git a/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java new file mode 100644 index 00000000..5b9b8435 --- /dev/null +++ b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015 Robert Atkinson + * + * Ported from the Swerve library by Craig MacFarlane + * Based upon contributions and original idea by dmssargent. + * + * 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 Robert Atkinson, Craig MacFarlane 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 com.qualcomm.robotcore.eventloop.opmode; + +import java.lang.annotation.*; + +/** + * Provides an easy and non-centralized way of determining the OpMode list + * shown on an FTC Driver Station. Put an {@link Autonomous} annotation on + * your autonomous OpModes that you want to show up in the driver station display. + * + * If you want to temporarily disable an opmode, then set then also add + * a {@link Disabled} annotation to it. + * + * @see TeleOp + * @see Disabled + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Autonomous +{ + /** + * The name to be used on the driver station display. If empty, the name of + * the OpMode class will be used. + * @return the name to use for the OpMode in the driver station. + */ + String name() default ""; + + /** + * Optionally indicates a group of other OpModes with which the annotated + * OpMode should be sorted on the driver station OpMode list. + * @return the group into which the annotated OpMode is to be categorized + */ + String group() default ""; + + /** + * The name of the TeleOp OpMode you'd like to have automagically preselected + * on the Driver Station when selecting this Autonomous OpMode. If empty, then + * nothing will be automagically preselected. + * + * @return see above + */ + String preselectTeleOp() default ""; +} \ No newline at end of file diff --git a/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java new file mode 100644 index 00000000..cc6ac9f8 --- /dev/null +++ b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015 Robert Atkinson + * + * Ported from the Swerve library by Craig MacFarlane + * Based upon contributions and original idea by dmssargent. + * + * 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 Robert Atkinson, Craig MacFarlane 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 com.qualcomm.robotcore.eventloop.opmode; + +import java.lang.annotation.*; + +/** + * Provides an easy and non-centralized way of determining the OpMode list + * shown on an FTC Driver Station. Put an {@link TeleOp} annotation on + * your teleop OpModes that you want to show up in the driver station display. + * + * If you want to temporarily disable an opmode from showing up, then set then also add + * a {@link Disabled} annotation to it. + * + * @see Autonomous + * @see Disabled + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TeleOp +{ + /** + * The name to be used on the driver station display. If empty, the name of + * the OpMode class will be used. + * @return the name to use for the OpMode on the driver station + */ + String name() default ""; + + /** + * Optionally indicates a group of other OpModes with which the annotated + * OpMode should be sorted on the driver station OpMode list. + * @return the group into which the annotated OpMode is to be categorized + */ + String group() default ""; +} \ No newline at end of file diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 8c6d0154..f206f3c2 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -3,7 +3,6 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter plugins { - id 'java' id 'org.jetbrains.kotlin.jvm' id 'com.github.johnrengelman.shadow' id 'maven-publish' @@ -49,7 +48,7 @@ dependencies { implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" implementation "com.github.deltacv:steve:1.0.0" - implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" + implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version" implementation 'info.picocli:picocli:4.6.1' implementation 'com.google.code.gson:gson:2.8.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 5c405ec7..47bb644d 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 @@ -195,7 +195,6 @@ class EOCVSim(val params: Parameters = Parameters()) { inputSourceManager.init() //loading user created input sources pipelineManager.init() //init pipeline manager (scan for pipelines) - pipelineManager.subscribePipelineHandler(TimestampedPipelineHandler()) tunerManager.init() //init tunable variables manager @@ -213,8 +212,9 @@ class EOCVSim(val params: Parameters = Parameters()) { inputSourceManager.inputSourceLoader.saveInputSourcesToFile() - visualizer.waitForFinishingInit() + visualizer.joinInit() + pipelineManager.subscribePipelineHandler(TimestampedPipelineHandler()) pipelineManager.subscribePipelineHandler(OpModePipelineHandler(inputSourceManager, visualizer.viewport)) visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one @@ -224,6 +224,9 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.pipelineSelectorPanel.updatePipelinesList() //update pipelines and pick first one (DefaultPipeline) visualizer.pipelineSelectorPanel.selectedIndex = 0 + visualizer.opModeSelectorPanel.updateOpModesList() //update opmodes and pick first one (DefaultPipeline) + visualizer.opModeSelectorPanel.selectedIndex = 0 + //post output mats from the pipeline to the visualizer viewport pipelineManager.pipelineOutputPosters.add(visualizer.viewport) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 27475102..eec3cefc 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -26,13 +26,11 @@ import com.formdev.flatlaf.FlatLaf; import com.github.serivesmejia.eocvsim.Build; import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.*; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel; import io.github.deltacv.vision.external.gui.SwingOpenCvViewport; import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.InputSourceDropTarget; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.TelemetryPanel; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.TopMenuBar; import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel; import com.github.serivesmejia.eocvsim.gui.theme.Theme; import com.github.serivesmejia.eocvsim.gui.util.ReflectTaskbar; @@ -75,8 +73,13 @@ public class Visualizer { public JSplitPane globalSplitPane = null; public JSplitPane imageTunerSplitPane = null; + public PipelineOpModeSwitchablePanel pipelineOpModeSwitchablePanel = null; + public PipelineSelectorPanel pipelineSelectorPanel = null; public SourceSelectorPanel sourceSelectorPanel = null; + + public OpModeSelectorPanel opModeSelectorPanel = null; + public TelemetryPanel telemetryPanel; private String title = "EasyOpenCV Simulator v" + Build.standardVersionString; @@ -135,9 +138,14 @@ public void init(Theme theme) { tunerMenuPanel = new JPanel(); skiaPanel.add(tunerMenuPanel, BorderLayout.SOUTH); - pipelineSelectorPanel = new PipelineSelectorPanel(eocvSim); - sourceSelectorPanel = new SourceSelectorPanel(eocvSim); - telemetryPanel = new TelemetryPanel(); + pipelineOpModeSwitchablePanel = new PipelineOpModeSwitchablePanel(eocvSim); + + pipelineSelectorPanel = pipelineOpModeSwitchablePanel.getPipelineSelectorPanel(); + sourceSelectorPanel = pipelineOpModeSwitchablePanel.getSourceSelectorPanel(); + + opModeSelectorPanel = pipelineOpModeSwitchablePanel.getOpModeSelectorPanel(); + + telemetryPanel = new TelemetryPanel(); rightContainer = new JPanel(); @@ -153,18 +161,7 @@ public void init(Theme theme) { rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS)); - /* - * PIPELINE SELECTOR - */ - - pipelineSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(pipelineSelectorPanel); - - /* - * SOURCE SELECTOR - */ - sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(sourceSelectorPanel); + rightContainer.add(pipelineOpModeSwitchablePanel); /* * TELEMETRY @@ -284,7 +281,7 @@ public void componentResized(ComponentEvent evt) { public boolean hasFinishedInit() { return hasFinishedInitializing; } - public void waitForFinishingInit() { + public void joinInit() { while (!hasFinishedInitializing) { Thread.yield(); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt new file mode 100644 index 00000000..30dff791 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -0,0 +1,59 @@ +package com.github.serivesmejia.eocvsim.gui.component.visualizer + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeControlsPanel +import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel +import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel +import javax.swing.BoxLayout +import javax.swing.JPanel +import javax.swing.JTabbedPane +import javax.swing.border.EmptyBorder + +class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { + + val pipelinePanel = JPanel() + + val pipelineSelectorPanel = PipelineSelectorPanel(eocvSim) + val sourceSelectorPanel = SourceSelectorPanel(eocvSim) + + val opModePanel = JPanel() + + val opModeSelectorPanel = OpModeSelectorPanel(eocvSim) + val opModeControlsPanel = OpModeControlsPanel() + + private var beforeAllowPipelineSwitching = false + + init { + pipelinePanel.layout = BoxLayout(pipelinePanel, BoxLayout.Y_AXIS) + + pipelineSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + sourceSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + + pipelinePanel.add(pipelineSelectorPanel) + pipelinePanel.add(sourceSelectorPanel) + + opModePanel.layout = BoxLayout(opModePanel, BoxLayout.Y_AXIS) + + opModeSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + opModeControlsPanel.border = EmptyBorder(0, 20, 0, 20) + + opModePanel.add(opModeSelectorPanel) + opModePanel.add(opModeControlsPanel) + + add("Pipeline", pipelinePanel) + add("OpMode", opModePanel) + + addChangeListener { + val sourceTabbedPane = it.source as JTabbedPane + val index = sourceTabbedPane.selectedIndex + + if(index == 0) { + pipelineSelectorPanel.allowPipelineSwitching = beforeAllowPipelineSwitching + } else if(index == 1) { + beforeAllowPipelineSwitching = pipelineSelectorPanel.allowPipelineSwitching + pipelineSelectorPanel.allowPipelineSwitching = false + } + } + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt new file mode 100644 index 00000000..5a1e481c --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -0,0 +1,17 @@ +package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode + +import javax.swing.JPanel +import java.awt.GridBagConstraints +import java.awt.GridBagLayout + +class OpModeControlsPanel : JPanel() { + + init { + val c = GridBagConstraints() + layout = GridBagLayout() + + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt new file mode 100644 index 00000000..4dd47649 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -0,0 +1,134 @@ +package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.util.ReflectUtil +import com.qualcomm.robotcore.eventloop.opmode.OpMode +import com.qualcomm.robotcore.util.Range +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.* +import javax.swing.event.ListSelectionEvent + +class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { + + var selectedIndex: Int + get() = opModeSelector.selectedIndex + set(value) { + runBlocking { + launch(Dispatchers.Swing) { + opModeSelector.selectedIndex = value + } + } + } + + val opModeSelector = JList() + val opModeSelectorScroll = JScrollPane() + + val opModeSelectorLabel = JLabel("OpModes") + + // + private val indexMap = mutableMapOf() + + var allowOpModeSwitching = false + + private var beforeSelectedPipeline = -1 + + init { + layout = GridBagLayout() + + opModeSelectorLabel.font = opModeSelectorLabel.font.deriveFont(20.0f) + + opModeSelectorLabel.horizontalAlignment = JLabel.CENTER + + add(opModeSelectorLabel, GridBagConstraints().apply { + gridy = 0 + ipady = 20 + }) + + opModeSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) + opModeSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION + + opModeSelectorScroll.setViewportView(opModeSelector) + opModeSelectorScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS + opModeSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + + add(opModeSelectorScroll, GridBagConstraints().apply { + gridy = 1 + + weightx = 0.5 + weighty = 1.0 + fill = GridBagConstraints.BOTH + + ipadx = 120 + ipady = 20 + }) + + registerListeners() + } + + private fun registerListeners() { + + //listener for changing pipeline + opModeSelector.addListSelectionListener { evt: ListSelectionEvent -> + if(!allowOpModeSwitching) return@addListSelectionListener + + if (opModeSelector.selectedIndex != -1) { + val pipeline = indexMap[opModeSelector.selectedIndex] ?: return@addListSelectionListener + + if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) { + if (!eocvSim.pipelineManager.paused) { + eocvSim.pipelineManager.requestChangePipeline(pipeline) + beforeSelectedPipeline = pipeline + } else { + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + opModeSelector.setSelectedIndex(beforeSelectedPipeline) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.pipelineManager.requestChangePipeline(pipeline) + beforeSelectedPipeline = pipeline + } + } + } + } else { + opModeSelector.setSelectedIndex(1) + } + } + } + + fun updateOpModesList() = runBlocking { + launch(Dispatchers.Swing) { + val listModel = DefaultListModel() + var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE) + + indexMap.clear() + + for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) { + if(ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) { + listModel.addElement(pipeline.clazz.simpleName) + indexMap[selectorIndex] = managerIndex + + selectorIndex++ + } + } + + opModeSelector.fixedCellWidth = 240 + opModeSelector.model = listModel + + revalAndRepaint() + } + } + + fun revalAndRepaint() { + opModeSelector.revalidate() + opModeSelector.repaint() + opModeSelectorScroll.revalidate() + opModeSelectorScroll.repaint() + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index 73b2c559..7f8d603d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -26,6 +26,9 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.util.ReflectUtil +import com.qualcomm.robotcore.eventloop.opmode.OpMode +import com.qualcomm.robotcore.util.Range import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -53,6 +56,9 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { val pipelineSelectorLabel = JLabel("Pipelines") + // + private val indexMap = mutableMapOf() + val buttonsPanel = PipelineSelectorButtonsPanel(eocvSim) var allowPipelineSwitching = false @@ -104,7 +110,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { if(!allowPipelineSwitching) return@addListSelectionListener if (pipelineSelector.selectedIndex != -1) { - val pipeline = pipelineSelector.selectedIndex + val pipeline = indexMap[pipelineSelector.selectedIndex] ?: return@addListSelectionListener if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) { if (!eocvSim.pipelineManager.paused) { @@ -129,8 +135,17 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { fun updatePipelinesList() = runBlocking { launch(Dispatchers.Swing) { val listModel = DefaultListModel() - for (pipeline in eocvSim.pipelineManager.pipelines) { - listModel.addElement(pipeline.clazz.simpleName) + var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE) + + indexMap.clear() + + for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) { + if(!ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) { + listModel.addElement(pipeline.clazz.simpleName) + indexMap[selectorIndex] = managerIndex + + selectorIndex++ + } } pipelineSelector.fixedCellWidth = 240 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 8ac7efe7..6b248321 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 @@ -682,7 +682,10 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi eocvSim.onMainUpdate.doOnce { setPaused(paused, pauseReason) } } - fun refreshGuiPipelineList() = eocvSim.visualizer.pipelineSelectorPanel.updatePipelinesList() + fun refreshGuiPipelineList() { + eocvSim.visualizer.pipelineSelectorPanel.updatePipelinesList() + eocvSim.visualizer.opModeSelectorPanel.updateOpModesList() + } } diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index 9209c8d3..4a3400e7 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -12,6 +12,8 @@ import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.OpenCvViewport +enum class OpModeState { SELECTED, INIT, START, STOP, STOPPED } + class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private val viewport: OpenCvViewport) : SpecificPipelineHandler( { it is OpMode } ) { diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt index ec6fb78d..65a35306 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt @@ -8,22 +8,33 @@ import com.qualcomm.robotcore.eventloop.opmode.OpMode import io.github.deltacv.vision.external.source.ViewportAndSourceHander import io.github.deltacv.vision.external.source.VisionSource import io.github.deltacv.vision.external.source.VisionSourceHander +import org.opencv.core.Mat import org.opencv.core.Size +import org.opencv.videoio.VideoCapture import org.openftc.easyopencv.OpenCvViewport import java.io.File +import java.io.IOException import java.lang.IllegalArgumentException import java.net.URLConnection +import javax.imageio.ImageIO class VisionInputSourceHander(val stopNotifier: EventHandler, val viewport: OpenCvViewport) : ViewportAndSourceHander { - private fun isImage(path: String): Boolean { - val mimeType: String = URLConnection.getFileNameMap().getContentTypeFor(path) - return mimeType.startsWith("image") - } + private fun isImage(path: String) = try { + ImageIO.read(File(path)) != null + } catch(ex: IOException) { false } private fun isVideo(path: String): Boolean { - val mimeType: String = URLConnection.getFileNameMap().getContentTypeFor(path) - return mimeType.startsWith("video") + val capture = VideoCapture(path) + val mat = Mat() + + capture.read(mat) + + val isVideo = !mat.empty() + + capture.release() + + return isVideo } override fun hand(name: String): VisionSource { diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 11c85229..805fd90b 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -1,7 +1,6 @@ import java.nio.file.Paths plugins { - id 'java' id 'org.jetbrains.kotlin.jvm' } @@ -10,7 +9,7 @@ apply from: '../build.common.gradle' dependencies { implementation project(':EOCV-Sim') - implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" + implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib" } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java new file mode 100644 index 00000000..30e040a5 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java @@ -0,0 +1,185 @@ +/* Copyright (c) 2023 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.robotcontroller.external.samples; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import java.util.List; +import android.util.Size; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; +import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; +import org.firstinspires.ftc.vision.apriltag.AprilTagGameDatabase; + +/** + * This 2023-2024 OpMode illustrates the basics of AprilTag recognition and pose estimation, + * including Java Builder structures for specifying Vision parameters. + * + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. + */ +@TeleOp(name = "Concept: AprilTag", group = "Concept") +// @Disabled +public class ConceptAprilTag extends LinearOpMode { + + private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera + + /** + * {@link #aprilTag} is the variable to store our instance of the AprilTag processor. + */ + private AprilTagProcessor aprilTag; + + /** + * {@link #visionPortal} is the variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch Play to start OpMode"); + telemetry.update(); + waitForStart(); + + if (opModeIsActive()) { + while (opModeIsActive()) { + + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Share the CPU. + sleep(20); + } + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end method runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor. + aprilTag = new AprilTagProcessor.Builder() + //.setDrawAxes(false) + //.setDrawCubeProjection(false) + //.setDrawTagOutline(true) + //.setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) + //.setTagLibrary(AprilTagGameDatabase.getCenterStageTagLibrary()) + //.setOutputUnits(DistanceUnit.INCH, AngleUnit.DEGREES) + + // == CAMERA CALIBRATION == + // If you do not manually specify calibration parameters, the SDK will attempt + // to load a predefined calibration for your camera. + //.setLensIntrinsics(578.272, 578.272, 402.145, 221.506) + + // ... these parameters are fx, fy, cx, cy. + + .build(); + + // Create the vision portal by using a builder. + VisionPortal.Builder builder = new VisionPortal.Builder(); + + // Set the camera (webcam vs. built-in RC phone camera). + if (USE_WEBCAM) { + builder.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")); + } else { + builder.setCamera(BuiltinCameraDirection.BACK); + } + + // Choose a camera resolution. Not all cameras support all resolutions. + //builder.setCameraResolution(new Size(640, 480)); + + // Enable the RC preview (LiveView). Set "false" to omit camera monitoring. + //builder.enableCameraMonitoring(true); + + // Set the stream format; MJPEG uses less bandwidth than default YUY2. + //builder.setStreamFormat(VisionPortal.StreamFormat.YUY2); + + // Choose whether or not LiveView stops if no processors are enabled. + // If set "true", monitor shows solid orange screen if no processors enabled. + // If set "false", monitor shows camera view without annotations. + //builder.setAutoStopLiveView(false); + + // Set and enable the processor. + builder.addProcessor(aprilTag); + + // Build the Vision Portal, using the above settings. + visionPortal = builder.build(); + + // Disable or re-enable the aprilTag processor at any time. + //visionPortal.setProcessorEnabled(aprilTag, true); + + } // end method initAprilTag() + + + /** + * Function to add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + List currentDetections = aprilTag.getDetections(); + telemetry.addData("# AprilTags Detected", currentDetections.size()); + + // Step through the list of detections and display info for each one. + for (AprilTagDetection detection : currentDetections) { + if (detection.metadata != null) { + telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name)); + telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z)); + telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw)); + telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation)); + } else { + telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id)); + telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y)); + } + } // end for() loop + + // Add "key" information to telemetry + telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist."); + telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)"); + telemetry.addLine("RBE = Range, Bearing & Elevation"); + + } // end method telemetryAprilTag() + +} // end class \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java index ce8a6789..6f457e5f 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java @@ -31,7 +31,7 @@ import com.qualcomm.robotcore.eventloop.opmode.Disabled; import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; -// import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; import java.util.List; import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; @@ -46,7 +46,7 @@ * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. */ -// @TeleOp(name = "Concept: AprilTag Easy", group = "Concept") +@TeleOp(name = "Concept: AprilTag Easy", group = "Concept") // @Disabled public class ConceptAprilTagEasy extends LinearOpMode { @@ -71,6 +71,7 @@ public void runOpMode() { telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); telemetry.addData(">", "Touch Play to start OpMode"); telemetry.update(); + waitForStart(); if (opModeIsActive()) { @@ -102,7 +103,7 @@ private void initAprilTag() { // Create the vision portal the easy way. if (USE_WEBCAM) { visionPortal = VisionPortal.easyCreateWithDefaults( - hardwareMap.get(WebcamName.class, "Webcam 1"), aprilTag); + hardwareMap.get(WebcamName.class, "C:\\Users\\s3riv\\Downloads\\IMG_6112.avi"), aprilTag); } else { visionPortal = VisionPortal.easyCreateWithDefaults( BuiltinCameraDirection.BACK, aprilTag); diff --git a/Vision/build.gradle b/Vision/build.gradle index bfe7fe40..18a23198 100644 --- a/Vision/build.gradle +++ b/Vision/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'java' id 'kotlin' id 'maven-publish' } @@ -29,9 +28,7 @@ configurations.all { dependencies { implementation project(':Common') - implementation("com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version") { - if(env == 'dev') { changing = true } - } + implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version" api "org.openpnp:opencv:$opencv_version" diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java index 5f095ab1..f0c39b30 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java @@ -1,9 +1,8 @@ package com.qualcomm.robotcore.hardware; import io.github.deltacv.vision.external.source.ThreadSourceHander; -import io.github.deltacv.vision.external.source.ftc.SourcedCameraNameImpl; +import io.github.deltacv.vision.internal.source.ftc.SourcedCameraNameImpl; import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; -import org.openftc.easyopencv.OpenCvViewport; public class HardwareMap { diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java similarity index 91% rename from Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java rename to Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java index a4de3009..832628c4 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/ftc/SourcedCameraNameImpl.java +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java @@ -1,10 +1,9 @@ -package io.github.deltacv.vision.external.source.ftc; +package io.github.deltacv.vision.internal.source.ftc; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.qualcomm.robotcore.util.SerialNumber; import io.github.deltacv.vision.external.source.VisionSource; -import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName; import org.jetbrains.annotations.NotNull; public class SourcedCameraNameImpl extends SourcedCameraName { diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java index 79973bbe..e3dd8c0b 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java @@ -39,7 +39,7 @@ import java.util.List; import io.github.deltacv.vision.external.source.ThreadSourceHander; -import io.github.deltacv.vision.external.source.ftc.SourcedCameraNameImpl; +import io.github.deltacv.vision.internal.source.ftc.SourcedCameraNameImpl; import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; @@ -196,9 +196,7 @@ public Builder setStreamFormat(StreamFormat streamFormat) */ public Builder enableCameraMonitoring(boolean enableLiveView) { - // dont care + l + ratio - - // return setCameraMonitorViewId(viewId); + setCameraMonitorViewId(1); return this; } diff --git a/build.common.gradle b/build.common.gradle index e67f6f18..07f92ee5 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -1,13 +1,5 @@ -java { - sourceCompatibility = JavaVersion.VERSION_1_10 - targetCompatibility = JavaVersion.VERSION_1_10 -} - -if (project.getPluginManager().hasPlugin("org.jetbrains.kotlin.jvm")) { - compileKotlin { - kotlinOptions { - jvmTarget = "10" - freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" - } - } -} +java { + toolchain { + languageVersion = JavaLanguageVersion.of(10) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6f6bea26..8628c343 100644 --- a/build.gradle +++ b/build.gradle @@ -4,12 +4,12 @@ import java.time.format.DateTimeFormatter buildscript { ext { - kotlin_version = "1.8.0" + kotlin_version = "1.9.0" kotlinx_coroutines_version = "1.5.0-native-mt" slf4j_version = "1.7.32" log4j_version = "2.17.1" opencv_version = "4.5.5-1" - apriltag_plugin_version = "2.0.0-A" + apriltag_plugin_version = "2.0.0-B" skiko_version = "0.7.75" classgraph_version = "4.8.108" @@ -31,10 +31,16 @@ buildscript { } } +plugins { + id 'java' +} + allprojects { group 'com.github.deltacv' version '3.5.0' + apply plugin: 'java' + ext { standardVersion = version } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b65adcfb..56ff32c7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 51b5ae1a..3e05ebcd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,10 @@ pluginManagement { } } +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version "0.4.0" +} + rootProject.name = 'EOCV-Sim' include 'TeamCode' From db7b735fec4442a29847e31a8f90ac02aa94b8c1 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 16 Aug 2023 23:13:59 -0600 Subject: [PATCH 20/46] Work on GUI for opmodes and github actions to java 10 --- .github/workflows/build_ci.yml | 2 +- .github/workflows/release_ci.yml | 2 +- .../PipelineOpModeSwitchablePanel.kt | 33 ++++--- .../visualizer/opmode/OpModeSelectorPanel.kt | 98 ++++++++----------- .../pipeline/PipelineSelectorPanel.kt | 4 +- 5 files changed, 64 insertions(+), 75 deletions(-) diff --git a/.github/workflows/build_ci.yml b/.github/workflows/build_ci.yml index 1e0ef849..f927e63b 100644 --- a/.github/workflows/build_ci.yml +++ b/.github/workflows/build_ci.yml @@ -12,7 +12,7 @@ jobs: uses: actions/setup-java@v2.1.0 with: distribution: adopt - java-version: 8 + java-version: 10 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index d3425656..14778108 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -15,7 +15,7 @@ jobs: uses: actions/setup-java@v2.1.0 with: distribution: adopt - java-version: 8 + java-version: 10 - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 30dff791..32cfc56f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -4,7 +4,9 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeControlsPanel import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel -import javax.swing.BoxLayout +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.GridLayout import javax.swing.JPanel import javax.swing.JTabbedPane import javax.swing.border.EmptyBorder @@ -21,24 +23,27 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val opModeSelectorPanel = OpModeSelectorPanel(eocvSim) val opModeControlsPanel = OpModeControlsPanel() - private var beforeAllowPipelineSwitching = false + private var beforeAllowPipelineSwitching: Boolean? = null init { - pipelinePanel.layout = BoxLayout(pipelinePanel, BoxLayout.Y_AXIS) - - pipelineSelectorPanel.border = EmptyBorder(0, 20, 0, 20) - sourceSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + pipelinePanel.layout = GridLayout(2, 1) + pipelineSelectorPanel.border = EmptyBorder(0, 20, 20, 20) pipelinePanel.add(pipelineSelectorPanel) - pipelinePanel.add(sourceSelectorPanel) - opModePanel.layout = BoxLayout(opModePanel, BoxLayout.Y_AXIS) + sourceSelectorPanel.border = EmptyBorder(0, 20, 20, 20) + pipelinePanel.add(sourceSelectorPanel) - opModeSelectorPanel.border = EmptyBorder(0, 20, 0, 20) - opModeControlsPanel.border = EmptyBorder(0, 20, 0, 20) + opModePanel.layout = GridBagLayout() - opModePanel.add(opModeSelectorPanel) - opModePanel.add(opModeControlsPanel) + opModePanel.add(opModeSelectorPanel, GridBagConstraints().apply { + gridy = 0 + ipady = 20 + }) + opModePanel.add(opModeControlsPanel, GridBagConstraints().apply { + gridy = 1 + ipady = 20 + }) add("Pipeline", pipelinePanel) add("OpMode", opModePanel) @@ -47,8 +52,8 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val sourceTabbedPane = it.source as JTabbedPane val index = sourceTabbedPane.selectedIndex - if(index == 0) { - pipelineSelectorPanel.allowPipelineSwitching = beforeAllowPipelineSwitching + if(index == 0 && beforeAllowPipelineSwitching != null) { + pipelineSelectorPanel.allowPipelineSwitching = beforeAllowPipelineSwitching!! } else if(index == 1) { beforeAllowPipelineSwitching = pipelineSelectorPanel.allowPipelineSwitching pipelineSelectorPanel.allowPipelineSwitching = false diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index 4dd47649..eff2ff21 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.swing.Swing +import java.awt.BorderLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout import javax.swing.* @@ -17,55 +18,63 @@ import javax.swing.event.ListSelectionEvent class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { - var selectedIndex: Int - get() = opModeSelector.selectedIndex + var selectedIndex = -1 set(value) { - runBlocking { - launch(Dispatchers.Swing) { - opModeSelector.selectedIndex = value - } - } + field = value } - val opModeSelector = JList() - val opModeSelectorScroll = JScrollPane() - - val opModeSelectorLabel = JLabel("OpModes") - // private val indexMap = mutableMapOf() - var allowOpModeSwitching = false + val autonomousButton = JButton("\\/") + + val textPanel = JPanel() + val selectOpModeLabel = JLabel("Select Op Mode") + val buttonDescriptorLabel = JLabel("<- Autonomous | TeleOp ->") + + val teleopButton = JButton("\\/") + + var allowOpModeSwitching = false private var beforeSelectedPipeline = -1 init { layout = GridBagLayout() + textPanel.layout = GridBagLayout() - opModeSelectorLabel.font = opModeSelectorLabel.font.deriveFont(20.0f) - - opModeSelectorLabel.horizontalAlignment = JLabel.CENTER - - add(opModeSelectorLabel, GridBagConstraints().apply { + add(autonomousButton, GridBagConstraints().apply { + gridx = 0 gridy = 0 ipady = 20 }) - opModeSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) - opModeSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION + selectOpModeLabel.horizontalTextPosition = JLabel.CENTER + selectOpModeLabel.horizontalAlignment = JLabel.CENTER - opModeSelectorScroll.setViewportView(opModeSelector) - opModeSelectorScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS - opModeSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + buttonDescriptorLabel.horizontalTextPosition = JLabel.CENTER + buttonDescriptorLabel.horizontalAlignment = JLabel.CENTER - add(opModeSelectorScroll, GridBagConstraints().apply { + textPanel.add(selectOpModeLabel, GridBagConstraints().apply { + gridx = 0 + gridy = 0 + ipady = 0 + }) + + textPanel.add(buttonDescriptorLabel, GridBagConstraints().apply { + gridx = 0 gridy = 1 + ipadx = 10 + }) - weightx = 0.5 - weighty = 1.0 - fill = GridBagConstraints.BOTH + add(textPanel, GridBagConstraints().apply { + gridx = 1 + gridy = 0 + ipadx = 20 + }) - ipadx = 120 + add(teleopButton, GridBagConstraints().apply { + gridx = 2 + gridy = 0 ipady = 20 }) @@ -74,35 +83,10 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { private fun registerListeners() { - //listener for changing pipeline - opModeSelector.addListSelectionListener { evt: ListSelectionEvent -> - if(!allowOpModeSwitching) return@addListSelectionListener - - if (opModeSelector.selectedIndex != -1) { - val pipeline = indexMap[opModeSelector.selectedIndex] ?: return@addListSelectionListener - - if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.pipelineManager.requestChangePipeline(pipeline) - beforeSelectedPipeline = pipeline - } else { - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - opModeSelector.setSelectedIndex(beforeSelectedPipeline) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) - eocvSim.pipelineManager.requestChangePipeline(pipeline) - beforeSelectedPipeline = pipeline - } - } - } - } else { - opModeSelector.setSelectedIndex(1) - } - } } fun updateOpModesList() = runBlocking { - launch(Dispatchers.Swing) { + /* launch(Dispatchers.Swing) { val listModel = DefaultListModel() var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE) @@ -121,14 +105,14 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { opModeSelector.model = listModel revalAndRepaint() - } + }*/ } fun revalAndRepaint() { - opModeSelector.revalidate() + /* opModeSelector.revalidate() opModeSelector.repaint() opModeSelectorScroll.revalidate() - opModeSelectorScroll.repaint() + opModeSelectorScroll.repaint() */ } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index 7f8d603d..f3caf733 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -42,11 +42,11 @@ import javax.swing.event.ListSelectionEvent class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { var selectedIndex: Int - get() = pipelineSelector.selectedIndex + get() = indexMap[pipelineSelector.selectedIndex] ?: -1 set(value) { runBlocking { launch(Dispatchers.Swing) { - pipelineSelector.selectedIndex = value + pipelineSelector.selectedIndex = indexMap.entries.find { it.value == value }?.key ?: -1 } } } From ef3a1f848591e6848ad36300388b41371e9caffb Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 16 Aug 2023 23:33:43 -0600 Subject: [PATCH 21/46] Downgrade gradle --- .../com/github/serivesmejia/eocvsim/input/InputSource.java | 4 ++-- .../serivesmejia/eocvsim/input/InputSourceManager.java | 5 +++++ .../serivesmejia/eocvsim/input/source/ImageSource.java | 6 +++++- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java index c9db10b8..e8f394d0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java @@ -41,13 +41,13 @@ public abstract class InputSource implements Comparable { protected transient long createdOn = -1L; public abstract boolean init(); - public abstract void reset(); + public void cleanIfDirty() { } + public abstract void close(); public abstract void onPause(); - public abstract void onResume(); public void setSize(Size size) {} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index bb8a0aaa..6c8e4bd0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -245,7 +245,12 @@ public boolean setInputSource(String sourceName) { logger.info("Set InputSource to " + currentInputSource.toString() + " (" + src.getClass().getSimpleName() + ")"); return true; + } + public void cleanSourceIfDirty() { + if(currentInputSource != null) { + currentInputSource.cleanIfDirty(); + } } public boolean isNameOnUse(String name) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java index efc985e1..155a807f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java @@ -142,16 +142,20 @@ public void readImage() { @Override public Mat update() { - if (isPaused) return lastCloneTo; if (lastCloneTo == null) lastCloneTo = matRecycler.takeMat(); if (img == null) return null; + lastCloneTo.release(); img.copyTo(lastCloneTo); return lastCloneTo; + } + @Override + public void cleanIfDirty() { + readImage(); } @Override diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 56ff32c7..17b4076a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file From 93864bb1dfd9f90b33049d9b60f32cb517a85c7e Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 17 Aug 2023 01:44:01 -0600 Subject: [PATCH 22/46] Icons class reestructuration & work on OpMode controls --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 4 +- .../eocvsim/gui/EOCVSimIconLibrary.kt | 27 +++++++++ .../serivesmejia/eocvsim/gui/IconLibrary.kt | 17 ++++++ .../github/serivesmejia/eocvsim/gui/Icons.kt | 52 +++++++++-------- .../serivesmejia/eocvsim/gui/Visualizer.java | 7 ++- .../eocvsim/gui/component/PopupX.kt | 20 +++++-- .../gui/component/tuner/ColorPicker.kt | 3 +- .../tuner/TunableFieldPanelOptions.kt | 9 +-- .../PipelineOpModeSwitchablePanel.kt | 20 ++++--- .../visualizer/opmode/OpModeControlsPanel.kt | 32 +++++++++-- .../visualizer/opmode/OpModePopupPanel.kt | 12 ++++ .../visualizer/opmode/OpModeSelectorPanel.kt | 53 ++++++------------ .../eocvsim/gui/dialog/SplashScreen.kt | 3 +- .../gui/util/icon/PipelineListIconRenderer.kt | 5 +- .../util/icon/SourcesListIconRenderer.java | 7 ++- .../eocvsim/input/InputSourceManager.java | 2 + .../eocvsim/input/source/ImageSource.java | 1 - .../eocvsim/pipeline/PipelineManager.kt | 3 +- .../eventloop/opmode/OpModePipelineHandler.kt | 2 + .../images/icon/ico_arrow_dropdown.png | Bin 0 -> 318 bytes .../resources/images/icon/ico_not_started.png | Bin 0 -> 759 bytes .../main/resources/images/icon/ico_play.png | Bin 0 -> 869 bytes .../main/resources/images/icon/ico_stop.png | Bin 0 -> 726 bytes .../external/gui/SwingOpenCvViewport.kt | 8 +++ .../easyopencv/OpenCvViewRenderer.java | 6 -- 25 files changed, 195 insertions(+), 98 deletions(-) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt create mode 100644 EOCV-Sim/src/main/resources/images/icon/ico_arrow_dropdown.png create mode 100644 EOCV-Sim/src/main/resources/images/icon/ico_not_started.png create mode 100644 EOCV-Sim/src/main/resources/images/icon/ico_play.png create mode 100644 EOCV-Sim/src/main/resources/images/icon/ico_stop.png 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 47bb644d..e24012c2 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 @@ -308,6 +308,8 @@ class EOCVSim(val params: Parameters = Parameters()) { } break //bye bye + } catch (ex: InterruptedException) { + break // bye bye } //limit FPG @@ -436,7 +438,7 @@ class EOCVSim(val params: Parameters = Parameters()) { val isPaused = if (pipelineManager.paused) " (Paused)" else "" val isRecording = if (isCurrentlyRecording()) " RECORDING" else "" - val msg = isRecording + pipelineFpsMsg + posterFpsMsg + isPaused + val msg = isRecording + isPaused if (pipelineManager.currentPipeline == null) { visualizer.setTitleMessage("No pipeline$msg${workspaceMsg}") diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt new file mode 100644 index 00000000..51b6ac8f --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt @@ -0,0 +1,27 @@ +package com.github.serivesmejia.eocvsim.gui + +object EOCVSimIconLibrary { + + val icoEOCVSim by icon("ico_eocvsim", "/images/icon/ico_eocvsim.png", false) + + val icoImg by icon("ico_img", "/images/icon/ico_img.png") + val icoCam by icon("ico_cam", "/images/icon/ico_cam.png") + val icoVid by icon("ico_vid", "/images/icon/ico_vid.png") + + val icoConfig by icon("ico_config", "/images/icon/ico_config.png") + val icoSlider by icon("ico_slider", "/images/icon/ico_slider.png") + val icoTextbox by icon("ico_textbox", "/images/icon/ico_textbox.png") + val icoColorPick by icon("ico_colorpick", "/images/icon/ico_colorpick.png") + + val icoArrowDropdown by icon("ico_arrow_dropdown", "/images/icon/ico_arrow_dropdown.png") + + val icoNotStarted by icon("ico_not_started", "/images/icon/ico_not_started.png") + val icoPlay by icon("ico_play", "/images/icon/ico_play.png") + val icoStop by icon("ico_stop", "/images/icon/ico_stop.png") + + val icoGears by icon("ico_gears", "/images/icon/ico_gears.png") + val icoHammer by icon("ico_hammer", "/images/icon/ico_hammer.png") + + val icoColorPickPointer by icon("ico_colorpick_pointer", "/images/icon/ico_colorpick_pointer.png") + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt new file mode 100644 index 00000000..b5d95097 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt @@ -0,0 +1,17 @@ +package com.github.serivesmejia.eocvsim.gui + +import java.awt.image.BufferedImage +import javax.swing.ImageIcon +import kotlin.reflect.KProperty + +fun icon(name: String, path: String, allowInvert: Boolean = true) = EOCVSimIconDelegate(name, path, allowInvert) + +class EOCVSimIconDelegate(val name: String, val path: String, allowInvert: Boolean = true) { + init { + Icons.addFutureImage(name, path, allowInvert) + } + + operator fun getValue(eocvSimIconLibrary: EOCVSimIconLibrary, property: KProperty<*>): Icons.NamedImageIcon { + return Icons.getImage(name) + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt index cc0697c4..dbd863e6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt @@ -26,6 +26,7 @@ package com.github.serivesmejia.eocvsim.gui import com.github.serivesmejia.eocvsim.gui.util.GuiUtil import com.github.serivesmejia.eocvsim.util.loggerForThis import io.github.deltacv.vision.external.gui.util.ImgUtil +import java.awt.Image import java.awt.image.BufferedImage import java.util.NoSuchElementException import javax.swing.ImageIcon @@ -34,8 +35,8 @@ object Icons { private val bufferedImages = HashMap() - private val icons = HashMap() - private val resizedIcons = HashMap() + private val icons = HashMap() + private val resizedIcons = HashMap() private val futureIcons = mutableListOf() @@ -43,25 +44,7 @@ object Icons { val logger by loggerForThis() - init { - addFutureImage("ico_eocvsim", "/images/icon/ico_eocvsim.png", false) - - addFutureImage("ico_img", "/images/icon/ico_img.png") - addFutureImage("ico_cam", "/images/icon/ico_cam.png") - addFutureImage("ico_vid", "/images/icon/ico_vid.png") - - addFutureImage("ico_config", "/images/icon/ico_config.png") - addFutureImage("ico_slider", "/images/icon/ico_slider.png") - addFutureImage("ico_textbox", "/images/icon/ico_textbox.png") - addFutureImage("ico_colorpick", "/images/icon/ico_colorpick.png") - - addFutureImage("ico_gears", "/images/icon/ico_gears.png") - addFutureImage("ico_hammer", "/images/icon/ico_hammer.png") - - addFutureImage("ico_colorpick_pointer", "/images/icon/ico_colorpick_pointer.png") - } - - fun getImage(name: String): ImageIcon { + fun getImage(name: String): NamedImageIcon { for(futureIcon in futureIcons.toTypedArray()) { if(futureIcon.name == name) { logger.trace("Loading future icon $name") @@ -81,7 +64,7 @@ object Icons { getImageResized(name, width, height) } - fun getImageResized(name: String, width: Int, height: Int): ImageIcon { + fun getImageResized(name: String, width: Int, height: Int): NamedImageIcon { //determines the icon name from the: //name, widthxheight, is inverted or is original val resIconName = "$name-${width}x${height}${ @@ -95,7 +78,7 @@ object Icons { val icon = if(resizedIcons.contains(resIconName)) { resizedIcons[resIconName] } else { - resizedIcons[resIconName] = ImgUtil.scaleImage(getImage(name), width, height) + resizedIcons[resIconName] = NamedImageIcon(name, ImgUtil.scaleImage(getImage(name), width, height).image) resizedIcons[resIconName] } @@ -113,7 +96,7 @@ object Icons { } bufferedImages[name] = Image(buffImg, allowInvert) - icons[name] = ImageIcon(buffImg) + icons[name] = NamedImageIcon(name, buffImg) } fun setDark(dark: Boolean) { @@ -142,4 +125,25 @@ object Icons { data class FutureIcon(val name: String, val resourcePath: String, val allowInvert: Boolean) + class NamedImageIcon internal constructor(val name: String, image: java.awt.Image) : ImageIcon(image) { + fun resized(width: Int, height: Int) = getImageResized(name, width, height) + + fun lazyResized(width: Int, height: Int) = lazyGetImageResized(name, width, height) + + fun scaleToFit(suggestedWidth: Int, suggestedHeight: Int): NamedImageIcon { + val width = iconWidth + val height = iconHeight + + return if (width > height) { + val newWidth = suggestedWidth + val newHeight = (height * newWidth) / width + resized(newWidth, newHeight) + } else { + val newHeight = suggestedHeight + val newWidth = (width * newHeight) / height + resized(newWidth, newHeight) + } + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index eec3cefc..05c138de 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -127,7 +127,12 @@ public void init(Theme theme) { //instantiate all swing elements after theme installation frame = new JFrame(); - viewport = new SwingOpenCvViewport(new Size(1080, 720), "deltacv EOCV-Sim v" + Build.standardVersionString); + String fpsMeterDescriptor = "deltacv EOCV-Sim v" + Build.standardVersionString; + if(Build.isDev) fpsMeterDescriptor += "-dev"; + + viewport = new SwingOpenCvViewport(new Size(1080, 720), fpsMeterDescriptor); + viewport.setDark(FlatLaf.isLafDark()); + JLayeredPane skiaPanel = viewport.skiaPanel(); skiaPanel.setLayout(new BorderLayout()); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt index 15cf5bd3..166eb5b7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt @@ -29,10 +29,7 @@ import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener -import javax.swing.JPanel -import javax.swing.JPopupMenu -import javax.swing.JWindow -import javax.swing.Popup +import javax.swing.* class PopupX @JvmOverloads constructor(windowAncestor: Window, private val panel: JPanel, @@ -97,4 +94,19 @@ class PopupX @JvmOverloads constructor(windowAncestor: Window, fun setLocation(width: Int, height: Int) = window.setLocation(width, height) + companion object { + + fun JComponent.popUpXOnThis(panel: JPanel, + closeOnFocusLost: Boolean = true, + fixX: Boolean = false, + fixY: Boolean = true): PopupX { + + val frame = SwingUtilities.getWindowAncestor(this) + val location = locationOnScreen + + return PopupX(frame, panel, location.x, location.y, closeOnFocusLost, fixX, fixY) + } + + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt index d13994eb..f065ff2b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt @@ -23,6 +23,7 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.gui.Icons import io.github.deltacv.vision.external.gui.component.ImageX @@ -42,7 +43,7 @@ class ColorPicker(private val imageX: ImageX) { 200 } else { 35 } - val colorPickIco = Icons.getImageResized("ico_colorpick_pointer", size, size).image + val colorPickIco = EOCVSimIconLibrary.icoColorPickPointer.resized(size, size).image val colorPickCursor = Toolkit.getDefaultToolkit().createCustomCursor( colorPickIco, Point(0, 0), "Color Pick Pointer" 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 a00d2cdb..2e4557af 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 @@ -24,6 +24,7 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.Icons import com.github.serivesmejia.eocvsim.gui.component.PopupX import io.github.deltacv.vision.external.util.extension.cvtColor @@ -39,10 +40,10 @@ import javax.swing.event.AncestorListener class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, eocvSim: EOCVSim) : JPanel() { - private val sliderIco by Icons.lazyGetImageResized("ico_slider", 15, 15) - private val textBoxIco by Icons.lazyGetImageResized("ico_textbox", 15, 15) - private val configIco by Icons.lazyGetImageResized("ico_config", 15, 15) - private val colorPickIco by Icons.lazyGetImageResized("ico_colorpick", 15, 15) + private val sliderIco by EOCVSimIconLibrary.icoSlider.lazyResized(15, 15) + private val textBoxIco by EOCVSimIconLibrary.icoTextbox.lazyResized(15, 15) + private val configIco by EOCVSimIconLibrary.icoConfig.lazyResized(15, 15) + private val colorPickIco by EOCVSimIconLibrary.icoColorPick.lazyResized(15, 15) private val textBoxSliderToggle = JToggleButton() private val configButton = JButton() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 32cfc56f..76197c1d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -34,16 +34,13 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { sourceSelectorPanel.border = EmptyBorder(0, 20, 20, 20) pipelinePanel.add(sourceSelectorPanel) - opModePanel.layout = GridBagLayout() + opModePanel.layout = GridLayout(2, 1) - opModePanel.add(opModeSelectorPanel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) - opModePanel.add(opModeControlsPanel, GridBagConstraints().apply { - gridy = 1 - ipady = 20 - }) + opModeSelectorPanel.border = EmptyBorder(0, 20, 20, 20) + opModePanel.add(opModeSelectorPanel) + + opModeControlsPanel.border = EmptyBorder(0, 20, 20, 20) + opModePanel.add(opModeControlsPanel) add("Pipeline", pipelinePanel) add("OpMode", opModePanel) @@ -61,4 +58,9 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { } } + fun updateSelectorLists() { + pipelineSelectorPanel.updatePipelinesList() + opModeSelectorPanel.updateOpModesList() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt index 5a1e481c..9a663254 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -1,17 +1,41 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary +import java.awt.BorderLayout import javax.swing.JPanel import java.awt.GridBagConstraints import java.awt.GridBagLayout +import javax.swing.JButton +import javax.swing.SwingUtilities class OpModeControlsPanel : JPanel() { + val controlButton = JButton() + init { - val c = GridBagConstraints() - layout = GridBagLayout() + layout = BorderLayout() + + SwingUtilities.invokeLater { + updateControlButtonIcon() + } + + controlButton.addComponentListener(object : java.awt.event.ComponentAdapter() { + override fun componentResized(evt: java.awt.event.ComponentEvent?) { + updateControlButtonIcon() + } + }) + + add(controlButton, BorderLayout.CENTER) + } + + fun updateControlButtonIcon() { + val size = controlButton.size + val width = size.width + val height = size.height + + if(width <= 0 || height <= 0) return - c.fill = GridBagConstraints.BOTH; - c.anchor = GridBagConstraints.CENTER; + controlButton.icon = EOCVSimIconLibrary.icoNotStarted.scaleToFit(width, height) } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt new file mode 100644 index 00000000..b3045341 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt @@ -0,0 +1,12 @@ +package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode + +import javax.swing.JButton +import javax.swing.JPanel + +class OpModePopupPanel : JPanel() { + + init { + add(JButton("Test")) + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index eff2ff21..a2db088d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -1,20 +1,15 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import com.github.serivesmejia.eocvsim.util.ReflectUtil -import com.qualcomm.robotcore.eventloop.opmode.OpMode -import com.qualcomm.robotcore.util.Range -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary +import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnThis +import com.qualcomm.robotcore.eventloop.opmode.OpModeType import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing -import java.awt.BorderLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.awt.Insets import javax.swing.* -import javax.swing.event.ListSelectionEvent + class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { @@ -26,26 +21,27 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { // private val indexMap = mutableMapOf() - val autonomousButton = JButton("\\/") + val autonomousButton = JButton() val textPanel = JPanel() val selectOpModeLabel = JLabel("Select Op Mode") val buttonDescriptorLabel = JLabel("<- Autonomous | TeleOp ->") - val teleopButton = JButton("\\/") - - var allowOpModeSwitching = false - private var beforeSelectedPipeline = -1 + val teleopButton = JButton() init { layout = GridBagLayout() textPanel.layout = GridBagLayout() + autonomousButton.icon = EOCVSimIconLibrary.icoArrowDropdown + add(autonomousButton, GridBagConstraints().apply { gridx = 0 gridy = 0 ipady = 20 + + gridheight = 1 }) selectOpModeLabel.horizontalTextPosition = JLabel.CENTER @@ -72,40 +68,27 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { ipadx = 20 }) + teleopButton.icon = EOCVSimIconLibrary.icoArrowDropdown + add(teleopButton, GridBagConstraints().apply { gridx = 2 gridy = 0 ipady = 20 + + gridheight = 1 }) registerListeners() } private fun registerListeners() { - + autonomousButton.addActionListener { + autonomousButton.popUpXOnThis(OpModePopupPanel()).show() + } } fun updateOpModesList() = runBlocking { - /* launch(Dispatchers.Swing) { - val listModel = DefaultListModel() - var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE) - - indexMap.clear() - - for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) { - if(ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) { - listModel.addElement(pipeline.clazz.simpleName) - indexMap[selectorIndex] = managerIndex - - selectorIndex++ - } - } - - opModeSelector.fixedCellWidth = 240 - opModeSelector.model = listModel - revalAndRepaint() - }*/ } fun revalAndRepaint() { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt index 3987a033..9ae95f79 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt @@ -1,5 +1,6 @@ package com.github.serivesmejia.eocvsim.gui.dialog +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.Icons import com.github.serivesmejia.eocvsim.util.event.EventHandler import java.awt.* @@ -40,7 +41,7 @@ class SplashScreen(closeHandler: EventHandler? = null) : JDialog() { } class ImagePanel : JPanel(GridBagLayout()) { - val img = Icons.getImage("ico_eocvsim") + val img = EOCVSimIconLibrary.icoEOCVSim init { isOpaque = false diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt index 94f6e107..ffe4e950 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt @@ -1,5 +1,6 @@ package com.github.serivesmejia.eocvsim.gui.util.icon +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.Icons import com.github.serivesmejia.eocvsim.input.InputSourceManager import com.github.serivesmejia.eocvsim.pipeline.PipelineManager @@ -12,8 +13,8 @@ class PipelineListIconRenderer( private val pipelineManager: PipelineManager ) : DefaultListCellRenderer() { - private val gearsIcon by Icons.lazyGetImageResized("ico_gears", 15, 15) - private val hammerIcon by Icons.lazyGetImageResized("ico_hammer", 15, 15) + private val gearsIcon by EOCVSimIconLibrary.icoGears.lazyResized(15, 15) + private val hammerIcon by EOCVSimIconLibrary.icoHammer.lazyResized(15, 15) override fun getListCellRendererComponent( list: JList<*>, diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java index dd582038..d56f540a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java @@ -23,6 +23,7 @@ package com.github.serivesmejia.eocvsim.gui.util.icon; +import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary; import com.github.serivesmejia.eocvsim.gui.Icons; import com.github.serivesmejia.eocvsim.input.InputSourceManager; @@ -59,19 +60,19 @@ public Component getListCellRendererComponent( switch (sourceManager.getSourceType((String) value)) { case IMAGE: if(imageIcon == null) { - imageIcon = Icons.INSTANCE.getImageResized("ico_img", 15, 15); + imageIcon = EOCVSimIconLibrary.INSTANCE.getIcoImg().resized(15, 15); } label.setIcon(imageIcon); break; case CAMERA: if(camIcon == null) { - camIcon = Icons.INSTANCE.getImageResized("ico_cam", 15, 15); + camIcon = EOCVSimIconLibrary.INSTANCE.getIcoCam().resized(15, 15); } label.setIcon(camIcon); break; case VIDEO: if(vidIcon == null) { - vidIcon = Icons.INSTANCE.getImageResized("ico_vid", 15, 15); + vidIcon = EOCVSimIconLibrary.INSTANCE.getIcoVid().resized(15, 15); } label.setIcon(vidIcon); break; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 6c8e4bd0..b064fe2d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -107,7 +107,9 @@ public void update(boolean isPaused) { currentInputSource.setPaused(isPaused); Mat m = currentInputSource.update(); + if(m != null && !m.empty()) { + lastMatFromSource.release(); m.copyTo(lastMatFromSource); // add an extra alpha channel because that's what eocv returns for some reason... (more realistic simulation lol) Imgproc.cvtColor(lastMatFromSource, lastMatFromSource, Imgproc.COLOR_RGB2RGBA); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java index 155a807f..391f434a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java @@ -142,7 +142,6 @@ public void readImage() { @Override public Mat update() { - if (isPaused) return lastCloneTo; if (lastCloneTo == null) lastCloneTo = matRecycler.takeMat(); if (img == null) return null; 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 6b248321..2851c4d7 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 @@ -683,8 +683,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi } fun refreshGuiPipelineList() { - eocvSim.visualizer.pipelineSelectorPanel.updatePipelinesList() - eocvSim.visualizer.opModeSelectorPanel.updateOpModesList() + eocvSim.visualizer.pipelineOpModeSwitchablePanel.updateSelectorLists() } } diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index 4a3400e7..bdb58367 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -14,6 +14,8 @@ import org.openftc.easyopencv.OpenCvViewport enum class OpModeState { SELECTED, INIT, START, STOP, STOPPED } +enum class OpModeType { AUTONOMOUS, TELEOP } + class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private val viewport: OpenCvViewport) : SpecificPipelineHandler( { it is OpMode } ) { diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_arrow_dropdown.png b/EOCV-Sim/src/main/resources/images/icon/ico_arrow_dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba2fb502431448fdce483d973877c2897626c19 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}KRjI=Lo%G- zPLAX{6d=H&e*G)UyWeF@=e2q5IE0$^&05~^wm<&z%XMD6eteAo{`l`<=6$&r*m4+z zK4`NrJ}Q(O%>(KDy4x53KL;|ybg57q;9s#yLTN|@od=w58)pd7d`ihwrtgJ0q zzsBKA)~VNRd;!NKMFbiubAPU3JY^Xwa`6MJ=6uTscLg<{Zf4hb|AQ&?W>nCpr*@B} zE8Z1JXs&2nz2=iH&y>91jfPx%u}MThRA@u(na!(CQ5431_a6{*!ju6@`Is;@5hKb+6f%*Il$63mijav!xrEFx zF=pbU2m_`RbMhDHsrz2%ea}8;ueHzK_qKI+oqFqCd#&~C=d6#t&J>lesmlFl0l<7S zn3I8}z!qRVu%ht@CijiHEfAdko(f z9w?`vUun@{1#lBMW>J_?=EF=z>s4t1a0IxW(J_yzv%uxP4KV<_fF~Y{1!VUCkGu5t z0Z@8Wp3a?Zycm_8RA+)IwhzDs;JnvbA^D4d{AsN&0IINWG8Cy?cy#)kLKV9UfGXn& zqjjB5b4{|Ql&Icl=u!<*Ip^H6ULO!~VUI9Figs1f3& zzBy><;JgfsbX5I`sd*3@K^_LSItF2*{kq@21n$hoN4^l7YLx|KP2@A{=NZSx22`nW pmHG;$+RMIg0YLU6btPx&A4x<(RA@u(naitBQ545N*TB$YLU|NZ5=wcM$3S5qQmBi(Gx3Z(@|;K!GLgvR zhRiTAX5vwV0aJ=O`3v-|eXjHSoyXp5|MvOaPTf_%`VH1zYkl`xYp=%{pyf2sa{Czt zFw!1O>49m$a$o^4Bli==AGzNL;5G0o1^8Y8%mVfROMrz&gWdygfV;pqMSODXI zqreeB|9hdoz;)mnp#LU{0k91?0n9dO@-u%0t^yDItp7Qz0B!(>ED9oc1z>0BISYW7 zz?x9OJ@P5(XNJfK1#lBM5F#)k&qw#7)rt%Oun)MM&@hXrQ^4iA13mzofu|OY4e$;E zPpahB0nqcPcRKZJW5uY~Np+_5iLC>00XT2f))4WmB3U$$(K6xRD)FcJE#i4CtzMAfLFj|;52YBLPph*&%pc;0O};a zM=F*F1R$ez&Krv&<#iuZaZ`~SrZNxg02IqH)D1u~#scRYkTEerb7$0Ibo5GP0Ak}` zdddJ~`j-J`fK@$!DC9vIfM>w^NF{XuGLgp}keD|7Ik3SOLa_<#^FUtj99LsD8ZWAl z$Ymb*4NNc!&e%gdkQc3KPQ_B=XbAlQCi*`#5kji;y7^6khG%R+ zA=GpdtHP_V8E-UR>_JJ~)HAA%@+`1A!tm@!&@-z3`%$ElszP?sg-ox7Y%B2Cm&AGz zekKC2*7Zb7Bjt4;Q?hp~#HyOPcM*%Kd>xqNRxjEer6MiD3O!X704Px%kV!;ARA@u(nY(LMK@i4&`3FRM1#PuZLGaPU)<(24U=)QYHlhYW1VL>?P)tZ8 zK|v!bcG1RK8y}!(ir6Y>ulN_p2f6XydmcNpXYa{b$ijtN%+AcWv-8;RjFI^o%iMp~ z0jzZdQ$}DjFb?biw)Xvm@kih96Yv)Jl>&TI0Na5n-~h1K>Ci{u9q zfkRfs4ds;d+blY)0Oo-67KI6AKCe`?T9p<6XMy>Ij(JpF1#Z`E2mv?-yzp2Yf$VYM zd6nKe04k5F)2XYC7o)0^`b=qwtpjiaxbC$!BKezu{3)y|0P3*s6Evw*cy#*v%Q|)y z0CmQTM(aAACd(RS^&oZrfmH?I3$V-RRb_Y$`#sn6E3n%FK$GNmgYlk7TL5HF%}q^i zb(KZn6!55{05oC@%t~bdJojHX+l)XV56S>60!KO;ftSG1&=v9|aN7|mByw2cUfcqvF z;)Q~Is5b|OU6}x`jev3#g?Ouu2Q51jZv=Wevi`)>e-P@}XMlqaBCNMx@7p)PgSGg` zR~AhXVgj-=@R@aK#qj}xDmG { shouldPaintOrange = true diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java index 737930f3..c93c0588 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -261,12 +261,6 @@ public void render(Mat mat, Canvas canvas, OpenCvViewport.RenderHook userHook, O height = bitmapFromMat.getHeight(); aspectRatio = (float) width / height; - if (!offscreen) - { - //Draw the background each time to prevent double buffering problems - canvas.drawColor(RC_ACTIVITY_BG_COLOR); - } - // Cache current state, can change behind our backs OpenCvViewport.OptimizedRotation optimizedRotationSafe = optimizedViewRotation; From 2ff36778f3e26559ef1e88d07f30b4e821825f85 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 17 Aug 2023 17:58:40 -0600 Subject: [PATCH 23/46] OpMode controls fully functional in GUI --- .../serivesmejia/eocvsim/util/LogExt.kt | 0 .../eocvsim/util/event/EventHandler.kt | 292 +++++++++--------- .../eocvsim/util/event/EventListener.kt | 96 +++--- .../org/openftc/easyopencv/MatRecycler.java | 21 +- .../github/serivesmejia/eocvsim/gui/Icons.kt | 1 - .../serivesmejia/eocvsim/gui/Visualizer.java | 26 -- .../eocvsim/gui/component/PopupX.kt | 25 +- .../PipelineOpModeSwitchablePanel.kt | 8 +- .../visualizer/opmode/OpModeControlsPanel.kt | 104 ++++++- .../visualizer/opmode/OpModePopupPanel.kt | 14 +- .../visualizer/opmode/OpModeSelectorPanel.kt | 156 +++++++++- .../serivesmejia/eocvsim/gui/util/Enums.kt | 3 + .../eocvsim/gui/util/MatPosterImpl.java | 2 +- .../eocvsim/input/source/CameraSource.java | 18 +- .../eocvsim/input/source/ImageSource.java | 4 +- .../eocvsim/input/source/VideoSource.java | 12 +- .../eocvsim/pipeline/PipelineManager.kt | 18 +- .../pipeline/compiler/PipelineCompiler.kt | 2 - .../pipeline/handler/PipelineHandler.kt | 2 +- .../handler/SpecificPipelineHandler.kt | 6 +- .../util/exception/handling/CrashReport.kt | 3 - .../eocvsim/util/io/FileWatcher.kt | 1 - .../util/template/DefaultWorkspaceTemplate.kt | 1 - .../eventloop/opmode/OpModePipelineHandler.kt | 34 +- .../eocvsim/input/VisionInputSource.kt | 5 +- .../eocvsim/input/VisionInputSourceHander.kt | 14 +- .../external/samples/ConceptAprilTag.java | 1 + .../eventloop/opmode/LinearOpMode.java | 17 +- .../robotcore/eventloop/opmode/OpMode.java | 69 ++++- .../external/gui/SwingOpenCvViewport.kt | 14 +- .../external/source/VisionSourceBase.java | 37 ++- .../vision/external/util/FrameQueue.java | 4 +- .../deltacv/vision/internal/opmode/Enums.kt | 5 + .../vision/internal/opmode/OpModeNotifier.kt | 52 ++++ .../vision/internal/util/KillException.java | 4 + 35 files changed, 732 insertions(+), 339 deletions(-) rename {EOCV-Sim => Common}/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt (100%) rename {EOCV-Sim => Common}/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt (97%) rename {EOCV-Sim => Common}/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt (97%) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt create mode 100644 Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt create mode 100644 Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt create mode 100644 Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt similarity index 100% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt rename to Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt similarity index 97% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt rename to Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt index 9fb0062d..1a293fdf 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt +++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt @@ -1,146 +1,146 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.util.event - -import com.github.serivesmejia.eocvsim.util.loggerOf - -class EventHandler(val name: String) : Runnable { - - val logger by loggerOf("${name}-EventHandler") - - private val lock = Any() - private val onceLock = Any() - - val listeners: Array - get() { - synchronized(lock) { - return internalListeners.toTypedArray() - } - } - - val onceListeners: Array - get() { - synchronized(onceLock) { - return internalOnceListeners.toTypedArray() - } - } - - var callRightAway = false - - private val internalListeners = ArrayList() - private val internalOnceListeners = ArrayList() - - override fun run() { - for(listener in listeners) { - try { - runListener(listener, false) - } catch (ex: Exception) { - if(ex is InterruptedException) { - logger.warn("Rethrowing InterruptedException...") - throw ex - } else { - logger.error("Error while running listener ${listener.javaClass.name}", ex) - } - } - } - - val toRemoveOnceListeners = mutableListOf() - - //executing "doOnce" listeners - for(listener in onceListeners) { - try { - runListener(listener, true) - } catch (ex: Exception) { - if(ex is InterruptedException) { - logger.warn("Rethrowing InterruptedException...") - throw ex - } else { - logger.error("Error while running \"once\" ${listener.javaClass.name}", ex) - } - } - - toRemoveOnceListeners.add(listener) - } - - synchronized(onceLock) { - for(listener in toRemoveOnceListeners) { - internalOnceListeners.remove(listener) - } - } - } - - fun doOnce(listener: EventListener) { - if(callRightAway) - runListener(listener, true) - else synchronized(onceLock) { - internalOnceListeners.add(listener) - } - } - - fun doOnce(runnable: Runnable) = doOnce { runnable.run() } - - - fun doPersistent(listener: EventListener): EventListenerRemover { - synchronized(lock) { - internalListeners.add(listener) - } - - if(callRightAway) runListener(listener, false) - - return EventListenerRemover(this, listener, false) - } - - fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } - - fun removePersistentListener(listener: EventListener) { - if(internalListeners.contains(listener)) { - synchronized(lock) { internalListeners.remove(listener) } - } - } - - fun removeOnceListener(listener: EventListener) { - if(internalOnceListeners.contains(listener)) { - synchronized(onceLock) { internalOnceListeners.remove(listener) } - } - } - - fun removeAllListeners() { - removeAllPersistentListeners() - removeAllOnceListeners() - } - - fun removeAllPersistentListeners() = synchronized(lock) { - internalListeners.clear() - } - - fun removeAllOnceListeners() = synchronized(onceLock) { - internalOnceListeners.clear() - } - - operator fun invoke(listener: EventListener) = doPersistent(listener) - - private fun runListener(listener: EventListener, isOnce: Boolean) = - listener.run(EventListenerRemover(this, listener, isOnce)) - -} +/* + * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.util.event + +import com.github.serivesmejia.eocvsim.util.loggerOf + +class EventHandler(val name: String) : Runnable { + + val logger by loggerOf("${name}-EventHandler") + + private val lock = Any() + private val onceLock = Any() + + val listeners: Array + get() { + synchronized(lock) { + return internalListeners.toTypedArray() + } + } + + val onceListeners: Array + get() { + synchronized(onceLock) { + return internalOnceListeners.toTypedArray() + } + } + + var callRightAway = false + + private val internalListeners = ArrayList() + private val internalOnceListeners = ArrayList() + + override fun run() { + for(listener in listeners) { + try { + runListener(listener, false) + } catch (ex: Exception) { + if(ex is InterruptedException) { + logger.warn("Rethrowing InterruptedException...") + throw ex + } else { + logger.error("Error while running listener ${listener.javaClass.name}", ex) + } + } + } + + val toRemoveOnceListeners = mutableListOf() + + //executing "doOnce" listeners + for(listener in onceListeners) { + try { + runListener(listener, true) + } catch (ex: Exception) { + if(ex is InterruptedException) { + logger.warn("Rethrowing InterruptedException...") + throw ex + } else { + logger.error("Error while running \"once\" ${listener.javaClass.name}", ex) + } + } + + toRemoveOnceListeners.add(listener) + } + + synchronized(onceLock) { + for(listener in toRemoveOnceListeners) { + internalOnceListeners.remove(listener) + } + } + } + + fun doOnce(listener: EventListener) { + if(callRightAway) + runListener(listener, true) + else synchronized(onceLock) { + internalOnceListeners.add(listener) + } + } + + fun doOnce(runnable: Runnable) = doOnce { runnable.run() } + + + fun doPersistent(listener: EventListener): EventListenerRemover { + synchronized(lock) { + internalListeners.add(listener) + } + + if(callRightAway) runListener(listener, false) + + return EventListenerRemover(this, listener, false) + } + + fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } + + fun removePersistentListener(listener: EventListener) { + if(internalListeners.contains(listener)) { + synchronized(lock) { internalListeners.remove(listener) } + } + } + + fun removeOnceListener(listener: EventListener) { + if(internalOnceListeners.contains(listener)) { + synchronized(onceLock) { internalOnceListeners.remove(listener) } + } + } + + fun removeAllListeners() { + removeAllPersistentListeners() + removeAllOnceListeners() + } + + fun removeAllPersistentListeners() = synchronized(lock) { + internalListeners.clear() + } + + fun removeAllOnceListeners() = synchronized(onceLock) { + internalOnceListeners.clear() + } + + operator fun invoke(listener: EventListener) = doPersistent(listener) + + private fun runListener(listener: EventListener, isOnce: Boolean) = + listener.run(EventListenerRemover(this, listener, isOnce)) + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt similarity index 97% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt rename to Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt index 015d81e8..38544228 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt +++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt @@ -1,49 +1,49 @@ -/* - * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.util.event - -fun interface EventListener { - fun run(remover: EventListenerRemover) -} - -class EventListenerRemover( - val handler: EventHandler, - val listener: EventListener, - val isOnceListener: Boolean -) { - - private val attached = mutableListOf() - - fun removeThis() { - if(isOnceListener) - handler.removeOnceListener(listener) - else - handler.removePersistentListener(listener) - } - - fun attach(remover: EventListenerRemover) { - - } - +/* + * Copyright (c) 2021 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 com.github.serivesmejia.eocvsim.util.event + +fun interface EventListener { + fun run(remover: EventListenerRemover) +} + +class EventListenerRemover( + val handler: EventHandler, + val listener: EventListener, + val isOnceListener: Boolean +) { + + private val attached = mutableListOf() + + fun removeThis() { + if(isOnceListener) + handler.removeOnceListener(listener) + else + handler.removePersistentListener(listener) + } + + fun attach(remover: EventListenerRemover) { + + } + } \ No newline at end of file diff --git a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java index 30b7e4d2..6519bf04 100644 --- a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java +++ b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java @@ -54,18 +54,27 @@ public MatRecycler(int num) { this(num, 0, 0, CvType.CV_8UC3); } - public synchronized RecyclableMat takeMat() { + public synchronized RecyclableMat takeMatOrNull() { if (availableMats.size() == 0) { - throw new RuntimeException("All mats have been checked out!"); + return null; } - RecyclableMat mat = null; try { - mat = availableMats.take(); - mat.checkedOut = true; + return takeMatOrInterrupt(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + return null; } + } + + public synchronized RecyclableMat takeMatOrInterrupt() throws InterruptedException { + if(availableMats.size() == 0) { + throw new RuntimeException("All mats have been checked out!"); + } + + RecyclableMat mat; + + mat = availableMats.take(); + mat.checkedOut = true; return mat; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt index dbd863e6..76f864d8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt @@ -26,7 +26,6 @@ package com.github.serivesmejia.eocvsim.gui import com.github.serivesmejia.eocvsim.gui.util.GuiUtil import com.github.serivesmejia.eocvsim.util.loggerForThis import io.github.deltacv.vision.external.gui.util.ImgUtil -import java.awt.Image import java.awt.image.BufferedImage import java.util.NoSuchElementException import javax.swing.ImageIcon diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 05c138de..3eab4785 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -67,11 +67,7 @@ public class Visualizer { public TopMenuBar menuBar = null; public JPanel tunerMenuPanel = new JPanel(); - public JScrollPane imgScrollPane = null; - public JPanel rightContainer = null; - public JSplitPane globalSplitPane = null; - public JSplitPane imageTunerSplitPane = null; public PipelineOpModeSwitchablePanel pipelineOpModeSwitchablePanel = null; @@ -89,9 +85,6 @@ public class Visualizer { public ColorPicker colorPicker = null; - //stuff for zooming handling - private volatile boolean isCtrlPressed = false; - private volatile boolean hasFinishedInitializing = false; Logger logger = LoggerFactory.getLogger(getClass()); @@ -237,25 +230,6 @@ public void mouseClicked(MouseEvent e) { // } // }); - //listening for keyboard presses and releases, to check if ctrl key was pressed or released (handling zoom) - KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ke -> { - switch (ke.getID()) { - case KeyEvent.KEY_PRESSED: - if (ke.getKeyCode() == KeyEvent.VK_CONTROL) { - isCtrlPressed = true; - imgScrollPane.setWheelScrollingEnabled(false); //lock scrolling if ctrl is pressed - } - break; - case KeyEvent.KEY_RELEASED: - if (ke.getKeyCode() == KeyEvent.VK_CONTROL) { - isCtrlPressed = false; - imgScrollPane.setWheelScrollingEnabled(true); //unlock - } - break; - } - return false; //idk let's just return false 'cause keyboard input doesn't work otherwise - }); - //resizes all three JLists in right panel to make buttons visible in smaller resolutions frame.addComponentListener(new ComponentAdapter() { @Override diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt index 166eb5b7..d66c1344 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt @@ -23,6 +23,7 @@ package com.github.serivesmejia.eocvsim.gui.component +import com.github.serivesmejia.eocvsim.gui.util.Location import com.github.serivesmejia.eocvsim.util.event.EventHandler import java.awt.Window import java.awt.event.KeyAdapter @@ -97,6 +98,7 @@ class PopupX @JvmOverloads constructor(windowAncestor: Window, companion object { fun JComponent.popUpXOnThis(panel: JPanel, + popupLocation: Location = Location.TOP, closeOnFocusLost: Boolean = true, fixX: Boolean = false, fixY: Boolean = true): PopupX { @@ -104,7 +106,28 @@ class PopupX @JvmOverloads constructor(windowAncestor: Window, val frame = SwingUtilities.getWindowAncestor(this) val location = locationOnScreen - return PopupX(frame, panel, location.x, location.y, closeOnFocusLost, fixX, fixY) + val popup = PopupX(frame, panel, location.x, + if(popupLocation == Location.TOP) location.y else location.y + height, + closeOnFocusLost, fixX, fixY + ) + + popup.onShow { + popup.setLocation( + popup.window.location.x - width / 3, + if(popupLocation == Location.TOP) popup.window.location.y else popup.window.location.y + popup.window.height + ) + + val topRightPointX = popup.window.location.x + popup.window.width + + if(topRightPointX > frame.width) { + popup.setLocation( + popup.window.location.x - ((topRightPointX - frame.width) / 2), + popup.window.location.y + ) + } + } + + return popup } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 76197c1d..0201ed19 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -20,8 +20,8 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val opModePanel = JPanel() - val opModeSelectorPanel = OpModeSelectorPanel(eocvSim) - val opModeControlsPanel = OpModeControlsPanel() + val opModeControlsPanel = OpModeControlsPanel(eocvSim) + val opModeSelectorPanel = OpModeSelectorPanel(eocvSim, opModeControlsPanel) private var beforeAllowPipelineSwitching: Boolean? = null @@ -51,9 +51,13 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { if(index == 0 && beforeAllowPipelineSwitching != null) { pipelineSelectorPanel.allowPipelineSwitching = beforeAllowPipelineSwitching!! + + eocvSim.pipelineManager.requestChangePipeline(0) } else if(index == 1) { beforeAllowPipelineSwitching = pipelineSelectorPanel.allowPipelineSwitching pipelineSelectorPanel.allowPipelineSwitching = false + + opModeSelectorPanel.reset() } } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt index 9a663254..92b69a49 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -1,6 +1,11 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode +import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.qualcomm.robotcore.eventloop.opmode.OpMode +import io.github.deltacv.vision.internal.opmode.OpModeNotification +import io.github.deltacv.vision.internal.opmode.OpModeState import java.awt.BorderLayout import javax.swing.JPanel import java.awt.GridBagConstraints @@ -8,34 +13,105 @@ import java.awt.GridBagLayout import javax.swing.JButton import javax.swing.SwingUtilities -class OpModeControlsPanel : JPanel() { +class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { val controlButton = JButton() + private var currentManagerIndex: Int? = null + init { layout = BorderLayout() - SwingUtilities.invokeLater { - updateControlButtonIcon() + add(controlButton, BorderLayout.CENTER) + + controlButton.isEnabled = false + controlButton.icon = EOCVSimIconLibrary.icoNotStarted + + controlButton.addActionListener { + eocvSim.pipelineManager.onUpdate.doOnce { + if(eocvSim.pipelineManager.currentPipeline !is OpMode) return@doOnce + + val opMode = eocvSim.pipelineManager.currentPipeline as OpMode + val state = opMode.notifier.state + + opMode.notifier.notify(when(state) { + OpModeState.SELECTED -> OpModeNotification.INIT + OpModeState.INIT -> OpModeNotification.START + OpModeState.START -> OpModeNotification.STOP + else -> OpModeNotification.NOTHING + }) + } } + } + + fun stopCurrentOpMode() { + if(eocvSim.pipelineManager.currentPipeline !is OpMode) return + + val opMode = eocvSim.pipelineManager.currentPipeline as OpMode + opMode.notifier.notify(OpModeNotification.STOP) + } - controlButton.addComponentListener(object : java.awt.event.ComponentAdapter() { - override fun componentResized(evt: java.awt.event.ComponentEvent?) { - updateControlButtonIcon() + private fun notifySelected() { + if(eocvSim.pipelineManager.currentPipeline !is OpMode) return + val opMode = eocvSim.pipelineManager.currentPipeline as OpMode + + opMode.notifier.onStateChange { + val state = opMode.notifier.state + + SwingUtilities.invokeLater { + updateButtonState(state) } - }) - add(controlButton, BorderLayout.CENTER) + if(state == OpModeState.STOPPED) { + opModeSelected(currentManagerIndex!!) + } + } + + opMode.notifier.notify(OpModeState.SELECTED) + } + + private fun updateButtonState(state: OpModeState) { + when(state) { + OpModeState.SELECTED -> controlButton.isEnabled = true + OpModeState.INIT -> controlButton.icon = EOCVSimIconLibrary.icoPlay + OpModeState.START -> controlButton.icon = EOCVSimIconLibrary.icoStop + OpModeState.STOP -> { + controlButton.isEnabled = false + } + OpModeState.STOPPED -> { + controlButton.isEnabled = true + + controlButton.icon = EOCVSimIconLibrary.icoNotStarted + } + } } - fun updateControlButtonIcon() { - val size = controlButton.size - val width = size.width - val height = size.height + fun opModeSelected(managerIndex: Int) { + if (!eocvSim.pipelineManager.paused) { + eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) + currentManagerIndex = managerIndex + + eocvSim.pipelineManager.onUpdate.doOnce { + notifySelected() + } + } else { + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + controlButton.isEnabled = false + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) + currentManagerIndex = managerIndex - if(width <= 0 || height <= 0) return + eocvSim.pipelineManager.onUpdate.doOnce { + notifySelected() + } + } + } + } - controlButton.icon = EOCVSimIconLibrary.icoNotStarted.scaleToFit(width, height) + fun reset() { + controlButton.isEnabled = false + controlButton.icon = EOCVSimIconLibrary.icoNotStarted } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt index b3045341..0de78203 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt @@ -1,12 +1,22 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode import javax.swing.JButton +import javax.swing.JList import javax.swing.JPanel +import javax.swing.JScrollPane -class OpModePopupPanel : JPanel() { +class OpModePopupPanel(autonomousSelector: JList<*>) : JPanel() { init { - add(JButton("Test")) + val scroll = JScrollPane() + + scroll.setViewportView(autonomousSelector) + scroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS + scroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + + add(scroll) + + autonomousSelector.selectionModel.clearSelection() } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index a2db088d..877596f4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -3,7 +3,10 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnThis -import com.qualcomm.robotcore.eventloop.opmode.OpModeType +import com.github.serivesmejia.eocvsim.gui.util.Location +import com.github.serivesmejia.eocvsim.util.ReflectUtil +import com.qualcomm.robotcore.eventloop.opmode.* +import com.qualcomm.robotcore.util.Range import kotlinx.coroutines.runBlocking import java.awt.GridBagConstraints import java.awt.GridBagLayout @@ -11,28 +14,41 @@ import java.awt.Insets import javax.swing.* -class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { +class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeControlsPanel) : JPanel() { var selectedIndex = -1 set(value) { field = value } - // - private val indexMap = mutableMapOf() + // + private val autonomousIndexMap = mutableMapOf() + // + private val teleopIndexMap = mutableMapOf() val autonomousButton = JButton() + val selectOpModeLabelsPanel = JPanel() + val opModeNameLabelPanel = JPanel() + val textPanel = JPanel() + val opModeNameLabel = JLabel("") + val selectOpModeLabel = JLabel("Select Op Mode") val buttonDescriptorLabel = JLabel("<- Autonomous | TeleOp ->") val teleopButton = JButton() + val autonomousSelector = JList() + val teleopSelector = JList() + init { layout = GridBagLayout() - textPanel.layout = GridBagLayout() + selectOpModeLabelsPanel.layout = GridBagLayout() + + autonomousSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION + teleopSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION autonomousButton.icon = EOCVSimIconLibrary.icoArrowDropdown @@ -41,6 +57,9 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { gridy = 0 ipady = 20 + weightx = 1.0 + anchor = GridBagConstraints.WEST + gridheight = 1 }) @@ -50,18 +69,22 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { buttonDescriptorLabel.horizontalTextPosition = JLabel.CENTER buttonDescriptorLabel.horizontalAlignment = JLabel.CENTER - textPanel.add(selectOpModeLabel, GridBagConstraints().apply { + selectOpModeLabelsPanel.add(selectOpModeLabel, GridBagConstraints().apply { gridx = 0 gridy = 0 ipady = 0 }) - textPanel.add(buttonDescriptorLabel, GridBagConstraints().apply { + selectOpModeLabelsPanel.add(buttonDescriptorLabel, GridBagConstraints().apply { gridx = 0 gridy = 1 ipadx = 10 }) + textPanel.add(selectOpModeLabelsPanel) + + opModeNameLabelPanel.add(opModeNameLabel) + add(textPanel, GridBagConstraints().apply { gridx = 1 gridy = 0 @@ -75,6 +98,9 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { gridy = 0 ipady = 20 + weightx = 1.0 + anchor = GridBagConstraints.EAST + gridheight = 1 }) @@ -83,19 +109,123 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim) : JPanel() { private fun registerListeners() { autonomousButton.addActionListener { - autonomousButton.popUpXOnThis(OpModePopupPanel()).show() + val popup = autonomousButton.popUpXOnThis(OpModePopupPanel(autonomousSelector), Location.BOTTOM) + + opModeControlsPanel.stopCurrentOpMode() + + val listSelectionListener = object : javax.swing.event.ListSelectionListener { + override fun valueChanged(e: javax.swing.event.ListSelectionEvent?) { + if(!e!!.valueIsAdjusting) { + popup.hide() + autonomousSelector.removeListSelectionListener(this) + } + } + } + + autonomousSelector.addListSelectionListener(listSelectionListener) + + popup.show() + } + + teleopButton.addActionListener { + val popup = teleopButton.popUpXOnThis(OpModePopupPanel(teleopSelector), Location.BOTTOM) + + opModeControlsPanel.stopCurrentOpMode() + + val listSelectionListener = object : javax.swing.event.ListSelectionListener { + override fun valueChanged(e: javax.swing.event.ListSelectionEvent?) { + if(!e!!.valueIsAdjusting) { + popup.hide() + teleopSelector.removeListSelectionListener(this) + } + } + } + + teleopSelector.addListSelectionListener(listSelectionListener) + + popup.show() + } + + autonomousSelector.addListSelectionListener { + if(!it.valueIsAdjusting) { + val index = autonomousSelector.selectedIndex + if(index != -1) { + autonomousSelected(index) + } + } } + + teleopSelector.addListSelectionListener { + if(!it.valueIsAdjusting) { + val index = teleopSelector.selectedIndex + if(index != -1) { + teleOpSelected(index) + } + } + } + } + + private fun teleOpSelected(index: Int) { + opModeSelected(teleopIndexMap[index]!!, teleopSelector.selectedValue!!) + } + + private fun autonomousSelected(index: Int) { + opModeSelected(autonomousIndexMap[index]!!, autonomousSelector.selectedValue!!) + } + + private fun opModeSelected(managerIndex: Int, name: String) { + opModeNameLabel.text = name + + textPanel.removeAll() + textPanel.add(opModeNameLabelPanel) + + opModeControlsPanel.opModeSelected(managerIndex) } fun updateOpModesList() = runBlocking { + val autonomousListModel = DefaultListModel() + val teleopListModel = DefaultListModel() + var autonomousSelectorIndex = Range.clip(autonomousListModel.size() - 1, 0, Int.MAX_VALUE) + var teleopSelectorIndex = Range.clip(teleopListModel.size() - 1, 0, Int.MAX_VALUE) + + autonomousIndexMap.clear() + teleopIndexMap.clear() + + for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) { + if(ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) { + val type = pipeline.clazz.opModeType + + if(type == OpModeType.AUTONOMOUS) { + val autonomousAnnotation = pipeline.clazz.autonomousAnnotation + + autonomousListModel.addElement(autonomousAnnotation.name) + autonomousIndexMap[autonomousSelectorIndex] = managerIndex + autonomousSelectorIndex++ + } else if(type == OpModeType.TELEOP) { + val teleopAnnotation = pipeline.clazz.teleopAnnotation + + teleopListModel.addElement(teleopAnnotation.name) + teleopIndexMap[teleopSelectorIndex] = managerIndex + teleopSelectorIndex++ + } + } + } + + autonomousSelector.fixedCellWidth = 240 + autonomousSelector.model = autonomousListModel + + teleopSelector.fixedCellWidth = 240 + teleopSelector.model = teleopListModel } - fun revalAndRepaint() { - /* opModeSelector.revalidate() - opModeSelector.repaint() - opModeSelectorScroll.revalidate() - opModeSelectorScroll.repaint() */ + fun reset() { + textPanel.removeAll() + textPanel.add(selectOpModeLabelsPanel) + + eocvSim.pipelineManager.requestChangePipeline(null) + + opModeControlsPanel.reset() } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt new file mode 100644 index 00000000..5dc84534 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt @@ -0,0 +1,3 @@ +package com.github.serivesmejia.eocvsim.gui.util + +enum class Location { TOP, BOTTOM } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java index 62cca8f7..86b65e18 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java @@ -91,7 +91,7 @@ public void post(Mat m, Object context) { evict(postQueue.poll()); } - MatRecycler.RecyclableMat recycledMat = matRecycler.takeMat(); + MatRecycler.RecyclableMat recycledMat = matRecycler.takeMatOrNull(); m.copyTo(recycledMat); postQueue.offer(recycledMat); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java index f822e253..3998454a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java @@ -151,7 +151,7 @@ public boolean init() { } if (matRecycler == null) matRecycler = new MatRecycler(4); - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull(); camera.read(newFrame); @@ -187,8 +187,15 @@ public void close() { currentWebcamIndex = -1; } + private MatRecycler.RecyclableMat lastNewFrame = null; + @Override public Mat update() { + if(lastNewFrame != null) { + lastNewFrame.returnMat(); + lastNewFrame = null; + } + if (isPaused) { return lastFramePaused; } else if (lastFramePaused != null) { @@ -197,10 +204,11 @@ public Mat update() { lastFramePaused = null; } - if (lastFrame == null) lastFrame = matRecycler.takeMat(); + if (lastFrame == null) lastFrame = matRecycler.takeMatOrNull(); if (camera == null) return lastFrame; - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull(); + lastNewFrame = newFrame; camera.read(newFrame); capTimeNanos = System.nanoTime(); @@ -219,13 +227,15 @@ public Mat update() { newFrame.release(); newFrame.returnMat(); + lastNewFrame = null; + return lastFrame; } @Override public void onPause() { if (lastFrame != null) lastFrame.release(); - if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); + if (lastFramePaused == null) lastFramePaused = matRecycler.takeMatOrNull(); camera.read(lastFramePaused); Imgproc.cvtColor(lastFramePaused, lastFramePaused, Imgproc.COLOR_BGR2RGB); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java index 391f434a..049b64bd 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java @@ -121,7 +121,7 @@ public void readImage() { Mat readMat = Imgcodecs.imread(this.imgPath); - if (img == null) img = matRecycler.takeMat(); + if (img == null) img = matRecycler.takeMatOrNull(); if (readMat.empty()) { return; @@ -142,7 +142,7 @@ public void readImage() { @Override public Mat update() { - if (lastCloneTo == null) lastCloneTo = matRecycler.takeMat(); + if (lastCloneTo == null) lastCloneTo = matRecycler.takeMatOrNull(); if (img == null) return null; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index b310f1be..27021ef9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -86,7 +86,7 @@ public boolean init() { if (matRecycler == null) matRecycler = new MatRecycler(4); - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull(); newFrame.release(); video.read(newFrame); @@ -123,15 +123,13 @@ public void reset() { @Override public void close() { - if (video != null && video.isOpened()) video.release(); - if (lastFrame != null) lastFrame.returnMat(); + if (lastFrame != null && lastFrame.isCheckedOut()) lastFrame.returnMat(); if (lastFramePaused != null) { lastFramePaused.returnMat(); lastFramePaused = null; } - } @Override @@ -149,10 +147,10 @@ public Mat update() { Thread.currentThread().interrupt(); } - if (lastFrame == null) lastFrame = matRecycler.takeMat(); + if (lastFrame == null) lastFrame = matRecycler.takeMatOrNull(); if (video == null) return lastFrame; - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull(); video.read(newFrame); capTimeNanos = System.nanoTime(); @@ -182,7 +180,7 @@ public Mat update() { @Override public void onPause() { if (lastFrame != null) lastFrame.release(); - if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); + if (lastFramePaused == null) lastFramePaused = matRecycler.takeMatOrNull(); video.read(lastFramePaused); 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 2851c4d7..12b9f1f1 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 @@ -86,6 +86,9 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi private set var previousPipelineIndex = 0 + @Volatile var previousPipeline: OpenCvPipeline? = null + private set + val activePipelineContexts = ArrayList() private var currentPipelineContext: ExecutorCoroutineDispatcher? = null @@ -196,7 +199,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi if(currentPipeline != null) { for (pipelineHandler in pipelineHandlers) { - pipelineHandler.onChange(currentPipeline!!, currentTelemetry!!) + pipelineHandler.onChange(previousPipeline, currentPipeline!!, currentTelemetry!!) } } } @@ -497,7 +500,17 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi fun forceChangePipeline(index: Int?, applyLatestSnapshot: Boolean = false, applyStaticSnapshot: Boolean = false) { - if(index == null) return + if(index == null) { + previousPipelineIndex = currentPipelineIndex + + currentPipeline = null + currentPipelineIndex = -1 + + onPipelineChange.run() + logger.info("Set to null pipeline") + + return + } captureSnapshot() @@ -544,6 +557,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi } previousPipelineIndex = currentPipelineIndex + previousPipeline = currentPipeline currentPipeline = nextPipeline currentPipelineData = pipelines[index] diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt index df8676dd..b25fd31c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt @@ -26,10 +26,8 @@ package com.github.serivesmejia.eocvsim.pipeline.compiler import com.github.serivesmejia.eocvsim.util.* import com.github.serivesmejia.eocvsim.util.compiler.JarPacker import com.github.serivesmejia.eocvsim.util.compiler.compiler -import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler import java.io.File import java.io.PrintWriter -import java.lang.reflect.Field import java.nio.charset.Charset import java.util.* import javax.tools.* diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt index 8cbb1560..3fabde34 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt @@ -12,6 +12,6 @@ interface PipelineHandler { fun processFrame(currentInputSource: InputSource?) - fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) + fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt index 6386c286..66755f9f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt @@ -14,9 +14,9 @@ abstract class SpecificPipelineHandler( private set @Suppress("UNCHECKED_CAST") - override fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) { - if(typeChecker(pipeline)) { - this.pipeline = pipeline as P + override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) { + if(typeChecker(newPipeline)) { + this.pipeline = newPipeline as P this.telemetry = telemetry } else { this.pipeline = null diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt index 16bd52ea..4eee1641 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt @@ -30,10 +30,7 @@ import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.extension.plus import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder import com.github.serivesmejia.eocvsim.util.loggerForThis -import java.io.BufferedWriter import java.io.File -import java.io.FileWriter -import java.nio.CharBuffer import java.time.LocalDateTime import java.time.format.DateTimeFormatter diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt index 26651a40..8a690357 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt @@ -28,7 +28,6 @@ import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.loggerOf import org.slf4j.Logger import java.io.File -import java.util.* class FileWatcher(private val watchingDirectories: List, watchingFileExtensions: List?, diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt index b1009550..48037d28 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt @@ -25,7 +25,6 @@ package com.github.serivesmejia.eocvsim.workspace.util.template import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.loggerForThis -import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate import net.lingala.zip4j.ZipFile import java.io.File diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index bdb58367..d0fc8983 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -12,8 +12,6 @@ import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.OpenCvViewport -enum class OpModeState { SELECTED, INIT, START, STOP, STOPPED } - enum class OpModeType { AUTONOMOUS, TELEOP } class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private val viewport: OpenCvViewport) : SpecificPipelineHandler( @@ -23,9 +21,10 @@ class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private private val onStop = EventHandler("OpModePipelineHandler-onStop") override fun preInit() { - inputSourceManager.setInputSource(inputSourceManager.defaultInputSource) + if(pipeline == null) return - ThreadSourceHander.register(VisionInputSourceHander(onStop, viewport)) + inputSourceManager.setInputSource(inputSourceManager.defaultInputSource) + ThreadSourceHander.register(VisionInputSourceHander(pipeline!!.notifier, viewport)) pipeline?.telemetry = telemetry pipeline?.hardwareMap = HardwareMap() @@ -33,15 +32,30 @@ class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private override fun init() { } - override fun processFrame(currentInputSource: InputSource?) { } - override fun onChange(pipeline: OpenCvPipeline, telemetry: Telemetry) { - this.pipeline?.requestOpModeStop() - onStop.run() + override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) { + if(beforePipeline is OpMode && beforePipeline == pipeline) { + beforePipeline.requestOpModeStop() + onStop.run() + } - super.onChange(pipeline, telemetry) + super.onChange(beforePipeline, newPipeline, telemetry) } -} \ No newline at end of file +} + +val Class<*>.opModeType get() = when { + this.autonomousAnnotation != null -> OpModeType.AUTONOMOUS + this.teleopAnnotation != null -> OpModeType.TELEOP + else -> null +} + +val OpMode.opModeType get() = this.javaClass.opModeType + +val Class<*>.autonomousAnnotation get() = this.getAnnotation(Autonomous::class.java) +val Class<*>.teleopAnnotation get() = this.getAnnotation(TeleOp::class.java) + +val OpMode.autonomousAnnotation get() = this.javaClass.autonomousAnnotation +val OpMode.teleopAnnotation get() = this.javaClass.teleopAnnotation \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt index c0ac643d..32255731 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt @@ -2,9 +2,7 @@ package io.github.deltacv.eocvsim.input import com.github.serivesmejia.eocvsim.input.InputSource import com.github.serivesmejia.eocvsim.util.loggerForThis -import io.github.deltacv.vision.external.source.VisionSource import io.github.deltacv.vision.external.source.VisionSourceBase -import io.github.deltacv.vision.external.source.VisionSourced import io.github.deltacv.vision.external.util.Timestamped import org.opencv.core.Mat import org.opencv.core.Size @@ -36,14 +34,13 @@ class VisionInputSource( return true; } - private val emptyMat = Mat(); + private val emptyMat = Mat() override fun pullFrame(): Timestamped { try { val frame = inputSource.update(); return Timestamped(frame, inputSource.captureTimeNanos) } catch(e: Exception) { - logger.warn("Exception thrown while pulling frame from input source", e) return Timestamped(emptyMat, 0) } } diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt index 65a35306..5b553af0 100644 --- a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt +++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt @@ -8,6 +8,8 @@ import com.qualcomm.robotcore.eventloop.opmode.OpMode import io.github.deltacv.vision.external.source.ViewportAndSourceHander import io.github.deltacv.vision.external.source.VisionSource import io.github.deltacv.vision.external.source.VisionSourceHander +import io.github.deltacv.vision.internal.opmode.OpModeNotifier +import io.github.deltacv.vision.internal.opmode.OpModeState import org.opencv.core.Mat import org.opencv.core.Size import org.opencv.videoio.VideoCapture @@ -18,7 +20,7 @@ import java.lang.IllegalArgumentException import java.net.URLConnection import javax.imageio.ImageIO -class VisionInputSourceHander(val stopNotifier: EventHandler, val viewport: OpenCvViewport) : ViewportAndSourceHander { +class VisionInputSourceHander(val notifier: OpModeNotifier, val viewport: OpenCvViewport) : ViewportAndSourceHander { private fun isImage(path: String) = try { ImageIO.read(File(path)) != null @@ -52,8 +54,14 @@ class VisionInputSourceHander(val stopNotifier: EventHandler, val viewport: Open CameraSource(index, Size(640.0, 480.0)) }) - stopNotifier.doOnce { - source.stop() + notifier.onStateChange { + when(notifier.state) { + OpModeState.STOPPED -> { + source.stop() + it.removeThis() + } + else -> {} + } } return source diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java index 30e040a5..86b802fe 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java @@ -75,6 +75,7 @@ public void runOpMode() { telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); telemetry.addData(">", "Touch Play to start OpMode"); telemetry.update(); + waitForStart(); if (opModeIsActive()) { diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java index c89a49c8..01559735 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java @@ -1,6 +1,8 @@ package com.qualcomm.robotcore.eventloop.opmode; import io.github.deltacv.vision.external.source.ThreadSourceHander; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public abstract class LinearOpMode extends OpMode { protected final Object lock = new Object(); @@ -153,17 +155,24 @@ public final void stop() { /* * Get out of dodge. Been here, done this. */ - if(stopRequested) { return; } + if(stopRequested) { return; } stopRequested = true; helper.interrupt(); + + try { + helper.join(); + } catch (InterruptedException ignored) { + } } private static class LinearOpModeHelperThread extends Thread { LinearOpMode opMode; + static Logger logger = LoggerFactory.getLogger(LinearOpModeHelperThread.class); + public LinearOpModeHelperThread(LinearOpMode opMode) { super("Thread-LinearOpModeHelper-" + opMode.getClass().getSimpleName()); @@ -172,15 +181,21 @@ public LinearOpModeHelperThread(LinearOpMode opMode) { @Override public void run() { + logger.info("{}: starting", opMode.getClass().getSimpleName()); + try { opMode.runOpMode(); + Thread.sleep(0); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + logger.info("{}: interrupted", opMode.getClass().getSimpleName()); } catch (RuntimeException e) { synchronized (opMode.lock) { opMode.catchedException = e; } } + + logger.info("{}: stopped", opMode.getClass().getSimpleName()); } } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java index 990103c3..fcd8c3e7 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -33,14 +33,23 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.hardware.HardwareMap; import io.github.deltacv.vision.external.util.FrameQueue; +import io.github.deltacv.vision.internal.opmode.OpModeNotification; +import io.github.deltacv.vision.internal.opmode.OpModeNotifier; +import io.github.deltacv.vision.internal.opmode.OpModeState; import org.openftc.easyopencv.TimestampedOpenCvPipeline; import org.firstinspires.ftc.robotcore.external.Telemetry; import org.opencv.core.Mat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public abstract class OpMode extends TimestampedOpenCvPipeline { // never in my life would i have imagined... + private Logger logger = LoggerFactory.getLogger(OpMode.class); + public Telemetry telemetry; + public OpModeNotifier notifier = new OpModeNotifier(); + volatile boolean isStarted = false; volatile boolean stopRequested = false; @@ -100,29 +109,67 @@ public OpMode() { public void stop() {}; // normally called by OpModePipelineHandler public void requestOpModeStop() { - stop(); + notifier.notify(OpModeNotification.STOP); } /* BEGIN OpenCvPipeline Impl */ @Override public final void init(Mat mat) { - init(); - telemetry.update(); } - private boolean startCalled = false; - @Override public final Mat processFrame(Mat input, long captureTimeNanos) { - if(!startCalled) { - start(); - startCalled = true; - telemetry.update(); + OpModeNotification notification = notifier.poll(); + + if(notification != OpModeNotification.NOTHING) { + logger.info("OpModeNotification: {}, OpModeState: {}", notification, notifier.getState()); } - loop(); - telemetry.update(); + switch(notification) { + case INIT: + if(notifier.getState() == OpModeState.START) break; + + init(); + notifier.notify(OpModeState.INIT); + break; + case START: + if(notifier.getState() == OpModeState.STOP || notifier.getState() == OpModeState.STOPPED) break; + + start(); + notifier.notify(OpModeState.START); + break; + case STOP: + notifier.notify(OpModeState.STOP); + stop(); + notifier.notify(OpModeState.STOPPED); + break; + case NOTHING: + break; + } + + OpModeState state = notifier.getState(); + + switch(state) { + case SELECTED: + case STOP: + case STOPPED: + break; + case INIT: + init_loop(); + + if(!(this instanceof LinearOpMode)) { + telemetry.update(); + } + break; + case START: + loop(); + + if(!(this instanceof LinearOpMode)) { + telemetry.update(); + } + break; + } return null; // OpModes don't actually show anything to the viewport, we'll delegate that to OpenCvCamera-s } diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt index dc38dc21..846c1897 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt @@ -22,17 +22,14 @@ */ package io.github.deltacv.vision.external.gui -import android.graphics.Bitmap import android.graphics.Canvas -import io.github.deltacv.common.image.DynamicBufferedImageRecycler import io.github.deltacv.common.image.MatPoster +import io.github.deltacv.vision.internal.util.KillException import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue import org.jetbrains.skia.Color -import org.jetbrains.skiko.ExperimentalSkikoApi import org.jetbrains.skiko.GenericSkikoView import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkikoView -import org.jetbrains.skiko.swing.SkiaSwingLayer import org.opencv.core.Mat import org.opencv.core.Size import org.openftc.easyopencv.MatRecycler @@ -42,13 +39,11 @@ import org.openftc.easyopencv.OpenCvViewport import org.openftc.easyopencv.OpenCvViewport.OptimizedRotation import org.openftc.easyopencv.OpenCvViewport.RenderHook import org.slf4j.LoggerFactory -import java.awt.GridBagLayout -import java.awt.image.BufferedImage import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.TimeUnit import javax.swing.JComponent -import javax.swing.JPanel import javax.swing.SwingUtilities +import kotlin.jvm.Throws class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Vision") : OpenCvViewport, MatPoster { @@ -179,6 +174,7 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi } override fun setRecording(recording: Boolean) {} + override fun post(mat: Mat, userContext: Any) { synchronized(syncObj) { //did they give us null? @@ -201,7 +197,7 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi * instead of doing a new alloc and then having * to free it after rendering/eviction from queue */ - val matToCopyTo = framebufferRecycler!!.takeMat() + val matToCopyTo = framebufferRecycler!!.takeMatOrInterrupt() mat.copyTo(matToCopyTo) matToCopyTo.context = userContext @@ -288,7 +284,7 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi private fun renderCanvas(canvas: Canvas) { if(!::lastFrame.isInitialized) { - lastFrame = framebufferRecycler!!.takeMat() + lastFrame = framebufferRecycler!!.takeMatOrNull() } synchronized(canvasLock) { diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java index 298d23c6..6147cdaa 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java @@ -1,10 +1,14 @@ package io.github.deltacv.vision.external.source; import io.github.deltacv.vision.external.util.Timestamped; +import io.github.deltacv.vision.internal.util.KillException; import org.opencv.core.Mat; import org.opencv.core.Size; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Arrays; public abstract class VisionSourceBase implements VisionSource { @@ -41,13 +45,9 @@ public boolean remove(VisionSourced sourced) { @Override public final boolean stop() { - helperThread.interrupt(); + if(!helperThread.isAlive() || helperThread.isInterrupted()) return false; - for(VisionSourced sourced : sourceds) { - synchronized (sourced) { - sourced.stop(); - } - } + helperThread.interrupt(); return stopSource(); } @@ -69,29 +69,40 @@ private Timestamped pullFrameInternal() { private static class SourceBaseHelperThread extends Thread { VisionSourceBase sourceBase; + boolean shouldStop = false; + + Logger logger; public SourceBaseHelperThread(VisionSourceBase sourcedBase) { super("Thread-SourceBaseHelper-" + sourcedBase.getClass().getSimpleName()); + logger = LoggerFactory.getLogger(getName()); this.sourceBase = sourcedBase; } + @Override public void run() { - while (!isInterrupted()) { - Timestamped frame = sourceBase.pullFrameInternal(); + VisionSourced[] sourceds = new VisionSourced[0]; - VisionSourced[] sourceds; + logger.info("starting"); + + while (!isInterrupted() && !shouldStop) { + Timestamped frame = sourceBase.pullFrameInternal(); synchronized (sourceBase.lock) { sourceds = sourceBase.sourceds.toArray(new VisionSourced[0]); } - for(VisionSourced sourced : sourceds) { - synchronized (sourced) { - sourced.onNewFrame(frame.getValue(), frame.getTimestamp()); - } + for (VisionSourced sourced : sourceds) { + sourced.onNewFrame(frame.getValue(), frame.getTimestamp()); } } + + for(VisionSourced sourced : sourceds) { + sourced.stop(); + } + + logger.info("stop"); } } diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java index ce909b5d..270b8d59 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java @@ -19,7 +19,7 @@ public FrameQueue(int maxQueueItems) { } public Mat takeMatAndPost() { - Mat mat = matRecycler.takeMat(); + Mat mat = matRecycler.takeMatOrNull(); viewportQueue.add(mat); return mat; @@ -27,7 +27,7 @@ public Mat takeMatAndPost() { public Mat takeMat() { - return matRecycler.takeMat(); + return matRecycler.takeMatOrNull(); } public Mat poll() { diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt new file mode 100644 index 00000000..aef5400b --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt @@ -0,0 +1,5 @@ +package io.github.deltacv.vision.internal.opmode + +enum class OpModeNotification { INIT, START, STOP, NOTHING } + +enum class OpModeState { SELECTED, INIT, START, STOP, STOPPED } \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt new file mode 100644 index 00000000..bdfaaf42 --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt @@ -0,0 +1,52 @@ +package io.github.deltacv.vision.internal.opmode + +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue +import java.util.concurrent.ArrayBlockingQueue + +class OpModeNotifier { + + val notifications = EvictingBlockingQueue(ArrayBlockingQueue(10)) + + private val stateLock = Any() + var state = OpModeState.STOPPED + private set + get() { + synchronized(stateLock) { + return field + } + } + + val onStateChange = EventHandler("OpModeNotifier-onStateChange") + + fun notify(notification: OpModeNotification) { + notifications.offer(notification) + } + + fun notify(state: OpModeState) { + synchronized(stateLock) { + this.state = state + } + + onStateChange.run() + } + + fun notify(notification: OpModeNotification, state: OpModeState) { + notifications.offer(notification) + + synchronized(stateLock) { + this.state = state + } + onStateChange.run() + } + + fun reset() { + notifications.clear() + state = OpModeState.STOPPED + } + + fun poll(): OpModeNotification { + return notifications.poll() ?: OpModeNotification.NOTHING + } + +} \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java b/Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java new file mode 100644 index 00000000..28640b7d --- /dev/null +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java @@ -0,0 +1,4 @@ +package io.github.deltacv.vision.internal.util; + +public class KillException extends RuntimeException { +} From 8c752461b8529a552a6f3b29febff61838bcef5f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 17 Aug 2023 23:39:40 -0600 Subject: [PATCH 24/46] Cursed workarounds around pipeline/opmode selector bugs --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 7 +- .../eocvsim/gui/EOCVSimIconLibrary.kt | 1 + .../serivesmejia/eocvsim/gui/Visualizer.java | 1 + .../PipelineOpModeSwitchablePanel.kt | 71 ++++++++++++--- .../visualizer/opmode/OpModeControlsPanel.kt | 41 +++++---- .../visualizer/opmode/OpModeSelectorPanel.kt | 81 +++++++++++++++-- .../pipeline/PipelineSelectorPanel.kt | 82 ++++++++++++++---- .../eocvsim/pipeline/PipelineManager.kt | 8 +- .../compiler/CompiledPipelineManager.kt | 27 +++--- .../main/resources/images/icon/ico_flag.png | Bin 0 -> 783 bytes 10 files changed, 243 insertions(+), 76 deletions(-) create mode 100644 EOCV-Sim/src/main/resources/images/icon/ico_flag.png 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 e24012c2..6b764e4b 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 @@ -221,11 +221,10 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0 visualizer.sourceSelectorPanel.allowSourceSwitching = true - visualizer.pipelineSelectorPanel.updatePipelinesList() //update pipelines and pick first one (DefaultPipeline) - visualizer.pipelineSelectorPanel.selectedIndex = 0 + visualizer.pipelineOpModeSwitchablePanel.updateSelectorListsBlocking() - visualizer.opModeSelectorPanel.updateOpModesList() //update opmodes and pick first one (DefaultPipeline) - visualizer.opModeSelectorPanel.selectedIndex = 0 + visualizer.pipelineSelectorPanel.selectedIndex = 0 //update pipelines and pick first one (DefaultPipeline) + visualizer.opModeSelectorPanel.selectedIndex = 0 //update opmodes and pick first one (DefaultPipeline) //post output mats from the pipeline to the visualizer viewport pipelineManager.pipelineOutputPosters.add(visualizer.viewport) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt index 51b6ac8f..80131f9f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt @@ -15,6 +15,7 @@ object EOCVSimIconLibrary { val icoArrowDropdown by icon("ico_arrow_dropdown", "/images/icon/ico_arrow_dropdown.png") + val icoFlag by icon("ico_flag", "/images/icon/ico_flag.png") val icoNotStarted by icon("ico_not_started", "/images/icon/ico_not_started.png") val icoPlay by icon("ico_play", "/images/icon/ico_play.png") val icoStop by icon("ico_stop", "/images/icon/ico_stop.png") diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 3eab4785..6cf13c23 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -137,6 +137,7 @@ public void init(Theme theme) { skiaPanel.add(tunerMenuPanel, BorderLayout.SOUTH); pipelineOpModeSwitchablePanel = new PipelineOpModeSwitchablePanel(eocvSim); + pipelineOpModeSwitchablePanel.disableSwitching(); pipelineSelectorPanel = pipelineOpModeSwitchablePanel.getPipelineSelectorPanel(); sourceSelectorPanel = pipelineOpModeSwitchablePanel.getSourceSelectorPanel(); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 0201ed19..f9924a69 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -4,8 +4,10 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeControlsPanel import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel -import java.awt.GridBagConstraints -import java.awt.GridBagLayout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing import java.awt.GridLayout import javax.swing.JPanel import javax.swing.JTabbedPane @@ -23,8 +25,6 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val opModeControlsPanel = OpModeControlsPanel(eocvSim) val opModeSelectorPanel = OpModeSelectorPanel(eocvSim, opModeControlsPanel) - private var beforeAllowPipelineSwitching: Boolean? = null - init { pipelinePanel.layout = GridLayout(2, 1) @@ -49,14 +49,9 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val sourceTabbedPane = it.source as JTabbedPane val index = sourceTabbedPane.selectedIndex - if(index == 0 && beforeAllowPipelineSwitching != null) { - pipelineSelectorPanel.allowPipelineSwitching = beforeAllowPipelineSwitching!! - - eocvSim.pipelineManager.requestChangePipeline(0) + if(index == 0) { + opModeSelectorPanel.reset(0) } else if(index == 1) { - beforeAllowPipelineSwitching = pipelineSelectorPanel.allowPipelineSwitching - pipelineSelectorPanel.allowPipelineSwitching = false - opModeSelectorPanel.reset() } } @@ -67,4 +62,58 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { opModeSelectorPanel.updateOpModesList() } + fun updateSelectorListsBlocking() = runBlocking { + launch(Dispatchers.Swing) { + updateSelectorLists() + } + } + + fun refreshAndReselectCurrent() { + saveLastSwitching() + disableSwitching() + + pipelineSelectorPanel.refreshAndReselectCurrent() + opModeSelectorPanel.refreshAndReselectCurrent() + + setLastSwitching() + } + + fun refreshAndReselectCurrentBlocking() = runBlocking { + launch(Dispatchers.Swing) { + refreshAndReselectCurrent() + } + } + + fun enableSwitching() { + pipelineSelectorPanel.allowPipelineSwitching = true + opModeSelectorPanel.allowOpModeSwitching = true + } + + fun disableSwitching() { + pipelineSelectorPanel.allowPipelineSwitching = false + opModeSelectorPanel.allowOpModeSwitching = false + } + + fun saveLastSwitching() { + pipelineSelectorPanel.saveLastSwitching() + opModeSelectorPanel.saveLastSwitching() + } + + fun setLastSwitching() { + pipelineSelectorPanel.setLastSwitching() + opModeSelectorPanel.setLastSwitching() + } + + fun enableSwitchingBlocking() = runBlocking { + launch(Dispatchers.Swing) { + enableSwitching() + } + } + + fun disableSwitchingBlocking() = runBlocking { + launch(Dispatchers.Swing) { + disableSwitching() + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt index 92b69a49..58aa8608 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -17,15 +17,20 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { val controlButton = JButton() + var currentOpMode: OpMode? = null + private set + private var currentManagerIndex: Int? = null + var allowOpModeSwitching = false + init { layout = BorderLayout() add(controlButton, BorderLayout.CENTER) controlButton.isEnabled = false - controlButton.icon = EOCVSimIconLibrary.icoNotStarted + controlButton.icon = EOCVSimIconLibrary.icoFlag controlButton.addActionListener { eocvSim.pipelineManager.onUpdate.doOnce { @@ -52,6 +57,8 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { } private fun notifySelected() { + if(!allowOpModeSwitching) return + if(eocvSim.pipelineManager.currentPipeline !is OpMode) return val opMode = eocvSim.pipelineManager.currentPipeline as OpMode @@ -68,6 +75,8 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { } opMode.notifier.notify(OpModeState.SELECTED) + + currentOpMode = opMode } private fun updateButtonState(state: OpModeState) { @@ -81,37 +90,27 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { OpModeState.STOPPED -> { controlButton.isEnabled = true - controlButton.icon = EOCVSimIconLibrary.icoNotStarted + controlButton.icon = EOCVSimIconLibrary.icoFlag } } } fun opModeSelected(managerIndex: Int) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) - currentManagerIndex = managerIndex + eocvSim.pipelineManager.setPaused(false) - eocvSim.pipelineManager.onUpdate.doOnce { - notifySelected() - } - } else { - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - controlButton.isEnabled = false - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) - eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) - currentManagerIndex = managerIndex - - eocvSim.pipelineManager.onUpdate.doOnce { - notifySelected() - } - } + eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) + currentManagerIndex = managerIndex + + eocvSim.pipelineManager.onUpdate.doOnce { + notifySelected() } } fun reset() { controlButton.isEnabled = false - controlButton.icon = EOCVSimIconLibrary.icoNotStarted + controlButton.icon = EOCVSimIconLibrary.icoFlag + + currentOpMode?.requestOpModeStop() } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index 877596f4..49e1dbaa 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -4,9 +4,11 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnThis import com.github.serivesmejia.eocvsim.gui.util.Location +import com.github.serivesmejia.eocvsim.pipeline.PipelineData import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.qualcomm.robotcore.eventloop.opmode.* import com.qualcomm.robotcore.util.Range +import io.github.deltacv.vision.internal.opmode.OpModeState import kotlinx.coroutines.runBlocking import java.awt.GridBagConstraints import java.awt.GridBagLayout @@ -16,11 +18,17 @@ import javax.swing.* class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeControlsPanel) : JPanel() { - var selectedIndex = -1 + private var _selectedIndex = -1 + + var selectedIndex: Int + get() = _selectedIndex set(value) { - field = value + opModeControlsPanel.opModeSelected(value) + _selectedIndex = value } + private var pipelinesData = arrayOf() + // private val autonomousIndexMap = mutableMapOf() // @@ -43,6 +51,14 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC val autonomousSelector = JList() val teleopSelector = JList() + var allowOpModeSwitching = false + set(value) { + opModeControlsPanel.allowOpModeSwitching = value + field = value + } + + private var lastSwitching: Boolean? = null + init { layout = GridBagLayout() selectOpModeLabelsPanel.layout = GridBagLayout() @@ -179,13 +195,17 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC textPanel.removeAll() textPanel.add(opModeNameLabelPanel) + _selectedIndex = managerIndex; + opModeControlsPanel.opModeSelected(managerIndex) } - fun updateOpModesList() = runBlocking { + fun updateOpModesList() { val autonomousListModel = DefaultListModel() val teleopListModel = DefaultListModel() + pipelinesData = eocvSim.pipelineManager.pipelines.toArray(arrayOf()) + var autonomousSelectorIndex = Range.clip(autonomousListModel.size() - 1, 0, Int.MAX_VALUE) var teleopSelectorIndex = Range.clip(teleopListModel.size() - 1, 0, Int.MAX_VALUE) @@ -219,13 +239,62 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC teleopSelector.model = teleopListModel } - fun reset() { + fun reset(nextPipeline: Int? = null) { textPanel.removeAll() textPanel.add(selectOpModeLabelsPanel) - eocvSim.pipelineManager.requestChangePipeline(null) - opModeControlsPanel.reset() + + if(eocvSim.pipelineManager.currentPipeline == opModeControlsPanel.currentOpMode) { + val opMode = opModeControlsPanel.currentOpMode + + opMode?.notifier?.onStateChange?.let { + it { + val state = opMode.notifier.state + + if(state == OpModeState.STOPPED) { + it.removeThis() + eocvSim.pipelineManager.requestChangePipeline(nextPipeline) + } + } + } + } else { + eocvSim.pipelineManager.onUpdate.doOnce { + eocvSim.pipelineManager.requestChangePipeline(nextPipeline) + } + } + + _selectedIndex = -1 + } + + fun refreshAndReselectCurrent() { + val currentIndex = selectedIndex + if(currentIndex < 0) return + + val beforePipeline = pipelinesData[currentIndex] + + updateOpModesList() + + for((i, pipeline) in pipelinesData.withIndex()) { + if(pipeline.clazz.name == beforePipeline.clazz.name && pipeline.source == beforePipeline.source) { + selectedIndex = i + return + } + } + + selectedIndex = -1 + } + + + fun setLastSwitching() { + if(lastSwitching != null) { + allowOpModeSwitching = lastSwitching!! + lastSwitching = null + } + } + + fun saveLastSwitching() { + lastSwitching = allowOpModeSwitching } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index f3caf733..5ad2bb30 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -25,6 +25,7 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer +import com.github.serivesmejia.eocvsim.pipeline.PipelineData import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.qualcomm.robotcore.eventloop.opmode.OpMode @@ -33,7 +34,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.swing.Swing -import java.awt.FlowLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout import javax.swing.* @@ -51,8 +51,10 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { } } - val pipelineSelector = JList() - val pipelineSelectorScroll = JScrollPane() + private var pipelinesData = arrayOf() + + val pipelineSelector = JList() + val pipelineSelectorScroll = JScrollPane() val pipelineSelectorLabel = JLabel("Pipelines") @@ -62,6 +64,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { val buttonsPanel = PipelineSelectorButtonsPanel(eocvSim) var allowPipelineSwitching = false + private var lastSwitching: Boolean? = false private var beforeSelectedPipeline = -1 @@ -107,7 +110,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { //listener for changing pipeline pipelineSelector.addListSelectionListener { evt: ListSelectionEvent -> - if(!allowPipelineSwitching) return@addListSelectionListener + if (!allowPipelineSwitching) return@addListSelectionListener if (pipelineSelector.selectedIndex != -1) { val pipeline = indexMap[pipelineSelector.selectedIndex] ?: return@addListSelectionListener @@ -132,27 +135,27 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { } } - fun updatePipelinesList() = runBlocking { - launch(Dispatchers.Swing) { - val listModel = DefaultListModel() - var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE) + fun updatePipelinesList() { + val listModel = DefaultListModel() + var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE) - indexMap.clear() + indexMap.clear() - for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) { - if(!ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) { - listModel.addElement(pipeline.clazz.simpleName) - indexMap[selectorIndex] = managerIndex + pipelinesData = eocvSim.pipelineManager.pipelines.toArray(arrayOf()) - selectorIndex++ - } + for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) { + if (!ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) { + listModel.addElement(pipeline.clazz.simpleName) + indexMap[selectorIndex] = managerIndex + + selectorIndex++ } + } - pipelineSelector.fixedCellWidth = 240 - pipelineSelector.model = listModel + pipelineSelector.fixedCellWidth = 240 + pipelineSelector.model = listModel - revalAndRepaint() - } + revalAndRepaint() } fun revalAndRepaint() { @@ -162,4 +165,45 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { pipelineSelectorScroll.repaint() } + fun refreshAndReselectCurrent(changePipeline: Boolean = false) { + val currentIndex = selectedIndex + val beforePipeline = pipelinesData[currentIndex] + + updatePipelinesList() + + val beforeSwitching = allowPipelineSwitching + + if(!changePipeline) { + allowPipelineSwitching = false + } + + for((i, pipeline) in pipelinesData.withIndex()) { + if(pipeline.clazz.name == beforePipeline.clazz.name && pipeline.source == beforePipeline.source) { + selectedIndex = i + + if(!changePipeline) { + allowPipelineSwitching = beforeSwitching + } + return + } + } + + selectedIndex = 0 // default pipeline + + if(!changePipeline) { + allowPipelineSwitching = beforeSwitching + } + } + + fun setLastSwitching() { + if(lastSwitching != null) { + allowPipelineSwitching = lastSwitching!! + lastSwitching = null + } + } + + fun saveLastSwitching() { + lastSwitching = allowPipelineSwitching + } + } 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 12b9f1f1..50d896a9 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 @@ -115,6 +115,8 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi var lastInitialSnapshot: PipelineSnapshot? = null private set + var applyLatestSnapshotOnChange = false + val snapshotFieldFilter: (Field) -> Boolean = { // only snapshot fields managed by the variable tuner // when getTunableFieldOf returns null, it means that @@ -498,7 +500,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi */ @OptIn(ExperimentalCoroutinesApi::class) fun forceChangePipeline(index: Int?, - applyLatestSnapshot: Boolean = false, + applyLatestSnapshot: Boolean = applyLatestSnapshotOnChange, applyStaticSnapshot: Boolean = false) { if(index == null) { previousPipelineIndex = currentPipelineIndex @@ -700,6 +702,10 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi eocvSim.visualizer.pipelineOpModeSwitchablePanel.updateSelectorLists() } + fun refreshAndReselectCurrentPipeline() { + eocvSim.visualizer.pipelineOpModeSwitchablePanel.refreshAndReselectCurrent() + } + } enum class PipelineTimeout(val ms: Long, val coolName: String) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index d317ced3..a43834fa 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -125,11 +125,12 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { currentPipelineClassLoader = null val messageEnd = "(took $timeElapsed seconds)\n\n${result.message}".trim() - val pipelineSelectorPanel = pipelineManager.eocvSim.visualizer.pipelineSelectorPanel - val beforeAllowSwitching = pipelineSelectorPanel?.allowPipelineSwitching + val pipelineSelectorPanel = pipelineManager.eocvSim.visualizer.pipelineOpModeSwitchablePanel + + pipelineSelectorPanel.saveLastSwitching() if(fixSelectedPipeline) - pipelineSelectorPanel?.allowPipelineSwitching = false + pipelineSelectorPanel.disableSwitchingBlocking() pipelineManager.requestRemoveAllPipelinesFrom( PipelineSource.COMPILED_ON_RUNTIME, @@ -156,24 +157,22 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { } } - val beforePipeline = pipelineManager.currentPipelineData - pipelineManager.onUpdate.doOnce { - pipelineManager.refreshGuiPipelineList() - if(fixSelectedPipeline) { - if(beforePipeline != null) { - val pipeline = pipelineManager.getIndexOf(beforePipeline.clazz, beforePipeline.source) + pipelineManager.applyLatestSnapshotOnChange = true - pipelineManager.forceChangePipeline(pipeline, true) - } else { - pipelineManager.changePipeline(0) //default pipeline - } + pipelineManager.refreshAndReselectCurrentPipeline() - pipelineSelectorPanel?.allowPipelineSwitching = beforeAllowSwitching!! + pipelineManager.onPipelineChange.doOnce { + pipelineManager.applyLatestSnapshotOnChange = false + } + } else { + pipelineManager.refreshGuiPipelineList() } } + pipelineSelectorPanel.setLastSwitching() + if(result.status == PipelineCompileStatus.SUCCESS) { logger.info("$lastBuildOutputMessage\n") } else { diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_flag.png b/EOCV-Sim/src/main/resources/images/icon/ico_flag.png new file mode 100644 index 0000000000000000000000000000000000000000..d86bececd3e532a4b3f2f9b2d0502527a0446803 GIT binary patch literal 783 zcmV+q1MvKbP)Px%$w@>(RA@u(nLTS2Q4ofo`~fk&f@w{mf}o~ROB>Nj!4DKvY(i875d^gnK~b?0 zQSbv5JGDt0ZHxgWvCvjQdqe(!^1@Brdw2JonRCzH+=UD*T)1;)=G{5-xg)fkMp|w^ zYXDZ;gE>900oV;}2R0QRVfTxCD&5G{u>}0k?stan}DFUI6!iDUX5#UI931J?8;1 z4eYZD?vqbJKW33(1#lmjvD<}Ii&5E0b*3`Kh5%dxt{Sw(yx1#Z2H0beTV%No$et3_DOZJ^H5Mr^ z02wb39@5+2RaLR;l&dnHGivYxkk}M2s0OL>cTt`4d0=aU08+fL0Bo}WP$&7rXuQ}` zJP;E9H}I#5n~K~DRVhCKJQ-2|YB9Qcr78fP`#)$8l;WTYz#HJekb2-Pa4>d-JOMmt z50nDA$^(CZ^+W1`MPM}Mfz|_P{S`_L;=1Cr7kKZulA$_a3gXl>S_eSJ3xt(I08D9A z{r3y2wn8NiwFe$Aga8}`UdDo03#wn40eHL+0 Date: Fri, 18 Aug 2023 00:14:00 -0600 Subject: [PATCH 25/46] Fix nasty bugs with programatically updated selectors --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 2 + .../PipelineOpModeSwitchablePanel.kt | 26 -------- .../visualizer/opmode/OpModeSelectorPanel.kt | 61 ++++++------------- .../pipeline/PipelineSelectorPanel.kt | 56 ++++++++--------- .../eocvsim/pipeline/PipelineManager.kt | 16 ++++- .../compiler/CompiledPipelineManager.kt | 22 +------ 6 files changed, 60 insertions(+), 123 deletions(-) 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 6b764e4b..b27e40af 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 @@ -226,6 +226,8 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.pipelineSelectorPanel.selectedIndex = 0 //update pipelines and pick first one (DefaultPipeline) visualizer.opModeSelectorPanel.selectedIndex = 0 //update opmodes and pick first one (DefaultPipeline) + visualizer.pipelineOpModeSwitchablePanel.enableSwitchingBlocking() + //post output mats from the pipeline to the visualizer viewport pipelineManager.pipelineOutputPosters.add(visualizer.viewport) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index f9924a69..41ee3c2a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -68,22 +68,6 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { } } - fun refreshAndReselectCurrent() { - saveLastSwitching() - disableSwitching() - - pipelineSelectorPanel.refreshAndReselectCurrent() - opModeSelectorPanel.refreshAndReselectCurrent() - - setLastSwitching() - } - - fun refreshAndReselectCurrentBlocking() = runBlocking { - launch(Dispatchers.Swing) { - refreshAndReselectCurrent() - } - } - fun enableSwitching() { pipelineSelectorPanel.allowPipelineSwitching = true opModeSelectorPanel.allowOpModeSwitching = true @@ -94,16 +78,6 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { opModeSelectorPanel.allowOpModeSwitching = false } - fun saveLastSwitching() { - pipelineSelectorPanel.saveLastSwitching() - opModeSelectorPanel.saveLastSwitching() - } - - fun setLastSwitching() { - pipelineSelectorPanel.setLastSwitching() - opModeSelectorPanel.setLastSwitching() - } - fun enableSwitchingBlocking() = runBlocking { launch(Dispatchers.Swing) { enableSwitching() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index 49e1dbaa..b7dd9665 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -9,10 +9,10 @@ import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.qualcomm.robotcore.eventloop.opmode.* import com.qualcomm.robotcore.util.Range import io.github.deltacv.vision.internal.opmode.OpModeState -import kotlinx.coroutines.runBlocking import java.awt.GridBagConstraints import java.awt.GridBagLayout -import java.awt.Insets +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import javax.swing.* @@ -57,8 +57,6 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC field = value } - private var lastSwitching: Boolean? = null - init { layout = GridBagLayout() selectOpModeLabelsPanel.layout = GridBagLayout() @@ -162,23 +160,27 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC popup.show() } - autonomousSelector.addListSelectionListener { - if(!it.valueIsAdjusting) { - val index = autonomousSelector.selectedIndex - if(index != -1) { + autonomousSelector.addMouseListener(object: MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (!allowOpModeSwitching) return + + val index = (e.source as JList<*>).locationToIndex(e.point) + if(index >= 0) { autonomousSelected(index) } } - } + }) + + teleopSelector.addMouseListener(object: MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (!allowOpModeSwitching) return - teleopSelector.addListSelectionListener { - if(!it.valueIsAdjusting) { - val index = teleopSelector.selectedIndex - if(index != -1) { + val index = (e.source as JList<*>).locationToIndex(e.point) + if(index >= 0) { teleOpSelected(index) } } - } + }) } private fun teleOpSelected(index: Int) { @@ -265,36 +267,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC } _selectedIndex = -1 - } - - fun refreshAndReselectCurrent() { - val currentIndex = selectedIndex - if(currentIndex < 0) return - - val beforePipeline = pipelinesData[currentIndex] - - updateOpModesList() - - for((i, pipeline) in pipelinesData.withIndex()) { - if(pipeline.clazz.name == beforePipeline.clazz.name && pipeline.source == beforePipeline.source) { - selectedIndex = i - return - } - } - - selectedIndex = -1 - } - - - fun setLastSwitching() { - if(lastSwitching != null) { - allowOpModeSwitching = lastSwitching!! - lastSwitching = null - } - } - - fun saveLastSwitching() { - lastSwitching = allowOpModeSwitching + opModeControlsPanel.stopCurrentOpMode() } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index 5ad2bb30..7be3f7f9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.swing.Swing import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import javax.swing.* import javax.swing.event.ListSelectionEvent @@ -64,7 +66,6 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { val buttonsPanel = PipelineSelectorButtonsPanel(eocvSim) var allowPipelineSwitching = false - private var lastSwitching: Boolean? = false private var beforeSelectedPipeline = -1 @@ -107,31 +108,37 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { } private fun registerListeners() { + pipelineSelector.addMouseListener(object: MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (!allowPipelineSwitching) return - //listener for changing pipeline - pipelineSelector.addListSelectionListener { evt: ListSelectionEvent -> - if (!allowPipelineSwitching) return@addListSelectionListener - - if (pipelineSelector.selectedIndex != -1) { - val pipeline = indexMap[pipelineSelector.selectedIndex] ?: return@addListSelectionListener - - if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.pipelineManager.requestChangePipeline(pipeline) - beforeSelectedPipeline = pipeline - } else { - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - pipelineSelector.setSelectedIndex(beforeSelectedPipeline) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) + val index = (e.source as JList<*>).locationToIndex(e.point) + + if (index != -1) { + val pipeline = indexMap[index] ?: return + + if (pipeline != beforeSelectedPipeline) { + if (!eocvSim.pipelineManager.paused) { eocvSim.pipelineManager.requestChangePipeline(pipeline) beforeSelectedPipeline = pipeline + } else { + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + pipelineSelector.setSelectedIndex(beforeSelectedPipeline) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.pipelineManager.requestChangePipeline(pipeline) + beforeSelectedPipeline = pipeline + } } } + } else { + pipelineSelector.setSelectedIndex(0) } - } else { - pipelineSelector.setSelectedIndex(1) } + }) + + eocvSim.pipelineManager.onPipelineChange { + selectedIndex = eocvSim.pipelineManager.currentPipelineIndex } } @@ -195,15 +202,4 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { } } - fun setLastSwitching() { - if(lastSwitching != null) { - allowPipelineSwitching = lastSwitching!! - lastSwitching = null - } - } - - fun saveLastSwitching() { - lastSwitching = allowPipelineSwitching - } - } 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 50d896a9..554385a9 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 @@ -220,7 +220,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi } } - eocvSim.visualizer.pipelineSelectorPanel.allowPipelineSwitching = true + eocvSim.visualizer.pipelineOpModeSwitchablePanel.enableSwitchingBlocking() } } @@ -506,6 +506,9 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi previousPipelineIndex = currentPipelineIndex currentPipeline = null + currentPipelineName = "" + currentPipelineContext = null + currentPipelineData = null currentPipelineIndex = -1 onPipelineChange.run() @@ -702,8 +705,15 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi eocvSim.visualizer.pipelineOpModeSwitchablePanel.updateSelectorLists() } - fun refreshAndReselectCurrentPipeline() { - eocvSim.visualizer.pipelineOpModeSwitchablePanel.refreshAndReselectCurrent() + fun reloadPipelineByName() { + for((i, pipeline) in pipelines.withIndex()) { + if(pipeline.clazz.name == currentPipelineData?.clazz?.name && pipeline.source == currentPipelineData?.source) { + forceChangePipeline(i, true) + return + } + } + + forceChangePipeline(0) // default pipeline } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index a43834fa..b06a5dfa 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -125,13 +125,6 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { currentPipelineClassLoader = null val messageEnd = "(took $timeElapsed seconds)\n\n${result.message}".trim() - val pipelineSelectorPanel = pipelineManager.eocvSim.visualizer.pipelineOpModeSwitchablePanel - - pipelineSelectorPanel.saveLastSwitching() - - if(fixSelectedPipeline) - pipelineSelectorPanel.disableSwitchingBlocking() - pipelineManager.requestRemoveAllPipelinesFrom( PipelineSource.COMPILED_ON_RUNTIME, refreshGuiPipelineList = false, @@ -158,21 +151,10 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { } pipelineManager.onUpdate.doOnce { - if(fixSelectedPipeline) { - pipelineManager.applyLatestSnapshotOnChange = true - - pipelineManager.refreshAndReselectCurrentPipeline() - - pipelineManager.onPipelineChange.doOnce { - pipelineManager.applyLatestSnapshotOnChange = false - } - } else { - pipelineManager.refreshGuiPipelineList() - } + pipelineManager.refreshGuiPipelineList() + pipelineManager.reloadPipelineByName() } - pipelineSelectorPanel.setLastSwitching() - if(result.status == PipelineCompileStatus.SUCCESS) { logger.info("$lastBuildOutputMessage\n") } else { From 485d271df1f7782e08e736a035bb05ceba96e832 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 18 Aug 2023 12:22:20 -0600 Subject: [PATCH 26/46] Fixed issues with selectors, almost fully working --- .../PipelineOpModeSwitchablePanel.kt | 6 +++ .../visualizer/opmode/OpModeControlsPanel.kt | 5 ++- .../visualizer/opmode/OpModeSelectorPanel.kt | 45 ++++++++++++++++--- .../pipeline/PipelineSelectorPanel.kt | 3 ++ .../eocvsim/pipeline/PipelineManager.kt | 19 +++++++- .../eventloop/opmode/OpModePipelineHandler.kt | 4 +- .../robotcore/eventloop/opmode/OpMode.java | 22 +++++++-- 7 files changed, 90 insertions(+), 14 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 41ee3c2a..2a8a65ce 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -51,8 +51,14 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { if(index == 0) { opModeSelectorPanel.reset(0) + + pipelineSelectorPanel.isActive = true + opModeSelectorPanel.isActive = false } else if(index == 1) { opModeSelectorPanel.reset() + + pipelineSelectorPanel.isActive = false + opModeSelectorPanel.isActive = true } } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt index 58aa8608..e8ff4610 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -71,6 +71,7 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { if(state == OpModeState.STOPPED) { opModeSelected(currentManagerIndex!!) + it.removeThis() } } @@ -95,10 +96,10 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { } } - fun opModeSelected(managerIndex: Int) { + fun opModeSelected(managerIndex: Int, forceChangePipeline: Boolean = true) { eocvSim.pipelineManager.setPaused(false) - eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) + if(forceChangePipeline) eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) currentManagerIndex = managerIndex eocvSim.pipelineManager.onUpdate.doOnce { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index b7dd9665..79d3e406 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -6,6 +6,7 @@ import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnTh import com.github.serivesmejia.eocvsim.gui.util.Location import com.github.serivesmejia.eocvsim.pipeline.PipelineData import com.github.serivesmejia.eocvsim.util.ReflectUtil +import com.github.serivesmejia.eocvsim.util.loggerForThis import com.qualcomm.robotcore.eventloop.opmode.* import com.qualcomm.robotcore.util.Range import io.github.deltacv.vision.internal.opmode.OpModeState @@ -20,6 +21,8 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC private var _selectedIndex = -1 + private val logger by loggerForThis() + var selectedIndex: Int get() = _selectedIndex set(value) { @@ -57,6 +60,9 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC field = value } + var isActive = false + internal set + init { layout = GridBagLayout() selectOpModeLabelsPanel.layout = GridBagLayout() @@ -181,6 +187,30 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC } } }) + + eocvSim.pipelineManager.onPipelineChange { + // we are doing this to detect external pipeline changes + // in the event that this change was triggered by us, OpModeSelectorPanel, + // we need to hold on a cycle so that the state has been fully updated, + // just to be able to check correctly. + eocvSim.pipelineManager.onUpdate.doOnce { + if(isActive && opModeControlsPanel.currentOpMode != eocvSim.pipelineManager.currentPipeline) { + val opMode = eocvSim.pipelineManager.currentPipeline + + if(opMode is OpMode) { + val name = if (opMode.opModeType == OpModeType.AUTONOMOUS) + opMode.autonomousAnnotation.name + else opMode.teleopAnnotation.name + + logger.info("External change detected \"$name\"") + + opModeSelected(eocvSim.pipelineManager.currentPipelineIndex, name, false) + } else { + reset(-1) + } + } + } + } } private fun teleOpSelected(index: Int) { @@ -191,15 +221,15 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC opModeSelected(autonomousIndexMap[index]!!, autonomousSelector.selectedValue!!) } - private fun opModeSelected(managerIndex: Int, name: String) { + private fun opModeSelected(managerIndex: Int, name: String, forceChangePipeline: Boolean = true) { opModeNameLabel.text = name textPanel.removeAll() textPanel.add(opModeNameLabelPanel) - _selectedIndex = managerIndex; + _selectedIndex = managerIndex - opModeControlsPanel.opModeSelected(managerIndex) + opModeControlsPanel.opModeSelected(managerIndex, forceChangePipeline) } fun updateOpModesList() { @@ -256,11 +286,16 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC if(state == OpModeState.STOPPED) { it.removeThis() - eocvSim.pipelineManager.requestChangePipeline(nextPipeline) + + if(nextPipeline == null || nextPipeline >= 0) { + eocvSim.pipelineManager.onUpdate.doOnce { + eocvSim.pipelineManager.requestChangePipeline(nextPipeline) + } + } } } } - } else { + } else if(nextPipeline == null || nextPipeline >= 0) { eocvSim.pipelineManager.onUpdate.doOnce { eocvSim.pipelineManager.requestChangePipeline(nextPipeline) } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index 7be3f7f9..593af4be 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -69,6 +69,9 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { private var beforeSelectedPipeline = -1 + var isActive = false + internal set + init { layout = GridBagLayout() 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 554385a9..b94273ea 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 @@ -29,13 +29,13 @@ import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot -import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException 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 kotlinx.coroutines.* import org.firstinspires.ftc.robotcore.external.Telemetry import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl @@ -525,6 +525,8 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi logger.info("Changing to pipeline ${pipelineClass.name}") + debugLogCalled("forceChangePipeline") + var constructor: Constructor<*> try { @@ -614,7 +616,11 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi } } - fun requestForceChangePipeline(index: Int) = onUpdate.doOnce { forceChangePipeline(index) } + fun requestForceChangePipeline(index: Int) { + debugLogCalled("requestForceChangePipeline") + + onUpdate.doOnce { forceChangePipeline(index) } + } fun applyLatestSnapshot() { if(currentPipeline != null && latestSnapshot != null) { @@ -716,6 +722,15 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi forceChangePipeline(0) // default pipeline } + private fun debugLogCalled(name: String) { + val builder = StringBuilder() + for (s in Thread.currentThread().stackTrace) { + builder.appendLine(s.toString()) + } + + logger.debug("$name called in: {}", builder.toString().trim()) + } + } enum class PipelineTimeout(val ms: Long, val coolName: String) { diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt index d0fc8983..057c6b2a 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt @@ -36,8 +36,8 @@ class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private } override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) { - if(beforePipeline is OpMode && beforePipeline == pipeline) { - beforePipeline.requestOpModeStop() + if(beforePipeline is OpMode) { + beforePipeline.forceStop() onStop.run() } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java index fcd8c3e7..557d44fd 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java @@ -114,12 +114,21 @@ public void requestOpModeStop() { /* BEGIN OpenCvPipeline Impl */ + private boolean stopped = false; + @Override public final void init(Mat mat) { + if(stopped) { + throw new IllegalStateException("Trying to reuse already stopped OpMode"); + } } @Override public final Mat processFrame(Mat input, long captureTimeNanos) { + if(stopped) { + throw new IllegalStateException("Trying to reuse already stopped OpMode"); + } + OpModeNotification notification = notifier.poll(); if(notification != OpModeNotification.NOTHING) { @@ -140,9 +149,7 @@ public final Mat processFrame(Mat input, long captureTimeNanos) { notifier.notify(OpModeState.START); break; case STOP: - notifier.notify(OpModeState.STOP); - stop(); - notifier.notify(OpModeState.STOPPED); + forceStop(); break; case NOTHING: break; @@ -178,4 +185,13 @@ public final Mat processFrame(Mat input, long captureTimeNanos) { public final void onViewportTapped() { } + public void forceStop() { + if(stopped) return; + + notifier.notify(OpModeState.STOP); + stop(); + notifier.notify(OpModeState.STOPPED); + + stopped = true; + } } From 4dc37c2037d99a4e1e7149fd2bac69bd03701339 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 15:04:37 -0600 Subject: [PATCH 27/46] Update setup-java in ci --- .github/workflows/release_ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index 14778108..aa0f5251 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -12,10 +12,10 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 10 + distribution: 'zulu' + java-version: '10' - name: Grant execute permission for gradlew run: chmod +x gradlew From 22bcea131349460c6b31ce73c1728cd344328504 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 18 Aug 2023 16:03:03 -0600 Subject: [PATCH 28/46] Implement Pain.FontMetrics from android.graphics --- .../samples/ConceptStackProcessor.java | 148 ++++++++++++++++++ .../teamcode/processors/StackProcessor.java | 111 +++++++++++++ .../src/main/java/android/graphics/Paint.java | 51 ++++++ 3 files changed, 310 insertions(+) create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java new file mode 100644 index 00000000..95cd21b4 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java @@ -0,0 +1,148 @@ +/* Copyright (c) 2023 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.robotcontroller.external.samples; + +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.teamcode.processors.StackProcessor; +import org.firstinspires.ftc.vision.VisionPortal; +import org.firstinspires.ftc.vision.VisionProcessor; +import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; +import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; + +import java.util.List; + +/** + * This 2023-2024 OpMode illustrates the basics of AprilTag recognition and pose estimation, + * including Java Builder structures for specifying Vision parameters. + * + * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. + */ +@TeleOp(name = "Concept: Stack Processor", group = "Concept") +// @Disabled +public class ConceptStackProcessor extends LinearOpMode { + + private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera + + /** + * {@link #aprilTag} is the variable to store our instance of the AprilTag processor. + */ + private VisionProcessor aprilTag; + + /** + * {@link #visionPortal} is the variable to store our instance of the vision portal. + */ + private VisionPortal visionPortal; + + @Override + public void runOpMode() { + + initAprilTag(); + + // Wait for the DS start button to be touched. + telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); + telemetry.addData(">", "Touch Play to start OpMode"); + telemetry.update(); + + waitForStart(); + + if (opModeIsActive()) { + while (opModeIsActive()) { + + telemetryAprilTag(); + + // Push telemetry to the Driver Station. + telemetry.update(); + + // Share the CPU. + sleep(20); + } + } + + // Save more CPU resources when camera is no longer needed. + visionPortal.close(); + + } // end method runOpMode() + + /** + * Initialize the AprilTag processor. + */ + private void initAprilTag() { + + // Create the AprilTag processor. + aprilTag = new StackProcessor(); + + // Create the vision portal by using a builder. + VisionPortal.Builder builder = new VisionPortal.Builder(); + + // Set the camera (webcam vs. built-in RC phone camera). + if (USE_WEBCAM) { + builder.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")); + } else { + builder.setCamera(BuiltinCameraDirection.BACK); + } + + // Choose a camera resolution. Not all cameras support all resolutions. + //builder.setCameraResolution(new Size(640, 480)); + + // Enable the RC preview (LiveView). Set "false" to omit camera monitoring. + //builder.enableCameraMonitoring(true); + + // Set the stream format; MJPEG uses less bandwidth than default YUY2. + //builder.setStreamFormat(VisionPortal.StreamFormat.YUY2); + + // Choose whether or not LiveView stops if no processors are enabled. + // If set "true", monitor shows solid orange screen if no processors enabled. + // If set "false", monitor shows camera view without annotations. + //builder.setAutoStopLiveView(false); + + // Set and enable the processor. + builder.addProcessor(aprilTag); + + // Build the Vision Portal, using the above settings. + visionPortal = builder.build(); + + // Disable or re-enable the aprilTag processor at any time. + //visionPortal.setProcessorEnabled(aprilTag, true); + + } // end method initAprilTag() + + + /** + * Function to add telemetry about AprilTag detections. + */ + private void telemetryAprilTag() { + + } // end method telemetryAprilTag() + +} // end class \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java new file mode 100644 index 00000000..83737f19 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java @@ -0,0 +1,111 @@ +package org.firstinspires.ftc.teamcode.processors; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; + +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.Rect; +import org.opencv.imgproc.Imgproc; + +public class StackProcessor implements VisionProcessor { + public enum RingNumber { + ZERO, + ONE, + FOUR; + + public double avgHueValue; + } + + private final static int TEST_RECT_X = 240; + private final static int TEST_RECT_Y = 146; + private final static int TEST_RECT_WIDTH = 20; + private final static int TEST_RECT_HEIGHT = 60; + private final static double FOUR_STACK_HUE_THRESHOLD = 70; + private final static double ONE_STACK_HUE_THRESHOLD = 58; + private RingNumber result; + Paint rectPaint; + Paint textPaint; + android.graphics.Rect drawRectangle; + float textLineSize; + + public RingNumber getResult() { + return result; + } + private boolean initialFrameDone; + private Mat ring; + private Mat ringHSV; + private boolean initialDrawDone; + + @Override + public void init(int width, int height, CameraCalibration calibration) { + + } + + public void processFirstFrame(Mat frame) { + Rect rect = new Rect(TEST_RECT_X, TEST_RECT_Y, TEST_RECT_WIDTH, TEST_RECT_HEIGHT); + + ring = frame.submat(rect); + ringHSV = new Mat(ring.cols(), ring.rows(), ring.type()); + } + + @Override + public Object processFrame(Mat frame, long captureTimeNanos) { + if (!initialFrameDone) { + processFirstFrame(frame); + initialFrameDone = true; + } + Imgproc.cvtColor(ring, ringHSV, Imgproc.COLOR_BGR2HSV); + + double avgHueValue = Core.mean(ringHSV).val[0]; + + ring.release(); + ringHSV.release(); + + if (avgHueValue > FOUR_STACK_HUE_THRESHOLD) { + result = RingNumber.FOUR; + } else if (avgHueValue > ONE_STACK_HUE_THRESHOLD) { + result = RingNumber.ONE; + } else { + result = RingNumber.ZERO; + } + result.avgHueValue = avgHueValue; + + return result; + } + + public void drawFirstFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity) { + rectPaint = new Paint(); + rectPaint.setColor(Color.RED); + rectPaint.setStyle(Paint.Style.STROKE); + + textPaint = new Paint(); + textPaint.setTextSize(40 * scaleCanvasDensity); + textPaint.setColor(Color.GREEN); + + int left = Math.round(TEST_RECT_X * scaleBmpPxToCanvasPx); + int top = Math.round(TEST_RECT_Y * scaleBmpPxToCanvasPx); + int right = left + Math.round(TEST_RECT_WIDTH * scaleBmpPxToCanvasPx); + int bottom = top + Math.round(TEST_RECT_HEIGHT * scaleBmpPxToCanvasPx); + + Paint.FontMetrics fm = textPaint.getFontMetrics(); + textLineSize = (fm.descent - fm.ascent) * scaleBmpPxToCanvasPx; + + drawRectangle = new android.graphics.Rect(left, top, right, bottom); + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { + if (!initialDrawDone) { + drawFirstFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity); + initialDrawDone = true; + } + canvas.drawRect(drawRectangle, rectPaint); + canvas.drawText(userContext.toString(), 0, textLineSize, textPaint); + canvas.drawText(String.format("%.2f", ((RingNumber) userContext).avgHueValue), + 0, textLineSize * 2, textPaint); + } +} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java index ce9a9ede..81798a94 100644 --- a/Vision/src/main/java/android/graphics/Paint.java +++ b/Vision/src/main/java/android/graphics/Paint.java @@ -23,6 +23,8 @@ package android.graphics; +import org.jetbrains.skia.Font; +import org.jetbrains.skia.FontMetrics; import org.jetbrains.skia.PaintStrokeCap; import org.jetbrains.skia.PaintStrokeJoin; @@ -142,6 +144,38 @@ private Align(int nativeInt) { final int nativeInt; } + + /** + * Class that describes the various metrics for a font at a given text size. + * Remember, Y values increase going down, so those values will be positive, + * and values that measure distances going up will be negative. This class + * is returned by getFontMetrics(). + */ + public static class FontMetrics { + /** + * The maximum distance above the baseline for the tallest glyph in + * the font at a given text size. + */ + public float top; + /** + * The recommended distance above the baseline for singled spaced text. + */ + public float ascent; + /** + * The recommended distance below the baseline for singled spaced text. + */ + public float descent; + /** + * The maximum distance below the baseline for the lowest glyph in + * the font at a given text size. + */ + public float bottom; + /** + * The recommended additional space to add between lines of text. + */ + public float leading; + } + public final org.jetbrains.skia.Paint thePaint; private Typeface typeface; @@ -272,6 +306,23 @@ public Typeface getTypeface() { return typeface; } + private Font getFont() { + return FontCache.makeFont(getTypeface(), getTextSize()); + } + + public FontMetrics getFontMetrics() { + FontMetrics metrics = new FontMetrics(); + org.jetbrains.skia.FontMetrics fontMetrics = getFont().getMetrics(); + + metrics.top = fontMetrics.getTop(); + metrics.ascent = fontMetrics.getAscent(); + metrics.descent = fontMetrics.getDescent(); + metrics.bottom = fontMetrics.getBottom(); + metrics.leading = fontMetrics.getLeading(); + + return metrics; + } + public float getTextSize() { return textSize; } From d91b12e6ce4a2efa6d047b066b2596703b617eb3 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 17:33:41 -0600 Subject: [PATCH 29/46] Publish Vision to maven local (jitpack) --- jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index ce647a8b..bbf4c700 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -3,4 +3,4 @@ jdk: before_install: - chmod +x gradlew install: - - ./gradlew :EOCV-Sim:clean :EOCV-Sim:build :EOCV-Sim:publishToMavenLocal :Common:publishToMavenLocal -x :EOCV-Sim:test + - ./gradlew :EOCV-Sim:clean :EOCV-Sim:build :EOCV-Sim:publishToMavenLocal :Common:publishToMavenLocal :Vision:publishToMavenLocal -x :EOCV-Sim:test From 538abb88fb0798b5b3a2d872c0846a1870fac1e9 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 17:34:50 -0600 Subject: [PATCH 30/46] Use Oracle JDK in CI --- .github/workflows/release_ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index aa0f5251..f287c15b 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -8,13 +8,13 @@ on: jobs: build-and-release: if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref != 'ref/heads/master' }} - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 + - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'zulu' + distribution: 'oracle' java-version: '10' - name: Grant execute permission for gradlew From e6928221faf5cc724b46044d00732a1790d83720 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 17:37:43 -0600 Subject: [PATCH 31/46] Change to microsoft OpenJDK --- .github/workflows/release_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index f287c15b..13048a63 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -14,7 +14,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'oracle' + distribution: 'microsof' java-version: '10' - name: Grant execute permission for gradlew From 3aa2bbd012ac36530eb8b8f163198b64c03acc80 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 17:39:08 -0600 Subject: [PATCH 32/46] Fix typo --- .github/workflows/release_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index 13048a63..537c7dac 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -14,7 +14,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'microsof' + distribution: 'microsoft' java-version: '10' - name: Grant execute permission for gradlew From 7b30b2bb0ace98597bfbf9a4e253daf73aae097f Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 17:40:51 -0600 Subject: [PATCH 33/46] Go back to zulu jdk 13 --- .github/workflows/release_ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index 537c7dac..b7506112 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -14,8 +14,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - distribution: 'microsoft' - java-version: '10' + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew From 4cb1e704765e9d4b79eaa81045ff315eebd92a54 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Fri, 18 Aug 2023 17:42:33 -0600 Subject: [PATCH 34/46] Go back to Ubuntu --- .github/workflows/release_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index b7506112..53dfb8eb 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -8,7 +8,7 @@ on: jobs: build-and-release: if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref != 'ref/heads/master' }} - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up JDK From 2a8e1d871e77e141b1e3b4ee5adb4bc07d4908db Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 19 Aug 2023 15:33:10 -0600 Subject: [PATCH 35/46] Debugging lag issues --- .../main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt | 7 +++---- .../external/samples/ConceptStackProcessor.java | 1 - .../github/deltacv/vision/internal/util/KillException.java | 4 ---- 3 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java 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 b27e40af..4f9d1555 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 @@ -256,6 +256,9 @@ class EOCVSim(val params: Parameters = Parameters()) { pipelineStatisticsCalculator.avgOverheadTime ) } + + + updateVisualizerTitle() // update current pipeline in title } start() @@ -268,8 +271,6 @@ class EOCVSim(val params: Parameters = Parameters()) { //run all pending requested runnables onMainUpdate.run() - updateVisualizerTitle() - pipelineStatisticsCalculator.newInputFrameStart() inputSourceManager.update(pipelineManager.paused) @@ -434,8 +435,6 @@ class EOCVSim(val params: Parameters = Parameters()) { val workspaceMsg = " - ${workspaceManager.workspaceFile.absolutePath} $isBuildRunning" - val pipelineFpsMsg = " (${pipelineManager.pipelineFpsCounter.fps} Pipeline FPS)" - val posterFpsMsg = " (${visualizer.viewport} Viewport FPS)" val isPaused = if (pipelineManager.paused) " (Paused)" else "" val isRecording = if (isCurrentlyRecording()) " RECORDING" else "" diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java index 95cd21b4..54fc7a95 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java @@ -78,7 +78,6 @@ public void runOpMode() { if (opModeIsActive()) { while (opModeIsActive()) { - telemetryAprilTag(); // Push telemetry to the Driver Station. diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java b/Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java deleted file mode 100644 index 28640b7d..00000000 --- a/Vision/src/main/java/io/github/deltacv/vision/internal/util/KillException.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.github.deltacv.vision.internal.util; - -public class KillException extends RuntimeException { -} From 907fe5eea0df3d3c9cbceed4d3871cd3fd99a39c Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 19 Aug 2023 17:02:02 -0600 Subject: [PATCH 36/46] Make VisionProcessors be automatically detected alongside pipelines --- .../eocvsim/pipeline/PipelineManager.kt | 44 ++++++++++++++----- .../compiler/CompiledPipelineManager.kt | 2 +- .../pipeline/compiler/PipelineClassLoader.kt | 2 +- .../DefaultPipelineInstantiator.kt | 19 ++++++++ .../instantiator/PipelineInstantiator.kt | 11 +++++ .../processor/ProcessorInstantiator.kt | 29 ++++++++++++ .../processor/ProcessorPipeline.java | 34 ++++++++++++++ .../eocvsim/util/ClasspathScan.kt | 24 +++++++--- .../external/gui/SwingOpenCvViewport.kt | 1 - .../ftc/vision/VisionProcessorInternal.java | 2 +- .../apriltag/AprilTagProcessorImpl.java | 2 + 11 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java 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 b94273ea..741c53c7 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 @@ -27,8 +27,12 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler +import com.github.serivesmejia.eocvsim.pipeline.instantiator.DefaultPipelineInstantiator +import com.github.serivesmejia.eocvsim.pipeline.instantiator.PipelineInstantiator +import com.github.serivesmejia.eocvsim.pipeline.instantiator.processor.ProcessorInstantiator import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot +import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException @@ -39,10 +43,12 @@ import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator import kotlinx.coroutines.* import org.firstinspires.ftc.robotcore.external.Telemetry import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl +import org.firstinspires.ftc.vision.VisionProcessor import org.opencv.core.Mat import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.OpenCvViewport import org.openftc.easyopencv.processFrameInternal +import java.lang.RuntimeException import java.lang.reflect.Constructor import java.lang.reflect.Field import java.util.* @@ -129,6 +135,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi @JvmField val compiledPipelineManager = CompiledPipelineManager(this) private val pipelineHandlers = mutableListOf() + private val pipelineInstantiators = mutableMapOf, PipelineInstantiator>() //counting and tracking exceptions for logging and reporting purposes val pipelineExceptionTracker = PipelineExceptionTracker(this) @@ -138,6 +145,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi enum class PauseReason { USER_REQUESTED, IMAGE_ONE_ANALYSIS, NOT_PAUSED } + fun init() { logger.info("Initializing...") @@ -155,6 +163,11 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi logger.info("Found " + pipelines.size + " pipeline(s)") + // add instantiator for OpenCvPipeline + addInstantiator(OpenCvPipeline::class.java, DefaultPipelineInstantiator) + // add instantiator for VisionProcessor (wraps a VisionProcessor around an OpenCvPipeline) + addInstantiator(VisionProcessor::class.java, ProcessorInstantiator) + // changing to initial pipeline onUpdate.doOnce { if(compiledPipelineManager.isBuildRunning && staticSnapshot != null) @@ -287,7 +300,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi pipelineStatisticsCalculator.beforeProcessFrame() - val pipelineResult =currentPipeline?.processFrameInternal(inputMat) + val pipelineResult = currentPipeline?.processFrameInternal(inputMat) pipelineStatisticsCalculator.afterProcessFrame() @@ -436,10 +449,24 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi pipelineHandlers.add(handler) } + fun addInstantiator(instantiatorFor: Class<*>, instantiator: PipelineInstantiator) { + pipelineInstantiators.put(instantiatorFor, instantiator) + } + + fun getInstantiatorFor(clazz: Class<*>): PipelineInstantiator? { + for((instantiatorFor, instantiator) in pipelineInstantiators) { + if(ReflectUtil.hasSuperclass(clazz, instantiatorFor)) { + return instantiator + } + } + + return null + } + @Suppress("UNCHECKED_CAST") @JvmOverloads fun addPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) { try { - pipelines.add(PipelineData(source, C as Class)) + pipelines.add(PipelineData(source, C)) } catch (ex: Exception) { logger.warn("Error while adding pipeline class", ex) updateExceptionTracker(ex) @@ -527,7 +554,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi debugLogCalled("forceChangePipeline") - var constructor: Constructor<*> + val instantiator = getInstantiatorFor(pipelineClass) try { nextTelemetry = TelemetryImpl().apply { @@ -535,13 +562,8 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi addTransmissionReceiver(eocvSim.visualizer.telemetryPanel) } - try { //instantiate pipeline if it has a constructor of a telemetry parameter - constructor = pipelineClass.getConstructor(Telemetry::class.java) - nextPipeline = constructor.newInstance(nextTelemetry) as OpenCvPipeline - } catch (ex: NoSuchMethodException) { //instantiating with a constructor of no params - constructor = pipelineClass.getConstructor() - nextPipeline = constructor.newInstance() as OpenCvPipeline - } + nextPipeline = instantiator?.instantiate(pipelineClass, nextTelemetry) + ?: throw RuntimeException("No instantiator found for pipeline class ${pipelineClass.name}") logger.info("Instantiated pipeline class ${pipelineClass.name}") } catch (ex: NoSuchMethodException) { @@ -769,6 +791,6 @@ enum class PipelineFps(val fps: Int, val coolName: String) { } } -data class PipelineData(val source: PipelineSource, val clazz: Class) +data class PipelineData(val source: PipelineSource, val clazz: Class<*>) enum class PipelineSource { CLASSPATH, COMPILED_ON_RUNTIME } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index b06a5dfa..3945d87b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -224,7 +224,7 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { try { currentPipelineClassLoader = PipelineClassLoader(PIPELINES_OUTPUT_JAR) - val pipelines = mutableListOf>() + val pipelines = mutableListOf>() for(pipelineClass in currentPipelineClassLoader!!.pipelineClasses) { pipelines.add(pipelineClass) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt index b271bf46..fe1eb1bf 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt @@ -38,7 +38,7 @@ class PipelineClassLoader(pipelinesJar: File) : ClassLoader() { private val zipFile = ZipFile(pipelinesJar) private val loadedClasses = mutableMapOf>() - var pipelineClasses: List> + var pipelineClasses: List> private set init { 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 new file mode 100644 index 00000000..bd6049fb --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt @@ -0,0 +1,19 @@ +package com.github.serivesmejia.eocvsim.pipeline.instantiator + +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline + +object DefaultPipelineInstantiator : PipelineInstantiator { + + override fun instantiate(clazz: Class<*>, telemetry: Telemetry) = try { + //instantiate pipeline if it has a constructor of a telemetry parameter + val constructor = clazz.getConstructor(Telemetry::class.java) + constructor.newInstance(telemetry) as OpenCvPipeline + } catch (ex: NoSuchMethodException) { + //instantiating with a constructor of no params + val constructor = clazz.getConstructor() + constructor.newInstance() as OpenCvPipeline + } + +} \ 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 new file mode 100644 index 00000000..7b6ef3c9 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt @@ -0,0 +1,11 @@ +package com.github.serivesmejia.eocvsim.pipeline.instantiator + +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.openftc.easyopencv.OpenCvPipeline + +interface PipelineInstantiator { + + fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline + +} \ 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 new file mode 100644 index 00000000..4161fbc7 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt @@ -0,0 +1,29 @@ +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 org.firstinspires.ftc.robotcore.external.Telemetry +import org.firstinspires.ftc.vision.VisionProcessor +import org.openftc.easyopencv.OpenCvPipeline + +object ProcessorInstantiator : PipelineInstantiator { + + override fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline { + if(!ReflectUtil.hasSuperclass(clazz, VisionProcessor::class.java)) + throw IllegalArgumentException("Class $clazz does not extend VisionProcessor") + + val processor = try { + //instantiate pipeline if it has a constructor of a telemetry parameter + val constructor = clazz.getConstructor(Telemetry::class.java) + constructor.newInstance(telemetry) as VisionProcessor + } catch (ex: NoSuchMethodException) { + //instantiating with a constructor of no params + val constructor = clazz.getConstructor() + constructor.newInstance() as VisionProcessor + } + + return ProcessorPipeline(processor) + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java new file mode 100644 index 00000000..acfdd656 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java @@ -0,0 +1,34 @@ +package com.github.serivesmejia.eocvsim.pipeline.instantiator.processor; + +import android.graphics.Canvas; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import org.firstinspires.ftc.vision.VisionProcessor; +import org.opencv.core.Mat; +import org.openftc.easyopencv.TimestampedOpenCvPipeline; + +@Disabled +class ProcessorPipeline extends TimestampedOpenCvPipeline { + + private VisionProcessor processor; + + public ProcessorPipeline(VisionProcessor processor) { + this.processor = processor; + } + + @Override + public void init(Mat mat) { + processor.init(mat.width(), mat.height(), null); + } + + @Override + public Mat processFrame(Mat input, long captureTimeNanos) { + requestViewportDrawHook(processor.processFrame(input, captureTimeNanos)); + return input; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { + processor.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, userContext); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt index ef3c7897..dec7e900 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt @@ -30,6 +30,7 @@ import com.qualcomm.robotcore.eventloop.opmode.Disabled import com.qualcomm.robotcore.util.ElapsedTime import io.github.classgraph.ClassGraph import kotlinx.coroutines.* +import org.firstinspires.ftc.vision.VisionProcessor import org.openftc.easyopencv.OpenCvPipeline class ClasspathScan { @@ -58,7 +59,7 @@ class ClasspathScan { private lateinit var scanResultJob: Job @Suppress("UNCHECKED_CAST") - fun scan(jarFile: String? = null, classLoader: ClassLoader? = null): ScanResult { + fun scan(jarFile: String? = null, classLoader: ClassLoader? = null, addProcessorsAsPipelines: Boolean = true): ScanResult { val timer = ElapsedTime() val classGraph = ClassGraph() .enableClassInfo() @@ -79,12 +80,18 @@ class ClasspathScan { val tunableFieldClassesInfo = scanResult.getClassesWithAnnotation(RegisterTunableField::class.java.name) - val pipelineClasses = mutableListOf>() + val pipelineClasses = mutableListOf>() // i...don't even know how to name this, sorry, future readers // but classgraph for some reason does not have a recursive search for subclasses... fun searchPipelinesOfSuperclass(superclass: String) { - val pipelineClassesInfo = scanResult.getSubclasses(superclass) + val superclassClazz = if(classLoader != null) { + classLoader.loadClass(superclass) + } else Class.forName(superclass) + + val pipelineClassesInfo = if(superclassClazz.isInterface) + scanResult.getClassesImplementing(superclass) + else scanResult.getSubclasses(superclass) for(pipelineClassInfo in pipelineClassesInfo) { for(pipelineSubclassInfo in pipelineClassInfo.subclasses) { @@ -99,12 +106,12 @@ class ClasspathScan { classLoader.loadClass(pipelineClassInfo.name) } else Class.forName(pipelineClassInfo.name) - if(!pipelineClasses.contains(clazz) && ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) { + if(!pipelineClasses.contains(clazz) && ReflectUtil.hasSuperclass(clazz, superclassClazz)) { if(clazz.isAnnotationPresent(Disabled::class.java)) { logger.info("Found @Disabled pipeline ${clazz.typeName}") } else { logger.info("Found pipeline ${clazz.typeName}") - pipelineClasses.add(clazz as Class) + pipelineClasses.add(clazz) } } } @@ -113,6 +120,11 @@ class ClasspathScan { // start recursive hell searchPipelinesOfSuperclass(OpenCvPipeline::class.java.name) + if(addProcessorsAsPipelines) { + logger.info("Searching for VisionProcessors...") + searchPipelinesOfSuperclass(VisionProcessor::class.java.name) + } + logger.info("Found ${pipelineClasses.size} pipelines") val tunableFieldClasses = mutableListOf>>() @@ -166,7 +178,7 @@ class ClasspathScan { } data class ScanResult( - val pipelineClasses: Array>, + val pipelineClasses: Array>, val tunableFieldClasses: Array>>, val tunableFieldAcceptorClasses: Map>, Class> ) \ No newline at end of file diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt index 846c1897..34a341b4 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt @@ -24,7 +24,6 @@ package io.github.deltacv.vision.external.gui import android.graphics.Canvas import io.github.deltacv.common.image.MatPoster -import io.github.deltacv.vision.internal.util.KillException import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue import org.jetbrains.skia.Color import org.jetbrains.skiko.GenericSkikoView diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java index 620412a3..74202245 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java @@ -46,4 +46,4 @@ interface VisionProcessorInternal void init(int width, int height, CameraCalibration calibration); Object processFrame(Mat frame, long captureTimeNanos); void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext); -} +} \ No newline at end of file diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java index ea82e745..047538ae 100644 --- a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java +++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java @@ -35,6 +35,7 @@ import android.graphics.Canvas; +import com.qualcomm.robotcore.eventloop.opmode.Disabled; import com.qualcomm.robotcore.util.MovingStatistics; import org.firstinspires.ftc.robotcore.external.matrices.GeneralMatrixF; @@ -60,6 +61,7 @@ import java.util.ArrayList; +@Disabled public class AprilTagProcessorImpl extends AprilTagProcessor { public static final String TAG = "AprilTagProcessorImpl"; From c9dbea5791fdd1a2e0a44e437d0046c3ee3dddb5 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 19 Aug 2023 20:26:28 -0600 Subject: [PATCH 37/46] Fixed cyclical bugs --- .../PipelineOpModeSwitchablePanel.kt | 8 +- .../visualizer/opmode/OpModeControlsPanel.kt | 31 ++-- .../visualizer/opmode/OpModeSelectorPanel.kt | 32 ++-- .../eocvsim/input/InputSourceManager.java | 1 - .../eocvsim/pipeline/PipelineManager.kt | 4 +- .../samples/ConceptStackProcessor.java | 147 ------------------ .../teamcode/AprilTagProcessorPipeline.java | 39 ----- .../teamcode/processors/StackProcessor.java | 3 - .../external/source/VisionSourceBase.java | 1 - gradle/wrapper/gradle-wrapper.properties | 4 +- 10 files changed, 46 insertions(+), 224 deletions(-) delete mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java delete mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 2a8a65ce..97c4222f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -50,10 +50,10 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val index = sourceTabbedPane.selectedIndex if(index == 0) { - opModeSelectorPanel.reset(0) - pipelineSelectorPanel.isActive = true opModeSelectorPanel.isActive = false + + opModeSelectorPanel.reset(0) } else if(index == 1) { opModeSelectorPanel.reset() @@ -76,12 +76,12 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { fun enableSwitching() { pipelineSelectorPanel.allowPipelineSwitching = true - opModeSelectorPanel.allowOpModeSwitching = true + opModeSelectorPanel.isActive = true } fun disableSwitching() { pipelineSelectorPanel.allowPipelineSwitching = false - opModeSelectorPanel.allowOpModeSwitching = false + opModeSelectorPanel.isActive = false } fun enableSwitchingBlocking() = runBlocking { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt index e8ff4610..18255cbf 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -8,8 +8,6 @@ import io.github.deltacv.vision.internal.opmode.OpModeNotification import io.github.deltacv.vision.internal.opmode.OpModeState import java.awt.BorderLayout import javax.swing.JPanel -import java.awt.GridBagConstraints -import java.awt.GridBagLayout import javax.swing.JButton import javax.swing.SwingUtilities @@ -21,8 +19,9 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { private set private var currentManagerIndex: Int? = null + private var upcomingIndex: Int? = null - var allowOpModeSwitching = false + var isActive = false init { layout = BorderLayout() @@ -36,6 +35,8 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { eocvSim.pipelineManager.onUpdate.doOnce { if(eocvSim.pipelineManager.currentPipeline !is OpMode) return@doOnce + eocvSim.pipelineManager.setPaused(false, PipelineManager.PauseReason.NOT_PAUSED) + val opMode = eocvSim.pipelineManager.currentPipeline as OpMode val state = opMode.notifier.state @@ -50,17 +51,16 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { } fun stopCurrentOpMode() { - if(eocvSim.pipelineManager.currentPipeline !is OpMode) return - - val opMode = eocvSim.pipelineManager.currentPipeline as OpMode - opMode.notifier.notify(OpModeNotification.STOP) + if(eocvSim.pipelineManager.currentPipeline != currentOpMode || currentOpMode == null) return + currentOpMode!!.notifier.notify(OpModeNotification.STOP) } private fun notifySelected() { - if(!allowOpModeSwitching) return + if(!isActive) return if(eocvSim.pipelineManager.currentPipeline !is OpMode) return val opMode = eocvSim.pipelineManager.currentPipeline as OpMode + val opModeIndex = currentManagerIndex!! opMode.notifier.onStateChange { val state = opMode.notifier.state @@ -70,7 +70,10 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { } if(state == OpModeState.STOPPED) { - opModeSelected(currentManagerIndex!!) + if(isActive && opModeIndex == upcomingIndex) { + opModeSelected(currentManagerIndex!!) + } + it.removeThis() } } @@ -97,12 +100,16 @@ class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() { } fun opModeSelected(managerIndex: Int, forceChangePipeline: Boolean = true) { - eocvSim.pipelineManager.setPaused(false) + eocvSim.pipelineManager.requestSetPaused(false) + + if(forceChangePipeline) { + eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) + } - if(forceChangePipeline) eocvSim.pipelineManager.requestForceChangePipeline(managerIndex) - currentManagerIndex = managerIndex + upcomingIndex = managerIndex eocvSim.pipelineManager.onUpdate.doOnce { + currentManagerIndex = managerIndex notifySelected() } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index 79d3e406..f37e4a7e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -54,15 +54,12 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC val autonomousSelector = JList() val teleopSelector = JList() - var allowOpModeSwitching = false + var isActive = false set(value) { - opModeControlsPanel.allowOpModeSwitching = value + opModeControlsPanel.isActive = value field = value } - var isActive = false - internal set - init { layout = GridBagLayout() selectOpModeLabelsPanel.layout = GridBagLayout() @@ -168,7 +165,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC autonomousSelector.addMouseListener(object: MouseAdapter() { override fun mouseClicked(e: MouseEvent) { - if (!allowOpModeSwitching) return + if (!isActive) return val index = (e.source as JList<*>).locationToIndex(e.point) if(index >= 0) { @@ -179,7 +176,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC teleopSelector.addMouseListener(object: MouseAdapter() { override fun mouseClicked(e: MouseEvent) { - if (!allowOpModeSwitching) return + if (!isActive) return val index = (e.source as JList<*>).locationToIndex(e.point) if(index >= 0) { @@ -189,12 +186,17 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC }) eocvSim.pipelineManager.onPipelineChange { - // we are doing this to detect external pipeline changes + if(!isActive) return@onPipelineChange + + // we are doing this to detect external pipeline changes and reflect them + // accordingly in the UI. + // // in the event that this change was triggered by us, OpModeSelectorPanel, // we need to hold on a cycle so that the state has been fully updated, - // just to be able to check correctly. + // just to be able to check correctly and, if it was requested by + // OpModeSelectorPanel, skip this message and not do anything. eocvSim.pipelineManager.onUpdate.doOnce { - if(isActive && opModeControlsPanel.currentOpMode != eocvSim.pipelineManager.currentPipeline) { + if(isActive && opModeControlsPanel.currentOpMode != eocvSim.pipelineManager.currentPipeline && eocvSim.pipelineManager.currentPipeline != null) { val opMode = eocvSim.pipelineManager.currentPipeline if(opMode is OpMode) { @@ -205,7 +207,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC logger.info("External change detected \"$name\"") opModeSelected(eocvSim.pipelineManager.currentPipelineIndex, name, false) - } else { + } else if(isActive) { reset(-1) } } @@ -222,6 +224,8 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC } private fun opModeSelected(managerIndex: Int, name: String, forceChangePipeline: Boolean = true) { + if(!isActive) return + opModeNameLabel.text = name textPanel.removeAll() @@ -277,9 +281,9 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC opModeControlsPanel.reset() - if(eocvSim.pipelineManager.currentPipeline == opModeControlsPanel.currentOpMode) { - val opMode = opModeControlsPanel.currentOpMode + val opMode = opModeControlsPanel.currentOpMode + if(eocvSim.pipelineManager.currentPipeline == opMode && opMode != null && opMode.notifier.state != OpModeState.SELECTED) { opMode?.notifier?.onStateChange?.let { it { val state = opMode.notifier.state @@ -289,7 +293,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC if(nextPipeline == null || nextPipeline >= 0) { eocvSim.pipelineManager.onUpdate.doOnce { - eocvSim.pipelineManager.requestChangePipeline(nextPipeline) + eocvSim.pipelineManager.changePipeline(nextPipeline) } } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index b064fe2d..5cd65da5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -109,7 +109,6 @@ public void update(boolean isPaused) { Mat m = currentInputSource.update(); if(m != null && !m.empty()) { - lastMatFromSource.release(); m.copyTo(lastMatFromSource); // add an extra alpha channel because that's what eocv returns for some reason... (more realistic simulation lol) Imgproc.cvtColor(lastMatFromSource, lastMatFromSource, Imgproc.COLOR_RGB2RGBA); 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 741c53c7..3c627e6a 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 @@ -108,6 +108,8 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi return field } + var pauseOnImages = true + var pauseReason = PauseReason.NOT_PAUSED private set get() { @@ -615,7 +617,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi setPaused(false) //if pause on images option is turned on by user - if (eocvSim.configManager.config.pauseOnImages) { + if (eocvSim.configManager.config.pauseOnImages && pauseOnImages) { //pause next frame if current selected input source is an image eocvSim.inputSourceManager.pauseIfImageTwoFrames() } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java deleted file mode 100644 index 54fc7a95..00000000 --- a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptStackProcessor.java +++ /dev/null @@ -1,147 +0,0 @@ -/* Copyright (c) 2023 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.robotcontroller.external.samples; - -import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; -import com.qualcomm.robotcore.eventloop.opmode.TeleOp; -import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection; -import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; -import org.firstinspires.ftc.teamcode.processors.StackProcessor; -import org.firstinspires.ftc.vision.VisionPortal; -import org.firstinspires.ftc.vision.VisionProcessor; -import org.firstinspires.ftc.vision.apriltag.AprilTagDetection; -import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; - -import java.util.List; - -/** - * This 2023-2024 OpMode illustrates the basics of AprilTag recognition and pose estimation, - * including Java Builder structures for specifying Vision parameters. - * - * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. - * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list. - */ -@TeleOp(name = "Concept: Stack Processor", group = "Concept") -// @Disabled -public class ConceptStackProcessor extends LinearOpMode { - - private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera - - /** - * {@link #aprilTag} is the variable to store our instance of the AprilTag processor. - */ - private VisionProcessor aprilTag; - - /** - * {@link #visionPortal} is the variable to store our instance of the vision portal. - */ - private VisionPortal visionPortal; - - @Override - public void runOpMode() { - - initAprilTag(); - - // Wait for the DS start button to be touched. - telemetry.addData("DS preview on/off", "3 dots, Camera Stream"); - telemetry.addData(">", "Touch Play to start OpMode"); - telemetry.update(); - - waitForStart(); - - if (opModeIsActive()) { - while (opModeIsActive()) { - telemetryAprilTag(); - - // Push telemetry to the Driver Station. - telemetry.update(); - - // Share the CPU. - sleep(20); - } - } - - // Save more CPU resources when camera is no longer needed. - visionPortal.close(); - - } // end method runOpMode() - - /** - * Initialize the AprilTag processor. - */ - private void initAprilTag() { - - // Create the AprilTag processor. - aprilTag = new StackProcessor(); - - // Create the vision portal by using a builder. - VisionPortal.Builder builder = new VisionPortal.Builder(); - - // Set the camera (webcam vs. built-in RC phone camera). - if (USE_WEBCAM) { - builder.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1")); - } else { - builder.setCamera(BuiltinCameraDirection.BACK); - } - - // Choose a camera resolution. Not all cameras support all resolutions. - //builder.setCameraResolution(new Size(640, 480)); - - // Enable the RC preview (LiveView). Set "false" to omit camera monitoring. - //builder.enableCameraMonitoring(true); - - // Set the stream format; MJPEG uses less bandwidth than default YUY2. - //builder.setStreamFormat(VisionPortal.StreamFormat.YUY2); - - // Choose whether or not LiveView stops if no processors are enabled. - // If set "true", monitor shows solid orange screen if no processors enabled. - // If set "false", monitor shows camera view without annotations. - //builder.setAutoStopLiveView(false); - - // Set and enable the processor. - builder.addProcessor(aprilTag); - - // Build the Vision Portal, using the above settings. - visionPortal = builder.build(); - - // Disable or re-enable the aprilTag processor at any time. - //visionPortal.setProcessorEnabled(aprilTag, true); - - } // end method initAprilTag() - - - /** - * Function to add telemetry about AprilTag detections. - */ - private void telemetryAprilTag() { - - } // end method telemetryAprilTag() - -} // end class \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java deleted file mode 100644 index 2ade7b7a..00000000 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagProcessorPipeline.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.firstinspires.ftc.teamcode; - -import android.graphics.Canvas; -import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; -import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit; -import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration; -import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor; -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvPipeline; -import org.openftc.easyopencv.TimestampedOpenCvPipeline; - -public class AprilTagProcessorPipeline extends TimestampedOpenCvPipeline { - - AprilTagProcessor processor = new AprilTagProcessor.Builder() - .setOutputUnits(DistanceUnit.METER, AngleUnit.DEGREES) - .setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11) - .setDrawAxes(true) - .setDrawTagOutline(true) - .setDrawCubeProjection(true) - .build(); - - @Override - public void init(Mat firstFrame) { - processor.init(firstFrame.width(), firstFrame.height(), null); - } - - @Override - public Mat processFrame(Mat input, long captureTimeNanos) { - requestViewportDrawHook(processor.processFrame(input, captureTimeNanos)); - - return input; - } - - @Override - public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { - processor.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, userContext); - } - -} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java index 83737f19..defb5efb 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java @@ -62,9 +62,6 @@ public Object processFrame(Mat frame, long captureTimeNanos) { double avgHueValue = Core.mean(ringHSV).val[0]; - ring.release(); - ringHSV.release(); - if (avgHueValue > FOUR_STACK_HUE_THRESHOLD) { result = RingNumber.FOUR; } else if (avgHueValue > ONE_STACK_HUE_THRESHOLD) { diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java index 6147cdaa..d1d306be 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java @@ -1,7 +1,6 @@ package io.github.deltacv.vision.external.source; import io.github.deltacv.vision.external.util.Timestamped; -import io.github.deltacv.vision.internal.util.KillException; import org.opencv.core.Mat; import org.opencv.core.Size; import org.slf4j.Logger; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17b4076a..cf5dd7d2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists From 143e97a3f912290ae811fe1eabb99f812914e0ee Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sun, 20 Aug 2023 01:20:23 -0600 Subject: [PATCH 38/46] Make github actions support java 13 --- .github/workflows/build_ci.yml | 24 ++++---- .../serivesmejia/eocvsim/gui/Visualizer.java | 8 +-- .../gui/component/CollapsiblePanelX.kt | 55 +++++++++++++++++++ .../PipelineOpModeSwitchablePanel.kt | 33 +++++++++-- .../eocvsim/gui/util/ReflectTaskbar.kt | 1 + .../eocvsim/pipeline/PipelineManager.kt | 2 + .../external/samples/ConceptAprilTagEasy.java | 2 +- 7 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt diff --git a/.github/workflows/build_ci.yml b/.github/workflows/build_ci.yml index f927e63b..7d086724 100644 --- a/.github/workflows/build_ci.yml +++ b/.github/workflows/build_ci.yml @@ -8,11 +8,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 10 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle @@ -23,11 +23,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 8 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle @@ -38,11 +38,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 8 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 6cf13c23..2f0f5b30 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -33,7 +33,6 @@ import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel; import com.github.serivesmejia.eocvsim.gui.theme.Theme; -import com.github.serivesmejia.eocvsim.gui.util.ReflectTaskbar; import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompiler; import com.github.serivesmejia.eocvsim.util.event.EventHandler; import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher; @@ -94,10 +93,10 @@ public Visualizer(EOCVSim eocvSim) { } public void init(Theme theme) { - if(ReflectTaskbar.INSTANCE.isUsable()){ + if(Taskbar.isTaskbarSupported()){ try { //set icon for mac os (and other systems which do support this method) - ReflectTaskbar.INSTANCE.setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage()); + Taskbar.getTaskbar().setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage()); } catch (final UnsupportedOperationException e) { logger.warn("Setting the Taskbar icon image is not supported on this platform"); } catch (final SecurityException e) { @@ -134,7 +133,6 @@ public void init(Theme theme) { menuBar = new TopMenuBar(this, eocvSim); tunerMenuPanel = new JPanel(); - skiaPanel.add(tunerMenuPanel, BorderLayout.SOUTH); pipelineOpModeSwitchablePanel = new PipelineOpModeSwitchablePanel(eocvSim); pipelineOpModeSwitchablePanel.disableSwitching(); @@ -160,6 +158,7 @@ public void init(Theme theme) { rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS)); + pipelineOpModeSwitchablePanel.setBorder(new EmptyBorder(0, 0, 0, 0)); rightContainer.add(pipelineOpModeSwitchablePanel); /* @@ -172,6 +171,7 @@ public void init(Theme theme) { //global frame.getContentPane().setDropTarget(new InputSourceDropTarget(eocvSim)); + //frame.add(tunerMenuPanel, BorderLayout.SOUTH); frame.add(rightContainer, BorderLayout.EAST); //initialize other various stuff of the frame diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt new file mode 100644 index 00000000..58c6c386 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt @@ -0,0 +1,55 @@ +import java.awt.Color +import java.awt.Dimension +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.border.LineBorder +import javax.swing.border.TitledBorder + +class JCollapsiblePanel(title: String?, titleCol: Color?) : JPanel() { + private val border: TitledBorder + private var visibleSize: Dimension? = null + private var collapsible = true + + init { + border = TitledBorder(title) + border.titleColor = titleCol + border.border = LineBorder(Color.white) + setBorder(border) + + // as Titleborder has no access to the Label we fake the size data ;) + val l = JLabel(title) + val size = l.getPreferredSize() + + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (!collapsible) { + return + } + val i = getBorder().getBorderInsets(this@JCollapsiblePanel) + if (e.x < i.left + size.width && e.y < i.bottom + size.height) { + if (visibleSize == null || height > size.height) { + visibleSize = getSize() + } + if (getSize().height < visibleSize!!.height) { + maximumSize = Dimension(visibleSize!!.width, 20000) + minimumSize = visibleSize + } else { + maximumSize = Dimension(visibleSize!!.width, size.height) + } + revalidate() + e.consume() + } + } + }) + } + + fun setCollapsible(collapsible: Boolean) { + this.collapsible = collapsible + } + + fun setTitle(title: String?) { + border.title = title + } +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 97c4222f..516d479f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -8,7 +8,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.swing.Swing +import java.awt.GridBagConstraints +import java.awt.GridBagLayout import java.awt.GridLayout +import javax.swing.BoxLayout import javax.swing.JPanel import javax.swing.JTabbedPane import javax.swing.border.EmptyBorder @@ -26,13 +29,28 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { val opModeSelectorPanel = OpModeSelectorPanel(eocvSim, opModeControlsPanel) init { - pipelinePanel.layout = GridLayout(2, 1) + pipelinePanel.layout = GridBagLayout() - pipelineSelectorPanel.border = EmptyBorder(0, 20, 20, 20) - pipelinePanel.add(pipelineSelectorPanel) + pipelineSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + pipelinePanel.add(pipelineSelectorPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 0 - sourceSelectorPanel.border = EmptyBorder(0, 20, 20, 20) - pipelinePanel.add(sourceSelectorPanel) + weightx = 1.0 + weighty = 1.0 + fill = GridBagConstraints.BOTH + }) + + sourceSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + + pipelinePanel.add(sourceSelectorPanel, GridBagConstraints().apply { + gridx = 0 + gridy = 1 + + weightx = 1.0 + weighty = 1.0 + fill = GridBagConstraints.BOTH + }) opModePanel.layout = GridLayout(2, 1) @@ -42,7 +60,10 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { opModeControlsPanel.border = EmptyBorder(0, 20, 20, 20) opModePanel.add(opModeControlsPanel) - add("Pipeline", pipelinePanel) + add("Pipeline", JPanel().apply { + layout = BoxLayout(this, BoxLayout.LINE_AXIS) + add(pipelinePanel) + }) add("OpMode", opModePanel) addChangeListener { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt index f9c68161..c70c3cc2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt @@ -3,6 +3,7 @@ package com.github.serivesmejia.eocvsim.gui.util import java.awt.Image import java.lang.reflect.InvocationTargetException +@Deprecated("Use JDK 9 Taskbar API instead, this used to be a workaround when EOCV-Sim targeted JDK 8 before v3.4.0") object ReflectTaskbar { private val taskbarClass by lazy { Class.forName("java.awt.Taskbar") } 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 3c627e6a..eda0b060 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 @@ -596,6 +596,8 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi currentPipelineIndex = index currentPipelineName = currentPipeline!!.javaClass.simpleName + currentTelemetry?.update() // clear telemetry + val snap = PipelineSnapshot(currentPipeline!!, snapshotFieldFilter) lastInitialSnapshot = if(applyLatestSnapshot) { diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java index 6f457e5f..6af33365 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java @@ -103,7 +103,7 @@ private void initAprilTag() { // Create the vision portal the easy way. if (USE_WEBCAM) { visionPortal = VisionPortal.easyCreateWithDefaults( - hardwareMap.get(WebcamName.class, "C:\\Users\\s3riv\\Downloads\\IMG_6112.avi"), aprilTag); + hardwareMap.get(WebcamName.class, "Webcam 1"), aprilTag); } else { visionPortal = VisionPortal.easyCreateWithDefaults( BuiltinCameraDirection.BACK, aprilTag); From 235a2ce0138e925380a403c9590bc6cb42059fca Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 20 Aug 2023 02:38:02 -0600 Subject: [PATCH 39/46] UI redesigns - delete title labels and add collapsible variable tuner --- .../serivesmejia/eocvsim/gui/Visualizer.java | 21 +++++++-- .../gui/component/CollapsiblePanelX.kt | 31 +++++++++---- .../PipelineOpModeSwitchablePanel.kt | 5 +- .../component/visualizer/TelemetryPanel.kt | 10 ++-- .../gui/component/visualizer/TopMenuBar.kt | 14 ++---- .../pipeline/PipelineSelectorPanel.kt | 12 ++--- .../{ => pipeline}/SourceSelectorPanel.kt | 15 +++--- .../eocvsim/input/InputSourceManager.java | 3 +- .../eocvsim/pipeline/DefaultPipeline.java | 46 ++++++++++--------- .../easyopencv/OpenCvViewRenderer.java | 2 +- 10 files changed, 91 insertions(+), 68 deletions(-) rename EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/{ => pipeline}/SourceSelectorPanel.kt (94%) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 2f0f5b30..faa8c32e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -26,8 +26,10 @@ import com.formdev.flatlaf.FlatLaf; import com.github.serivesmejia.eocvsim.Build; import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.CollapsiblePanelX; import com.github.serivesmejia.eocvsim.gui.component.visualizer.*; import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.SourceSelectorPanel; import io.github.deltacv.vision.external.gui.SwingOpenCvViewport; import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker; import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; @@ -64,7 +66,7 @@ public class Visualizer { public SwingOpenCvViewport viewport = null; public TopMenuBar menuBar = null; - public JPanel tunerMenuPanel = new JPanel(); + public JPanel tunerMenuPanel; public JPanel rightContainer = null; @@ -77,6 +79,8 @@ public class Visualizer { public TelemetryPanel telemetryPanel; + public JPanel tunerCollapsible; + private String title = "EasyOpenCV Simulator v" + Build.standardVersionString; private String titleMsg = "No pipeline"; private String beforeTitle = ""; @@ -171,7 +175,14 @@ public void init(Theme theme) { //global frame.getContentPane().setDropTarget(new InputSourceDropTarget(eocvSim)); - //frame.add(tunerMenuPanel, BorderLayout.SOUTH); + tunerCollapsible = new CollapsiblePanelX("Tuner", null); + tunerCollapsible.setLayout(new BoxLayout(tunerCollapsible, BoxLayout.LINE_AXIS)); + tunerCollapsible.setVisible(false); + + JScrollPane tunerScrollPane = new JScrollPane(tunerMenuPanel); + tunerCollapsible.add(tunerScrollPane); + + frame.add(tunerCollapsible, BorderLayout.SOUTH); frame.add(rightContainer, BorderLayout.EAST); //initialize other various stuff of the frame @@ -329,6 +340,8 @@ public void updateTunerFields(List fields) { tunerMenuPanel.add(fieldPanel); fieldPanel.showFieldPanel(); } + + tunerCollapsible.setVisible(!fields.isEmpty()); } public void asyncCompilePipelines() { @@ -349,7 +362,7 @@ public void asyncCompilePipelines() { public void compilingUnsupported() { asyncPleaseWaitDialog( - "Runtime compiling is not supported on this JVM", + "Runtime pipeline builds are not supported on this JVM", "For further info, check the EOCV-Sim GitHub repo", "Close", new Dimension(320, 160), @@ -397,7 +410,7 @@ public void createVSCodeWorkspace() { } public void askOpenVSCode() { - DialogFactory.createYesOrNo(frame, "A new workspace was created. Do you wanna open VS Code?", "", + DialogFactory.createYesOrNo(frame, "A new workspace was created. Do you want to open VS Code?", "", (result) -> { if(result == 0) { VSCodeLauncher.INSTANCE.asyncLaunch(eocvSim.workspaceManager.getWorkspaceFile()); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt index 58c6c386..d2a00f34 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt @@ -1,3 +1,5 @@ +package com.github.serivesmejia.eocvsim.gui.component + import java.awt.Color import java.awt.Dimension import java.awt.event.MouseAdapter @@ -7,13 +9,17 @@ import javax.swing.JPanel import javax.swing.border.LineBorder import javax.swing.border.TitledBorder -class JCollapsiblePanel(title: String?, titleCol: Color?) : JPanel() { +class CollapsiblePanelX(title: String?, titleCol: Color?) : JPanel() { private val border: TitledBorder private var visibleSize: Dimension? = null private var collapsible = true + var isHidden = false + private set + init { border = TitledBorder(title) + border.titleColor = titleCol border.border = LineBorder(Color.white) setBorder(border) @@ -27,17 +33,22 @@ class JCollapsiblePanel(title: String?, titleCol: Color?) : JPanel() { if (!collapsible) { return } - val i = getBorder().getBorderInsets(this@JCollapsiblePanel) + + val i = getBorder().getBorderInsets(this@CollapsiblePanelX) if (e.x < i.left + size.width && e.y < i.bottom + size.height) { - if (visibleSize == null || height > size.height) { - visibleSize = getSize() - } - if (getSize().height < visibleSize!!.height) { - maximumSize = Dimension(visibleSize!!.width, 20000) - minimumSize = visibleSize - } else { - maximumSize = Dimension(visibleSize!!.width, size.height) + + for(e in components) { + e.isVisible = !isHidden + + border.title = if(isHidden) { + "$title (hidden)" + } else { + title + } + + isHidden = !isHidden } + revalidate() e.consume() } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 516d479f..486eb1d1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -4,6 +4,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeControlsPanel import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel +import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.SourceSelectorPanel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -31,7 +32,7 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { init { pipelinePanel.layout = GridBagLayout() - pipelineSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + pipelineSelectorPanel.border = EmptyBorder(20, 20, 0, 20) pipelinePanel.add(pipelineSelectorPanel, GridBagConstraints().apply { gridx = 0 gridy = 0 @@ -41,7 +42,7 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { fill = GridBagConstraints.BOTH }) - sourceSelectorPanel.border = EmptyBorder(0, 20, 0, 20) + sourceSelectorPanel.border = EmptyBorder(0, 20, -10, 20) pipelinePanel.add(sourceSelectorPanel, GridBagConstraints().apply { gridx = 0 diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt index e489f25e..d95ef6dc 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt @@ -27,10 +27,10 @@ class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver { telemetryLabel.font = telemetryLabel.font.deriveFont(20.0f) telemetryLabel.horizontalAlignment = JLabel.CENTER - add(telemetryLabel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) + // add(telemetryLabel, GridBagConstraints().apply { + // gridy = 0 + // ipady = 20 + //}) telemetryScroll.setViewportView(telemetryList) telemetryScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS @@ -52,7 +52,7 @@ class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver { telemetryList.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION add(telemetryScroll, GridBagConstraints().apply { - gridy = 1 + gridy = 0 weightx = 0.5 weighty = 1.0 diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index 0679428e..8159bd8b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -49,7 +49,6 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { @JvmField val mFileMenu = JMenu("File") @JvmField val mWorkspMenu = JMenu("Workspace") - @JvmField val mEditMenu = JMenu("Edit") @JvmField val mHelpMenu = JMenu("Help") @JvmField val workspCompile = JMenuItem("Build java files") @@ -90,6 +89,11 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { mFileMenu.addSeparator() + val editSettings = JMenuItem("Settings") + editSettings.addActionListener { DialogFactory.createConfigDialog(eocvSim) } + + mFileMenu.add(editSettings) + val fileRestart = JMenuItem("Restart") fileRestart.addActionListener { eocvSim.onMainUpdate.doOnce { eocvSim.restart() } } @@ -137,14 +141,6 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { add(mWorkspMenu) - // EDIT - - val editSettings = JMenuItem("Settings") - editSettings.addActionListener { DialogFactory.createConfigDialog(eocvSim) } - - mEditMenu.add(editSettings) - add(mEditMenu) - // HELP val helpUsage = JMenuItem("Documentation") diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index 593af4be..55d64690 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -79,10 +79,10 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { pipelineSelectorLabel.horizontalAlignment = JLabel.CENTER - add(pipelineSelectorLabel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) + //add(pipelineSelectorLabel, GridBagConstraints().apply { + // gridy = 0 + // ipady = 20 + //}) pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) pipelineSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION @@ -92,7 +92,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { pipelineSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED add(pipelineSelectorScroll, GridBagConstraints().apply { - gridy = 1 + gridy = 0 weightx = 0.5 weighty = 1.0 @@ -103,7 +103,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { }) add(buttonsPanel, GridBagConstraints().apply { - gridy = 2 + gridy = 1 ipady = 20 }) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt similarity index 94% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt index 12830556..cd4d77df 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt @@ -1,7 +1,8 @@ -package com.github.serivesmejia.eocvsim.gui.component.visualizer +package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.component.PopupX +import com.github.serivesmejia.eocvsim.gui.component.visualizer.CreateSourcePanel import com.github.serivesmejia.eocvsim.gui.util.icon.SourcesListIconRenderer import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero @@ -37,10 +38,10 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { sourceSelectorLabel.font = sourceSelectorLabel.font.deriveFont(20.0f) sourceSelectorLabel.horizontalAlignment = JLabel.CENTER - add(sourceSelectorLabel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) + // add(sourceSelectorLabel, GridBagConstraints().apply { + // gridy = 0 + // ipady = 20 + //}) sourceSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION @@ -49,7 +50,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { sourceSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED add(sourceSelectorScroll, GridBagConstraints().apply { - gridy = 1 + gridy = 0 weightx = 0.5 weighty = 1.0 @@ -82,7 +83,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { sourceSelectorButtonsContainer.add(sourceSelectorDeleteBtt) add(sourceSelectorButtonsContainer, GridBagConstraints().apply { - gridy = 2 + gridy = 1 ipady = 20 }) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 5cd65da5..c9cc01df 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -25,9 +25,8 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.Visualizer; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.SourceSelectorPanel; import com.github.serivesmejia.eocvsim.input.source.ImageSource; -import com.github.serivesmejia.eocvsim.input.source.VideoSource; import com.github.serivesmejia.eocvsim.pipeline.PipelineManager; import com.github.serivesmejia.eocvsim.util.SysUtil; import org.opencv.core.Mat; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java index 121321f2..3ed276ca 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java @@ -23,6 +23,10 @@ package com.github.serivesmejia.eocvsim.pipeline; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; import org.firstinspires.ftc.robotcore.external.Telemetry; import org.opencv.core.*; import org.opencv.imgproc.Imgproc; @@ -34,8 +38,20 @@ public class DefaultPipeline extends OpenCvPipeline { private Telemetry telemetry; + private Paint boxPaint; + private Paint textPaint; + public DefaultPipeline(Telemetry telemetry) { this.telemetry = telemetry; + + textPaint = new Paint(); + textPaint.setColor(Color.WHITE); + textPaint.setTextSize(30); + textPaint.setAntiAlias(true); + + boxPaint = new Paint(); + boxPaint.setColor(Color.BLACK); + boxPaint.setStyle(Paint.Style.FILL); } @Override @@ -51,32 +67,18 @@ public Mat processFrame(Mat input) { if (blur > 0 && blur % 2 == 1) { Imgproc.GaussianBlur(input, input, new Size(blur, blur), 0); + } else if (blur > 0) { + Imgproc.GaussianBlur(input, input, new Size(blur + 1, blur + 1), 0); } - // Outline - Imgproc.putText( - input, - "Default pipeline selected", - new Point(0, 22 * aspectRatioPercentage), - Imgproc.FONT_HERSHEY_PLAIN, - 2 * aspectRatioPercentage, - new Scalar(255, 255, 255), - (int) Math.round(5 * aspectRatioPercentage) - ); - - //Text - Imgproc.putText( - input, - "Default pipeline selected", - new Point(0, 22 * aspectRatioPercentage), - Imgproc.FONT_HERSHEY_PLAIN, - 2 * aspectRatioPercentage, - new Scalar(0, 0, 0), - (int) Math.round(2 * aspectRatioPercentage) - ); - return input; + } + + @Override + public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { + canvas.drawRect(new Rect(0, 0, 385, 45), boxPaint); + canvas.drawText("Default pipeline selected", 5, 33, textPaint); } } diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java index c93c0588..457d6cfc 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -74,7 +74,7 @@ public OpenCvViewRenderer(boolean renderingOffsceen, String fpsMeterDescriptor) metricsScale = 1.0f; - fpsMeterTextSize = 30 * metricsScale; + fpsMeterTextSize = 26 * metricsScale; statBoxW = (int) (450 * metricsScale); statBoxH = (int) (120 * metricsScale); statBoxTextLineSpacing = (int) (35 * metricsScale); From 05e462add9b69594588ba32883701e0f06ef0fad Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 20 Aug 2023 13:11:57 -0700 Subject: [PATCH 40/46] Major UI upgrades, implement Paint Style & make VisionProcessors work with the variable tuner --- .../serivesmejia/eocvsim/gui/Visualizer.java | 14 ++++- .../gui/component/CollapsiblePanelX.kt | 48 +++++++++++++--- .../eocvsim/gui/component/PopupX.kt | 55 ++++++++++++------- .../PipelineOpModeSwitchablePanel.kt | 15 ++++- .../component/visualizer/TelemetryPanel.kt | 6 ++ .../visualizer/opmode/OpModeControlsPanel.kt | 23 ++++++++ .../visualizer/opmode/OpModePopupPanel.kt | 24 +++++++- .../visualizer/opmode/OpModeSelectorPanel.kt | 30 ++++++++-- .../pipeline/PipelineSelectorButtonsPanel.kt | 26 ++++++++- .../serivesmejia/eocvsim/gui/util/Enums.kt | 5 +- ...PosterImpl.java => ThreadedMatPoster.java} | 12 ++-- .../gui/util/icon/PipelineListIconRenderer.kt | 14 ++--- .../eocvsim/input/InputSourceManager.java | 2 +- .../eocvsim/output/VideoRecordingSession.kt | 4 +- .../eocvsim/pipeline/DefaultPipeline.java | 6 +- .../eocvsim/pipeline/PipelineManager.kt | 3 + .../pipeline/handler/PipelineHandler.kt | 23 ++++++++ .../handler/SpecificPipelineHandler.kt | 23 ++++++++ .../DefaultPipelineInstantiator.kt | 26 ++++++++- .../instantiator/PipelineInstantiator.kt | 25 +++++++++ .../processor/ProcessorInstantiator.kt | 27 ++++++++- .../processor/ProcessorPipeline.java | 25 ++++++++- .../eocvsim/tuner/TunableField.java | 16 +++--- .../eocvsim/tuner/TunerManager.java | 13 ++--- .../eocvsim/tuner/field/BooleanField.java | 6 +- .../eocvsim/tuner/field/EnumField.kt | 10 ++-- .../eocvsim/tuner/field/NumericField.java | 6 +- .../eocvsim/tuner/field/StringField.java | 4 +- .../eocvsim/tuner/field/cv/PointField.java | 5 +- .../eocvsim/tuner/field/cv/RectField.kt | 6 +- .../eocvsim/tuner/field/cv/ScalarField.java | 6 +- .../tuner/field/numeric/DoubleField.java | 4 +- .../tuner/field/numeric/FloatField.java | 4 +- .../tuner/field/numeric/IntegerField.java | 4 +- .../tuner/field/numeric/LongField.java | 4 +- .../teamcode/processors/StackProcessor.java | 10 ++-- .../src/main/java/android/graphics/Paint.java | 18 +++++- .../eventloop/opmode/LinearOpMode.java | 31 +++++++++++ .../vision/external/PipelineRenderHook.kt | 25 ++++++++- .../vision/external/SourcedOpenCvCamera.java | 25 ++++++++- .../external/gui/SwingOpenCvViewport.kt | 14 ++--- .../external/source/ThreadSourceHander.java | 23 ++++++++ .../source/ViewportAndSourceHander.java | 23 ++++++++ .../vision/external/source/VisionSource.java | 23 ++++++++ .../external/source/VisionSourceBase.java | 23 ++++++++ .../external/source/VisionSourceHander.java | 23 ++++++++ .../vision/external/source/VisionSourced.java | 23 ++++++++ .../vision/external/util/FrameQueue.java | 24 +++++++- .../vision/external/util/Timestamped.java | 23 ++++++++ .../vision/internal/opmode/OpModeNotifier.kt | 27 ++++++++- .../source/ftc/SourcedCameraName.java | 24 +++++++- .../source/ftc/SourcedCameraNameImpl.java | 23 ++++++++ .../openftc/easyopencv/OpenCvPipeline.java | 2 - .../SourcedOpenCvCameraFactoryImpl.java | 23 ++++++++ 54 files changed, 796 insertions(+), 135 deletions(-) rename EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/{MatPosterImpl.java => ThreadedMatPoster.java} (93%) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index faa8c32e..bab7a8b2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -169,17 +169,25 @@ public void init(Theme theme) { * TELEMETRY */ - telemetryPanel.setBorder(new EmptyBorder(0, 20, 20, 20)); - rightContainer.add(telemetryPanel); + JPanel telemetryWithInsets = new JPanel(); + telemetryWithInsets.setLayout(new BoxLayout(telemetryWithInsets, BoxLayout.LINE_AXIS)); + telemetryWithInsets.setBorder(new EmptyBorder(0, 20, 20, 20)); + + telemetryWithInsets.add(telemetryPanel); + + rightContainer.add(telemetryWithInsets); //global frame.getContentPane().setDropTarget(new InputSourceDropTarget(eocvSim)); - tunerCollapsible = new CollapsiblePanelX("Tuner", null); + tunerCollapsible = new CollapsiblePanelX("Tuner", null, null); tunerCollapsible.setLayout(new BoxLayout(tunerCollapsible, BoxLayout.LINE_AXIS)); tunerCollapsible.setVisible(false); JScrollPane tunerScrollPane = new JScrollPane(tunerMenuPanel); + tunerScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + tunerScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + tunerCollapsible.add(tunerScrollPane); frame.add(tunerCollapsible, BorderLayout.SOUTH); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt index d2a00f34..0132cae5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2023 Sebastian Erives + * Credit where it's due - based off of https://stackoverflow.com/a/52956783 + * + * 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.github.serivesmejia.eocvsim.gui.component import java.awt.Color @@ -9,23 +33,33 @@ import javax.swing.JPanel import javax.swing.border.LineBorder import javax.swing.border.TitledBorder -class CollapsiblePanelX(title: String?, titleCol: Color?) : JPanel() { +class CollapsiblePanelX @JvmOverloads constructor( + title: String?, + titleCol: Color?, + borderCol: Color? = Color.white +) : JPanel() { private val border: TitledBorder - private var visibleSize: Dimension? = null private var collapsible = true var isHidden = false private set init { - border = TitledBorder(title) + val titleAndDescriptor = if(isHidden) { + "$title (click here to expand)" + } else { + "$title (click here to hide)" + } + + border = TitledBorder(titleAndDescriptor) border.titleColor = titleCol - border.border = LineBorder(Color.white) + border.border = LineBorder(borderCol) + setBorder(border) // as Titleborder has no access to the Label we fake the size data ;) - val l = JLabel(title) + val l = JLabel(titleAndDescriptor) val size = l.getPreferredSize() addMouseListener(object : MouseAdapter() { @@ -41,9 +75,9 @@ class CollapsiblePanelX(title: String?, titleCol: Color?) : JPanel() { e.isVisible = !isHidden border.title = if(isHidden) { - "$title (hidden)" + "$title (click here to expand)" } else { - title + "$title (click here to hide)" } isHidden = !isHidden diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt index d66c1344..d9d7e590 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt @@ -23,8 +23,9 @@ package com.github.serivesmejia.eocvsim.gui.component -import com.github.serivesmejia.eocvsim.gui.util.Location +import com.github.serivesmejia.eocvsim.gui.util.Corner import com.github.serivesmejia.eocvsim.util.event.EventHandler +import java.awt.Point import java.awt.Window import java.awt.event.KeyAdapter import java.awt.event.KeyEvent @@ -97,32 +98,46 @@ class PopupX @JvmOverloads constructor(windowAncestor: Window, companion object { - fun JComponent.popUpXOnThis(panel: JPanel, - popupLocation: Location = Location.TOP, - closeOnFocusLost: Boolean = true, - fixX: Boolean = false, - fixY: Boolean = true): PopupX { + fun JComponent.popUpXOnThis( + panel: JPanel, + buttonCorner: Corner = Corner.TOP_LEFT, + popupCorner: Corner = Corner.BOTTOM_LEFT, + closeOnFocusLost: Boolean = true + ): PopupX { val frame = SwingUtilities.getWindowAncestor(this) val location = locationOnScreen - val popup = PopupX(frame, panel, location.x, - if(popupLocation == Location.TOP) location.y else location.y + height, - closeOnFocusLost, fixX, fixY + val cornerLocation: Point = when(buttonCorner) { + Corner.TOP_LEFT -> Point(location.x, location.y) + Corner.TOP_RIGHT -> Point(location.x + width, location.y) + Corner.BOTTOM_LEFT -> Point(location.x, location.y + height) + Corner.BOTTOM_RIGHT -> Point(location.x + width, location.y + height) + } + + val popup = PopupX(frame, panel, + cornerLocation.x, + cornerLocation.y, + closeOnFocusLost ) popup.onShow { - popup.setLocation( - popup.window.location.x - width / 3, - if(popupLocation == Location.TOP) popup.window.location.y else popup.window.location.y + popup.window.height - ) - - val topRightPointX = popup.window.location.x + popup.window.width - - if(topRightPointX > frame.width) { - popup.setLocation( - popup.window.location.x - ((topRightPointX - frame.width) / 2), - popup.window.location.y + when(popupCorner) { + Corner.TOP_LEFT -> popup.setLocation( + popup.window.location.x, + popup.window.location.y + popup.window.height + ) + Corner.TOP_RIGHT -> popup.setLocation( + popup.window.location.x - popup.window.width, + popup.window.location.y + popup.window.height + ) + Corner.BOTTOM_LEFT -> popup.setLocation( + popup.window.location.x + width, + popup.window.location.y + popup.window.height + ) + Corner.BOTTOM_RIGHT -> popup.setLocation( + popup.window.location.x - popup.window.width, + popup.window.location.y + popup.window.height ) } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt index 486eb1d1..7b8c21af 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt @@ -12,10 +12,12 @@ import kotlinx.coroutines.swing.Swing import java.awt.GridBagConstraints import java.awt.GridBagLayout import java.awt.GridLayout +import java.awt.Insets import javax.swing.BoxLayout import javax.swing.JPanel import javax.swing.JTabbedPane import javax.swing.border.EmptyBorder +import javax.swing.border.TitledBorder class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { @@ -32,7 +34,10 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { init { pipelinePanel.layout = GridBagLayout() - pipelineSelectorPanel.border = EmptyBorder(20, 20, 0, 20) + pipelineSelectorPanel.border = TitledBorder("Pipelines").apply { + border = EmptyBorder(0, 0, 0, 0) + } + pipelinePanel.add(pipelineSelectorPanel, GridBagConstraints().apply { gridx = 0 gridy = 0 @@ -40,9 +45,13 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { weightx = 1.0 weighty = 1.0 fill = GridBagConstraints.BOTH + + insets = Insets(10, 20, 5, 20) }) - sourceSelectorPanel.border = EmptyBorder(0, 20, -10, 20) + sourceSelectorPanel.border = TitledBorder("Sources").apply { + border = EmptyBorder(0, 0, 0, 0) + } pipelinePanel.add(sourceSelectorPanel, GridBagConstraints().apply { gridx = 0 @@ -51,6 +60,8 @@ class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() { weightx = 1.0 weighty = 1.0 fill = GridBagConstraints.BOTH + + insets = Insets(-5, 20, -10, 20) }) opModePanel.layout = GridLayout(2, 1) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt index d95ef6dc..7cc470a1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt @@ -9,6 +9,8 @@ import java.awt.GridLayout import java.awt.event.MouseEvent import java.awt.event.MouseMotionListener import javax.swing.* +import javax.swing.border.EmptyBorder +import javax.swing.border.TitledBorder class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver { @@ -18,6 +20,10 @@ class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver { val telemetryLabel = JLabel("Telemetry") init { + border = TitledBorder("Telemetry").apply { + border = EmptyBorder(0, 0, 0, 0) + } + layout = GridBagLayout() /* diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt index 18255cbf..5c01cedc 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode import com.github.serivesmejia.eocvsim.EOCVSim diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt index 0de78203..7c5637a2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt @@ -1,6 +1,28 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode -import javax.swing.JButton import javax.swing.JList import javax.swing.JPanel import javax.swing.JScrollPane diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index f37e4a7e..4077a6f8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -1,9 +1,32 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnThis -import com.github.serivesmejia.eocvsim.gui.util.Location +import com.github.serivesmejia.eocvsim.gui.util.Corner import com.github.serivesmejia.eocvsim.pipeline.PipelineData import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.loggerForThis @@ -16,7 +39,6 @@ import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.* - class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeControlsPanel) : JPanel() { private var _selectedIndex = -1 @@ -126,7 +148,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC private fun registerListeners() { autonomousButton.addActionListener { - val popup = autonomousButton.popUpXOnThis(OpModePopupPanel(autonomousSelector), Location.BOTTOM) + val popup = autonomousButton.popUpXOnThis(OpModePopupPanel(autonomousSelector), Corner.BOTTOM_LEFT, Corner.TOP_LEFT) opModeControlsPanel.stopCurrentOpMode() @@ -145,7 +167,7 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC } teleopButton.addActionListener { - val popup = teleopButton.popUpXOnThis(OpModePopupPanel(teleopSelector), Location.BOTTOM) + val popup = teleopButton.popUpXOnThis(OpModePopupPanel(teleopSelector), Corner.BOTTOM_RIGHT, Corner.TOP_RIGHT) opModeControlsPanel.stopCurrentOpMode() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt index 11bd34e7..45f8ba0b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt @@ -24,6 +24,7 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.component.PopupX import java.awt.GridBagConstraints import java.awt.GridBagLayout @@ -98,13 +99,34 @@ class PipelineSelectorButtonsPanel(eocvSim: EOCVSim) : JPanel(GridBagLayout()) { }) // WORKSPACE BUTTONS POPUP + pipelineCompileBtt.addActionListener { eocvSim.visualizer.asyncCompilePipelines() } - workspaceButtonsPanel.add(pipelineCompileBtt, GridBagConstraints()) + workspaceButtonsPanel.add(pipelineCompileBtt, GridBagConstraints().apply { + gridx = 0 + gridy = 0 + }) val selectWorkspBtt = JButton("Select workspace") selectWorkspBtt.addActionListener { eocvSim.visualizer.selectPipelinesWorkspace() } - workspaceButtonsPanel.add(selectWorkspBtt, GridBagConstraints().apply { gridx = 1 }) + workspaceButtonsPanel.add(selectWorkspBtt, GridBagConstraints().apply { + gridx = 1 + gridy = 0 + }) + + val outputBtt = JButton("Pipeline Output") + + outputBtt.addActionListener { DialogFactory.createPipelineOutput(eocvSim) } + workspaceButtonsPanel.add(outputBtt, GridBagConstraints().apply { + gridy = 1 + weightx = 1.0 + gridwidth = 2 + + fill = GridBagConstraints.HORIZONTAL + anchor = GridBagConstraints.CENTER + + insets = Insets(3, 0, 0, 0) + }) } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt index 5dc84534..a308cb46 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt @@ -1,3 +1,6 @@ package com.github.serivesmejia.eocvsim.gui.util -enum class Location { TOP, BOTTOM } \ No newline at end of file +enum class Corner { + TOP_LEFT, TOP_RIGHT, + BOTTOM_LEFT, BOTTOM_RIGHT +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java similarity index 93% rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java index 86b65e18..d38c12fe 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPosterImpl.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java @@ -34,7 +34,7 @@ import java.util.ArrayList; import java.util.concurrent.ArrayBlockingQueue; -public class MatPosterImpl implements MatPoster { +public class ThreadedMatPoster implements MatPoster { private final ArrayList postables = new ArrayList<>(); private final EvictingBlockingQueue postQueue; @@ -54,19 +54,19 @@ public class MatPosterImpl implements MatPoster { Logger logger; - public static MatPosterImpl createWithoutRecycler(String name, int maxQueueItems) { - return new MatPosterImpl(name, maxQueueItems, null); + public static ThreadedMatPoster createWithoutRecycler(String name, int maxQueueItems) { + return new ThreadedMatPoster(name, maxQueueItems, null); } - public MatPosterImpl(String name, int maxQueueItems) { + public ThreadedMatPoster(String name, int maxQueueItems) { this(name, new MatRecycler(maxQueueItems + 2)); } - public MatPosterImpl(String name, MatRecycler recycler) { + public ThreadedMatPoster(String name, MatRecycler recycler) { this(name, recycler.getSize(), recycler); } - public MatPosterImpl(String name, int maxQueueItems, MatRecycler recycler) { + public ThreadedMatPoster(String name, int maxQueueItems, MatRecycler recycler) { postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); matRecycler = recycler; posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt index ffe4e950..4414ddae 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt @@ -27,17 +27,11 @@ class PipelineListIconRenderer( list, value, index, isSelected, cellHasFocus ) as JLabel - val runtimePipelinesAmount = pipelineManager.getPipelinesFrom( - PipelineSource.COMPILED_ON_RUNTIME - ).size + val source = pipelineManager.pipelines[index].source - if(runtimePipelinesAmount > 0) { - val source = pipelineManager.pipelines[index].source - - label.icon = when(source) { - PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon - else -> hammerIcon - } + label.icon = when(source) { + PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon + else -> hammerIcon } return label diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index c9cc01df..eb759b80 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -69,7 +69,7 @@ public void init() { if(lastMatFromSource == null) lastMatFromSource = new Mat(); - Size size = new Size(320, 240); + Size size = new Size(640, 480); createDefaultImgInputSource("/images/ug_4.jpg", "ug_eocvsim_4.jpg", "Ultimate Goal 4 Ring", size); createDefaultImgInputSource("/images/ug_1.jpg", "ug_eocvsim_1.jpg", "Ultimate Goal 1 Ring", size); createDefaultImgInputSource("/images/ug_0.jpg", "ug_eocvsim_0.jpg", "Ultimate Goal 0 Ring", size); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt index 7d15e27d..77990999 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt @@ -23,7 +23,7 @@ package com.github.serivesmejia.eocvsim.output -import com.github.serivesmejia.eocvsim.gui.util.MatPosterImpl +import com.github.serivesmejia.eocvsim.gui.util.ThreadedMatPoster import com.github.serivesmejia.eocvsim.util.StrUtil import io.github.deltacv.vision.external.util.extension.aspectRatio import io.github.deltacv.vision.external.util.extension.clipTo @@ -47,7 +47,7 @@ class VideoRecordingSession( @Volatile private var videoMat: Mat? = null - val matPoster = MatPosterImpl("VideoRec", videoFps.toInt()) + val matPoster = ThreadedMatPoster("VideoRec", videoFps.toInt()) private val fpsCounter = FpsCounter() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java index 3ed276ca..47f0b24d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java @@ -23,9 +23,7 @@ package com.github.serivesmejia.eocvsim.pipeline; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; +import android.graphics.*; import android.graphics.Rect; import org.firstinspires.ftc.robotcore.external.Telemetry; import org.opencv.core.*; @@ -46,6 +44,7 @@ public DefaultPipeline(Telemetry telemetry) { textPaint = new Paint(); textPaint.setColor(Color.WHITE); + textPaint.setTypeface(Typeface.DEFAULT_ITALIC); textPaint.setTextSize(30); textPaint.setAntiAlias(true); @@ -77,7 +76,6 @@ public Mat processFrame(Mat input) { @Override public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { canvas.drawRect(new Rect(0, 0, 385, 45), boxPaint); - canvas.drawText("Default pipeline selected", 5, 33, textPaint); } 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 eda0b060..41aaf1ea 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 @@ -86,6 +86,8 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi private set @Volatile var currentPipelineData: PipelineData? = null private set + var currentTunerTarget: Any? = null + private set var currentPipelineName = "" private set var currentPipelineIndex = -1 @@ -595,6 +597,7 @@ class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: Pi currentTelemetry = nextTelemetry currentPipelineIndex = index currentPipelineName = currentPipeline!!.javaClass.simpleName + currentTunerTarget = instantiator.variableTunerTargetObject(currentPipeline!!) currentTelemetry?.update() // clear telemetry diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt index 3fabde34..fa6d5824 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.pipeline.handler import com.github.serivesmejia.eocvsim.input.InputSource diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt index 66755f9f..568c32b2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.pipeline.handler import org.firstinspires.ftc.robotcore.external.Telemetry 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 bd6049fb..f4b74ec5 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 @@ -1,6 +1,28 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.pipeline.instantiator -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import org.firstinspires.ftc.robotcore.external.Telemetry import org.openftc.easyopencv.OpenCvPipeline @@ -16,4 +38,6 @@ object DefaultPipelineInstantiator : PipelineInstantiator { constructor.newInstance() as OpenCvPipeline } + override fun variableTunerTargetObject(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 7b6ef3c9..c3e30c49 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 @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.pipeline.instantiator import com.github.serivesmejia.eocvsim.pipeline.PipelineManager @@ -8,4 +31,6 @@ interface PipelineInstantiator { fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline + fun variableTunerTargetObject(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 4161fbc7..d42134c8 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 @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.pipeline.instantiator.processor import com.github.serivesmejia.eocvsim.pipeline.PipelineManager @@ -8,7 +31,6 @@ import org.firstinspires.ftc.vision.VisionProcessor import org.openftc.easyopencv.OpenCvPipeline object ProcessorInstantiator : PipelineInstantiator { - override fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline { if(!ReflectUtil.hasSuperclass(clazz, VisionProcessor::class.java)) throw IllegalArgumentException("Class $clazz does not extend VisionProcessor") @@ -26,4 +48,7 @@ object ProcessorInstantiator : PipelineInstantiator { return ProcessorPipeline(processor) } + override fun variableTunerTargetObject(pipeline: OpenCvPipeline): VisionProcessor = + (pipeline as ProcessorPipeline).processor + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java index acfdd656..d56cf1a6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 com.github.serivesmejia.eocvsim.pipeline.instantiator.processor; import android.graphics.Canvas; @@ -9,7 +32,7 @@ @Disabled class ProcessorPipeline extends TimestampedOpenCvPipeline { - private VisionProcessor processor; + VisionProcessor processor; public ProcessorPipeline(VisionProcessor processor) { this.processor = processor; 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 dfecbe76..6cd1dd8a 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 @@ -30,15 +30,13 @@ import org.openftc.easyopencv.OpenCvPipeline; import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; public abstract class TunableField { protected Field reflectionField; protected TunableFieldPanel fieldPanel; - protected OpenCvPipeline pipeline; + protected Object target; protected AllowMode allowMode; protected EOCVSim eocvSim; @@ -51,17 +49,17 @@ public abstract class TunableField { private TunableFieldPanel.Mode recommendedMode = null; - public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + public TunableField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { this.reflectionField = reflectionField; - this.pipeline = instance; + this.target = target; this.allowMode = allowMode; this.eocvSim = eocvSim; - initialFieldValue = reflectionField.get(instance); + initialFieldValue = reflectionField.get(target); } - public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - this(instance, reflectionField, eocvSim, AllowMode.TEXT); + public TunableField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + this(target, reflectionField, eocvSim, AllowMode.TEXT); } public abstract void init(); @@ -72,7 +70,7 @@ public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocv public void setPipelineFieldValue(T newValue) throws IllegalAccessException { if (hasChanged()) { //execute if value is not the same to save resources - reflectionField.set(pipeline, newValue); + reflectionField.set(target, newValue); onValueChange.run(); } } 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 3c3fbfa7..089fbb2b 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 @@ -84,7 +84,7 @@ public void init() { } if (eocvSim.pipelineManager.getCurrentPipeline() != null) { - addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline()); + addFieldsFrom(eocvSim.pipelineManager.getCurrentTunerTarget()); eocvSim.visualizer.updateTunerFields(createTunableFieldPanels()); for(TunableField field : fields.toArray(new TunableField[0])) { @@ -145,11 +145,10 @@ public Class getTunableFieldOf(Field field) { return tunableFieldClass; } - public void addFieldsFrom(OpenCvPipeline pipeline) { + public void addFieldsFrom(Object target) { + if (target == null) return; - if (pipeline == null) return; - - Field[] fields = pipeline.getClass().getFields(); + Field[] fields = target.getClass().getFields(); for (Field field : fields) { Class tunableFieldClass = getTunableFieldOf(field); @@ -161,8 +160,8 @@ public void addFieldsFrom(OpenCvPipeline pipeline) { //now, lets do some more reflection to instantiate this TunableField //and add it to the list... try { - Constructor constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, Field.class, EOCVSim.class); - this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); + Constructor constructor = tunableFieldClass.getConstructor(Object.class, Field.class, EOCVSim.class); + this.fields.add(constructor.newInstance(target, field, eocvSim)); } catch(InvocationTargetException e) { if(e.getCause() instanceof CancelTunableFieldAddingException) { String message = e.getCause().getMessage(); 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 289e49a3..8e6b3380 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 @@ -38,15 +38,13 @@ public class BooleanField extends TunableField { boolean lastVal; volatile boolean hasChanged = false; - public BooleanField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - - super(instance, reflectionField, eocvSim, AllowMode.TEXT); + public BooleanField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.TEXT); setGuiFieldAmount(0); setGuiComboBoxAmount(1); value = (boolean) initialFieldValue; - } @Override 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 42f60e99..eb592693 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,14 +4,12 @@ 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 com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableFieldAcceptor -import org.openftc.easyopencv.OpenCvPipeline import java.lang.reflect.Field @RegisterTunableField -class EnumField(private val instance: OpenCvPipeline, +class EnumField(target: Any, reflectionField: Field, - eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) { + eocvSim: EOCVSim) : TunableField>(target, reflectionField, eocvSim, AllowMode.TEXT) { val values = reflectionField.type.enumConstants @@ -45,7 +43,7 @@ class EnumField(private val instance: OpenCvPipeline, override fun setGuiFieldValue(index: Int, newValue: String) { currentValue = java.lang.Enum.valueOf(initialValue::class.java, newValue) - reflectionField.set(instance, currentValue) + reflectionField.set(target, currentValue) } override fun getValue() = currentValue @@ -56,7 +54,7 @@ class EnumField(private val instance: OpenCvPipeline, return values } - override fun hasChanged() = reflectionField.get(instance) != beforeValue + override fun hasChanged() = reflectionField.get(target) != 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 cdca560a..2b5af34e 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 @@ -36,8 +36,8 @@ public class NumericField extends TunableField { protected volatile boolean hasChanged = false; - public NumericField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, allowMode); + public NumericField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + super(target, reflectionField, eocvSim, allowMode); } @Override @@ -50,7 +50,7 @@ public void update() { if (value == null) return; try { - value = (T) reflectionField.get(pipeline); + value = (T) reflectionField.get(target); } catch (IllegalAccessException e) { e.printStackTrace(); } 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 886ee3a5..d80895b1 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 @@ -40,8 +40,8 @@ public class StringField extends TunableField { volatile boolean hasChanged = false; - public StringField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.TEXT); + public StringField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.TEXT); if(initialFieldValue != null) { value = (String) initialFieldValue; 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 6972c82e..7b7a6734 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 @@ -40,9 +40,8 @@ public class PointField extends TunableField { volatile boolean hasChanged = false; - public PointField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public PointField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue != null) { Point p = (Point) initialFieldValue; 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 f8dc89d4..b83cf1de 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 @@ -31,8 +31,8 @@ import org.openftc.easyopencv.OpenCvPipeline import java.lang.reflect.Field @RegisterTunableField -class RectField(instance: OpenCvPipeline, reflectionField: Field, eocvSim: EOCVSim) : - TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { +class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) : + TunableField(target, 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) @@ -56,7 +56,7 @@ class RectField(instance: OpenCvPipeline, reflectionField: Field, eocvSim: EOCVS override fun update() { if(hasChanged()){ - initialRect = reflectionField.get(pipeline) as Rect + initialRect = reflectionField.get(target) as Rect rect[0] = initialRect.x.toDouble() rect[1] = initialRect.y.toDouble() 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 6df85a78..fa79db64 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 @@ -43,8 +43,8 @@ public class ScalarField extends TunableField { volatile boolean hasChanged = false; - public ScalarField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public ScalarField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); if(initialFieldValue == null) { scalar = new Scalar(0, 0, 0); @@ -63,7 +63,7 @@ public void init() { } @Override public void update() { try { - scalar = (Scalar) reflectionField.get(pipeline); + scalar = (Scalar) reflectionField.get(target); } catch (IllegalAccessException e) { e.printStackTrace(); } 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 32dd0ef2..572bedbd 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 @@ -35,8 +35,8 @@ public class DoubleField extends NumericField { private double beforeValue; - public DoubleField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public DoubleField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (double) initialFieldValue; } 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 ec788b74..321e01dd 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 @@ -35,8 +35,8 @@ public class FloatField extends NumericField { protected float beforeValue; - public FloatField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + public FloatField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); value = (float) initialFieldValue; } 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 99b8aa01..08ff9bf2 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 @@ -35,8 +35,8 @@ public class IntegerField extends NumericField { protected int beforeValue; - public IntegerField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + public IntegerField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (int) initialFieldValue; } 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 45fdff7f..6645a505 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 @@ -35,8 +35,8 @@ public class LongField extends NumericField { private long beforeValue; - public LongField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + public LongField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); value = (long) initialFieldValue; } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java index defb5efb..0be87b5d 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java @@ -21,11 +21,11 @@ public enum RingNumber { } private final static int TEST_RECT_X = 240; - private final static int TEST_RECT_Y = 146; - private final static int TEST_RECT_WIDTH = 20; - private final static int TEST_RECT_HEIGHT = 60; - private final static double FOUR_STACK_HUE_THRESHOLD = 70; - private final static double ONE_STACK_HUE_THRESHOLD = 58; + public static int TEST_RECT_Y = 146; + public static int TEST_RECT_WIDTH = 20; + public static int TEST_RECT_HEIGHT = 60; + public static double FOUR_STACK_HUE_THRESHOLD = 70; + public static double ONE_STACK_HUE_THRESHOLD = 58; private RingNumber result; Paint rectPaint; Paint textPaint; diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java index 81798a94..fe351669 100644 --- a/Vision/src/main/java/android/graphics/Paint.java +++ b/Vision/src/main/java/android/graphics/Paint.java @@ -196,7 +196,23 @@ public Paint setAntiAlias(boolean value) { } public Paint setStyle(Style style) { - // TODO: uh oh... + // Map Style to Skiko Mode enum + org.jetbrains.skia.PaintMode mode = null; + + switch(style) { + case FILL: + mode = org.jetbrains.skia.PaintMode.FILL; + break; + case STROKE: + mode = org.jetbrains.skia.PaintMode.STROKE; + break; + case FILL_AND_STROKE: + mode = org.jetbrains.skia.PaintMode.STROKE_AND_FILL; + break; + } + + thePaint.setMode(mode); + return this; } diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java index 01559735..18d101c4 100644 --- a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java +++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java @@ -1,3 +1,34 @@ +/* Copyright (c) 2014 Qualcomm Technologies Inc + +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 Qualcomm Technologies Inc 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 com.qualcomm.robotcore.eventloop.opmode; import io.github.deltacv.vision.external.source.ThreadSourceHander; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt b/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt index c3a9d035..ff0e6b60 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external import android.graphics.Canvas @@ -8,7 +31,7 @@ object PipelineRenderHook : RenderHook { override fun onDrawFrame(canvas: Canvas, onscreenWidth: Int, onscreenHeight: Int, scaleBmpPxToCanvasPx: Float, canvasDensityScale: Float, userContext: Any) { val frameContext = userContext as OpenCvViewport.FrameContext - // We must make sure that we call onDrawFrame() for the same pipeline which set the + // We must make sure that we call onDrawFrame() for the same pipeline that set the // context object when requesting a draw hook. (i.e. we can't just call onDrawFrame() // for whatever pipeline happens to be currently attached; it might have an entirely // different notion of what to expect in the context object) diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java index 995b0947..cf01296c 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 OpenFTC & EOCV-Sim implementation by 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.vision.external; import io.github.deltacv.vision.external.source.VisionSource; @@ -140,7 +163,7 @@ public void onFrameStart() { @Override public void onNewFrame(Mat frame, long timestamp) { if(!isStreaming()) return; - if(frame.empty() || frame == null) return; + if(frame == null || frame.empty()) return; handleFrameUserCrashable(frame, timestamp); } diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt index 34a341b4..1915f5b8 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 OpenFTC Team & Sebastian Erives + * Copyright (c) 2023 OpenFTC Team & EOCV-Sim implementation by 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 @@ -319,12 +319,12 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi } /* - * For some reason, the canvas will very occasionally be null upon closing. - * Stack Overflow seems to suggest this means the canvas has been destroyed. - * However, surfaceDestroyed(), which is called right before the surface is - * destroyed, calls checkState(), which *SHOULD* block until we die. This - * works most of the time, but not always? We don't yet understand... - */ + * For some reason, the canvas will very occasionally be null upon closing. + * Stack Overflow seems to suggest this means the canvas has been destroyed. + * However, surfaceDestroyed(), which is called right before the surface is + * destroyed, calls checkState(), which *SHOULD* block until we die. This + * works most of the time, but not always? We don't yet understand... + */ if (canvas != null) { renderer.render(mat, canvas, renderHook, mat.context) } else { diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java index 9cd22e00..5ad77b76 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.source; import java.util.HashMap; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java index 3c85f630..b0bc6a81 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.source; import org.openftc.easyopencv.OpenCvViewport; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java index af17d9ce..213251b0 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.source; import org.opencv.core.Size; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java index d1d306be..269430ce 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.source; import io.github.deltacv.vision.external.util.Timestamped; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java index 35d5166f..b4fd133a 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.source; public interface VisionSourceHander { diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java index 8f3bb489..4274c289 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.source; import org.opencv.core.Mat; diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java index 270b8d59..ec68e295 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.util; import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; @@ -25,7 +48,6 @@ public Mat takeMatAndPost() { return mat; } - public Mat takeMat() { return matRecycler.takeMatOrNull(); } diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java index 76c5094f..719fc0ee 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java +++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.external.util; public class Timestamped { diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt index bdfaaf42..cade7d42 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt @@ -1,12 +1,35 @@ +/* + * Copyright (c) 2023 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.vision.internal.opmode import com.github.serivesmejia.eocvsim.util.event.EventHandler import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue import java.util.concurrent.ArrayBlockingQueue -class OpModeNotifier { +class OpModeNotifier(maxNotificationsQueueSize: Int = 100) { - val notifications = EvictingBlockingQueue(ArrayBlockingQueue(10)) + val notifications = EvictingBlockingQueue(ArrayBlockingQueue(maxNotificationsQueueSize)) private val stateLock = Any() var state = OpModeState.STOPPED diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java index 7233bcf1..3b1f965a 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java @@ -1,10 +1,32 @@ +/* + * Copyright (c) 2023 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.vision.internal.source.ftc; import io.github.deltacv.vision.external.source.VisionSource; import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCharacteristics; import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; - public abstract class SourcedCameraName implements WebcamName { public abstract VisionSource getSource(); diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java index 832628c4..6efc4e02 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java +++ b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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.vision.internal.source.ftc; import androidx.annotation.NonNull; diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java index 53a2c53e..13cac400 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java @@ -14,8 +14,6 @@ public abstract class OpenCvPipeline { public void onViewportTapped() { } public void init(Mat mat) { } - - public Object getUserContextForDrawHook() { return userContext; diff --git a/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java index c252c815..67592c78 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java +++ b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2023 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 org.openftc.easyopencv; import io.github.deltacv.vision.external.SourcedOpenCvCamera; From 44a33bddbbe3413f19c97f4046a1199d75195131 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 20 Aug 2023 15:45:44 -0700 Subject: [PATCH 41/46] Fix color picker --- .../serivesmejia/eocvsim/gui/Visualizer.java | 8 +++--- .../gui/component/tuner/ColorPicker.kt | 26 +++++++++---------- .../tuner/TunableFieldPanelConfig.kt | 6 ++++- .../tuner/TunableFieldPanelOptions.kt | 10 +++---- .../eocvsim/tuner/TunableField.java | 4 +++ .../external/gui/SwingOpenCvViewport.kt | 6 +++++ 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index bab7a8b2..06c12185 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -129,6 +129,8 @@ public void init(Theme theme) { viewport = new SwingOpenCvViewport(new Size(1080, 720), fpsMeterDescriptor); viewport.setDark(FlatLaf.isLafDark()); + colorPicker = new ColorPicker(viewport); + JLayeredPane skiaPanel = viewport.skiaPanel(); skiaPanel.setLayout(new BorderLayout()); @@ -206,8 +208,6 @@ public void init(Theme theme) { frame.setExtendedState(JFrame.MAXIMIZED_BOTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - // colorPicker = new ColorPicker(viewport.image); - frame.setVisible(true); onInitFinished.run(); @@ -237,7 +237,7 @@ public void windowClosing(WindowEvent e) { //handling onViewportTapped evts viewport.getComponent().addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { -// if(!colorPicker.isPicking()) + if(!colorPicker.isPicking()) eocvSim.pipelineManager.callViewportTapped(); } }); @@ -275,7 +275,7 @@ public void componentResized(ComponentEvent evt) { // stop color-picking mode when changing pipeline // TODO: find out why this breaks everything????? - // eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking()); + eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking()); } public boolean hasFinishedInit() { return hasFinishedInitializing; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt index f065ff2b..7826c415 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt @@ -25,18 +25,18 @@ package com.github.serivesmejia.eocvsim.gui.component.tuner import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.gui.Icons -import io.github.deltacv.vision.external.gui.component.ImageX import com.github.serivesmejia.eocvsim.util.event.EventHandler +import io.github.deltacv.vision.external.gui.SwingOpenCvViewport import org.opencv.core.Scalar -import java.awt.Color import java.awt.Cursor import java.awt.Point +import java.awt.Robot +import java.awt.Toolkit import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.awt.Toolkit -class ColorPicker(private val imageX: ImageX) { + +class ColorPicker(private val viewport: SwingOpenCvViewport) { companion object { private val size = if(SysUtil.OS == SysUtil.OperatingSystem.WINDOWS) { @@ -68,10 +68,8 @@ class ColorPicker(private val imageX: ImageX) { override fun mouseClicked(e: MouseEvent) { //if clicked with primary button... if(e.button == MouseEvent.BUTTON1) { - //get the "packed" (in a single int value) color from the image at mouse position's pixel - val packedColor = imageX.image.getRGB(e.x, e.y) - //parse the "packed" color into four separate channels - val color = Color(packedColor, true) + // The pixel color at location x, y + val color = Robot().getPixelColor(e.xOnScreen, e.yOnScreen) //wrap Java's color to OpenCV's Scalar since we're EOCV-Sim not JavaCv-Sim right? colorRgb = Scalar( @@ -93,10 +91,10 @@ class ColorPicker(private val imageX: ImageX) { isPicking = true hasPicked = false - imageX.addMouseListener(clickListener) + viewport.component.addMouseListener(clickListener) - initialCursor = imageX.cursor - imageX.cursor = colorPickCursor + initialCursor = viewport.component.cursor + viewport.component.cursor = colorPickCursor } fun stopPicking() { @@ -108,8 +106,8 @@ class ColorPicker(private val imageX: ImageX) { onCancel.run() } - imageX.removeMouseListener(clickListener) - imageX.cursor = initialCursor + viewport.component.removeMouseListener(clickListener) + viewport.component.cursor = initialCursor } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt index 3b4b37a2..0d4e4a9a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt @@ -89,7 +89,7 @@ class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions LOCAL("From local config"), GLOBAL("From global config"), GLOBAL_DEFAULT("From default global config"), - TYPE_SPECIFIC("From specific config") + TYPE_SPECIFIC("From type config") } data class Config(var sliderRange: Size, @@ -294,6 +294,10 @@ class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions } configSourceLabel.text = localConfig.source.description + + if(currentConfig.source == ConfigSource.LOCAL || currentConfig.source == ConfigSource.TYPE_SPECIFIC) { + configSourceLabel.text += ": ${fieldOptions.fieldPanel.tunableField.fieldTypeName}" + } } //updates the actual configuration displayed on the field panel gui 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 2e4557af..b7d66e08 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 @@ -119,12 +119,12 @@ class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, //start picking if global color picker is not being used by other panel if(!colorPicker.isPicking && colorPickButton.isSelected) { - // startPicking(colorPicker) // TODO: Fix color picker + startPicking(colorPicker) } else { //handles cases when cancelling picking - //colorPicker.stopPicking() // TODO: Fix color picker - //if we weren't the ones controlling the last picking, - //start picking again to gain control for this panel - // if(colorPickButton.isSelected) startPicking(colorPicker) + colorPicker.stopPicking() + // if we weren't the ones controlling the last picking, + // start picking again to gain control for this panel + if(colorPickButton.isSelected) startPicking(colorPicker) } } 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 6cd1dd8a..c1c707d2 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 @@ -125,6 +125,10 @@ public final String getFieldName() { return reflectionField.getName(); } + public final String getFieldTypeName() { + return reflectionField.getType().getSimpleName(); + } + public final AllowMode getAllowMode() { return allowMode; } diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt index 1915f5b8..322633b3 100644 --- a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt +++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt @@ -372,6 +372,12 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi this.renderHook = renderHook } + fun pollLastFrame(dst: Mat) { + synchronized(canvasLock) { + lastFrame.copyTo(dst) + } + } + companion object { private const val VISION_PREVIEW_FRAME_QUEUE_CAPACITY = 2 private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 4 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1). From 0ddb4ff171164379d87dd57cca27312c2cb97a48 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 20 Aug 2023 22:21:45 -0700 Subject: [PATCH 42/46] Implement Paint(Paint) constructor & several UI improvements --- .../serivesmejia/eocvsim/gui/Visualizer.java | 29 +---- .../gui/component/CollapsiblePanelX.kt | 3 +- .../tuner/TunableFieldPanelConfig.kt | 4 +- .../component/visualizer/CreateSourcePanel.kt | 10 +- .../visualizer/opmode/OpModeSelectorPanel.kt | 4 + .../pipeline/PipelineSelectorPanel.kt | 2 +- .../eocvsim/gui/dialog/About.java | 2 +- .../gui/util/icon/PipelineListIconRenderer.kt | 5 +- .../src/main/resources/opensourcelibs.txt | 5 +- .../teamcode/processors/StackProcessor.java | 108 ------------------ .../src/main/java/android/graphics/Paint.java | 7 ++ .../easyopencv/OpenCvViewRenderer.java | 2 +- 12 files changed, 34 insertions(+), 147 deletions(-) delete mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 06c12185..323bed34 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -163,6 +163,10 @@ public void init(Theme theme) { */ rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS)); + // add pretty border + rightContainer.setBorder( + BorderFactory.createMatteBorder(0, 1, 0, 0, UIManager.getColor("Separator.foreground")) + ); pipelineOpModeSwitchablePanel.setBorder(new EmptyBorder(0, 0, 0, 0)); rightContainer.add(pipelineOpModeSwitchablePanel); @@ -182,7 +186,7 @@ public void init(Theme theme) { //global frame.getContentPane().setDropTarget(new InputSourceDropTarget(eocvSim)); - tunerCollapsible = new CollapsiblePanelX("Tuner", null, null); + tunerCollapsible = new CollapsiblePanelX("Variable Tuner", null, null); tunerCollapsible.setLayout(new BoxLayout(tunerCollapsible, BoxLayout.LINE_AXIS)); tunerCollapsible.setVisible(false); @@ -250,29 +254,6 @@ public void mouseClicked(MouseEvent e) { // } // }); - //resizes all three JLists in right panel to make buttons visible in smaller resolutions - frame.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent evt) { - double ratioH = frame.getSize().getHeight() / 645; - - double fontSize = 17 * ratioH; - Font font = pipelineSelectorPanel.getPipelineSelectorLabel().getFont().deriveFont((float)fontSize); - - pipelineSelectorPanel.getPipelineSelectorLabel().setFont(font); - pipelineSelectorPanel.revalAndRepaint(); - - sourceSelectorPanel.getSourceSelectorLabel().setFont(font); - sourceSelectorPanel.revalAndRepaint(); - - telemetryPanel.getTelemetryLabel().setFont(font); - telemetryPanel.revalAndRepaint(); - - rightContainer.revalidate(); - rightContainer.repaint(); - } - }); - // stop color-picking mode when changing pipeline // TODO: find out why this breaks everything????? eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking()); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt index 0132cae5..0822043a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt @@ -28,6 +28,7 @@ import java.awt.Color import java.awt.Dimension import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import javax.swing.BorderFactory import javax.swing.JLabel import javax.swing.JPanel import javax.swing.border.LineBorder @@ -54,7 +55,7 @@ class CollapsiblePanelX @JvmOverloads constructor( border = TitledBorder(titleAndDescriptor) border.titleColor = titleCol - border.border = LineBorder(borderCol) + border.border = BorderFactory.createMatteBorder(1, 1, 1, 1, borderCol) setBorder(border) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt index 0d4e4a9a..4c1cc071 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt @@ -57,10 +57,10 @@ class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions private val sliderRangeFieldsPanel = JPanel() private var sliderRangeFields = createRangeFields() - private val colorSpaceComboBox = EnumComboBox("Color space: ", PickerColorSpace::class.java, PickerColorSpace.values()) + private val colorSpaceComboBox = EnumComboBox("Color space: ", PickerColorSpace::class.java, PickerColorSpace.entries.toTypedArray()) private val applyToAllButtonPanel = JPanel(GridBagLayout()) - private val applyToAllButton = JToggleButton("Apply to all fields...") + private val applyToAllButton = JToggleButton("Apply to all variables...") private val applyModesPanel = JPanel() private val applyToAllGloballyButton = JButton("Globally") diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt index 5a73961c..48cc57e4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt @@ -37,13 +37,13 @@ import javax.swing.JPanel class CreateSourcePanel(eocvSim: EOCVSim) : JPanel(GridLayout(2, 1)) { private val sourceSelectComboBox = EnumComboBox( - "", SourceType::class.java, SourceType.values(), - { it.coolName }, { SourceType.fromCoolName(it) } + "", SourceType::class.java, SourceType.values(), + { it.coolName }, { SourceType.fromCoolName(it) } ) private val cameraDriverComboBox = EnumComboBox( - "Camera driver: ", WebcamDriver::class.java, WebcamDriver.values(), - { it.name.replace("_", " ") }, { WebcamDriver.valueOf(it.replace(" ", "_")) } + "Camera driver: ", WebcamDriver::class.java, WebcamDriver.values(), + { it.name.replace("_", " ") }, { WebcamDriver.valueOf(it.replace(" ", "_")) } ) @@ -69,4 +69,4 @@ class CreateSourcePanel(eocvSim: EOCVSim) : JPanel(GridLayout(2, 1)) { add(nextPanel) } -} +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt index 4077a6f8..c6c041a5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnThis import com.github.serivesmejia.eocvsim.gui.util.Corner +import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer import com.github.serivesmejia.eocvsim.pipeline.PipelineData import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.loggerForThis @@ -89,6 +90,9 @@ class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeC autonomousSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION teleopSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION + autonomousSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) { autonomousIndexMap } + teleopSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) { teleopIndexMap } + autonomousButton.icon = EOCVSimIconLibrary.icoArrowDropdown add(autonomousButton, GridBagConstraints().apply { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index 55d64690..7fc835a2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -84,7 +84,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { // ipady = 20 //}) - pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) + pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) { indexMap } pipelineSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION pipelineSelectorScroll.setViewportView(pipelineSelector) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java index 46554282..958a6670 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java @@ -129,7 +129,7 @@ private void initAbout() { osLibsList.addListSelectionListener(e -> { if(!e.getValueIsAdjusting()) { - String text = osLibsList.getModel().getElementAt(osLibsList.getSelectedIndex()); + String text = osLibsList.getSelectedValue(); String[] urls = StrUtil.findUrlsInString(text); if(urls.length > 0) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt index 4414ddae..71081a42 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt @@ -10,7 +10,8 @@ import javax.swing.* import java.awt.* class PipelineListIconRenderer( - private val pipelineManager: PipelineManager + private val pipelineManager: PipelineManager, + private val indexMapProvider: () -> Map ) : DefaultListCellRenderer() { private val gearsIcon by EOCVSimIconLibrary.icoGears.lazyResized(15, 15) @@ -27,7 +28,7 @@ class PipelineListIconRenderer( list, value, index, isSelected, cellHasFocus ) as JLabel - val source = pipelineManager.pipelines[index].source + val source = pipelineManager.pipelines[indexMapProvider()[index]!!].source label.icon = when(source) { PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon diff --git a/EOCV-Sim/src/main/resources/opensourcelibs.txt b/EOCV-Sim/src/main/resources/opensourcelibs.txt index c02ffecc..ce70c5a7 100644 --- a/EOCV-Sim/src/main/resources/opensourcelibs.txt +++ b/EOCV-Sim/src/main/resources/opensourcelibs.txt @@ -1,11 +1,12 @@ EOCV-Sim and its source code is distributed under the MIT License OpenCV - Under Apache 2.0 License -OpenCV for Desktop Java - Under Apache 2.0 License -FTC SDK - Some source code under the BSD License +OpenPnP OpenCV - Under Apache 2.0 License +FTC SDK - Some source code under BSD License EasyOpenCV - Some source code under MIT License EOCV-AprilTag-Plugin - Source code under MIT License webcam-capture - Under MIT License +Skiko - Under Apache 2.0 License Gson - Under Apache 2.0 License ClassGraph - Under MIT License FlatLaf - Under Apache 2.0 License diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java deleted file mode 100644 index 0be87b5d..00000000 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/processors/StackProcessor.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.firstinspires.ftc.teamcode.processors; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; - -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.Rect; -import org.opencv.imgproc.Imgproc; - -public class StackProcessor implements VisionProcessor { - public enum RingNumber { - ZERO, - ONE, - FOUR; - - public double avgHueValue; - } - - private final static int TEST_RECT_X = 240; - public static int TEST_RECT_Y = 146; - public static int TEST_RECT_WIDTH = 20; - public static int TEST_RECT_HEIGHT = 60; - public static double FOUR_STACK_HUE_THRESHOLD = 70; - public static double ONE_STACK_HUE_THRESHOLD = 58; - private RingNumber result; - Paint rectPaint; - Paint textPaint; - android.graphics.Rect drawRectangle; - float textLineSize; - - public RingNumber getResult() { - return result; - } - private boolean initialFrameDone; - private Mat ring; - private Mat ringHSV; - private boolean initialDrawDone; - - @Override - public void init(int width, int height, CameraCalibration calibration) { - - } - - public void processFirstFrame(Mat frame) { - Rect rect = new Rect(TEST_RECT_X, TEST_RECT_Y, TEST_RECT_WIDTH, TEST_RECT_HEIGHT); - - ring = frame.submat(rect); - ringHSV = new Mat(ring.cols(), ring.rows(), ring.type()); - } - - @Override - public Object processFrame(Mat frame, long captureTimeNanos) { - if (!initialFrameDone) { - processFirstFrame(frame); - initialFrameDone = true; - } - Imgproc.cvtColor(ring, ringHSV, Imgproc.COLOR_BGR2HSV); - - double avgHueValue = Core.mean(ringHSV).val[0]; - - if (avgHueValue > FOUR_STACK_HUE_THRESHOLD) { - result = RingNumber.FOUR; - } else if (avgHueValue > ONE_STACK_HUE_THRESHOLD) { - result = RingNumber.ONE; - } else { - result = RingNumber.ZERO; - } - result.avgHueValue = avgHueValue; - - return result; - } - - public void drawFirstFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity) { - rectPaint = new Paint(); - rectPaint.setColor(Color.RED); - rectPaint.setStyle(Paint.Style.STROKE); - - textPaint = new Paint(); - textPaint.setTextSize(40 * scaleCanvasDensity); - textPaint.setColor(Color.GREEN); - - int left = Math.round(TEST_RECT_X * scaleBmpPxToCanvasPx); - int top = Math.round(TEST_RECT_Y * scaleBmpPxToCanvasPx); - int right = left + Math.round(TEST_RECT_WIDTH * scaleBmpPxToCanvasPx); - int bottom = top + Math.round(TEST_RECT_HEIGHT * scaleBmpPxToCanvasPx); - - Paint.FontMetrics fm = textPaint.getFontMetrics(); - textLineSize = (fm.descent - fm.ascent) * scaleBmpPxToCanvasPx; - - drawRectangle = new android.graphics.Rect(left, top, right, bottom); - } - - @Override - public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) { - if (!initialDrawDone) { - drawFirstFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity); - initialDrawDone = true; - } - canvas.drawRect(drawRectangle, rectPaint); - canvas.drawText(userContext.toString(), 0, textLineSize, textPaint); - canvas.drawText(String.format("%.2f", ((RingNumber) userContext).avgHueValue), - 0, textLineSize * 2, textPaint); - } -} \ No newline at end of file diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java index fe351669..afaa064a 100644 --- a/Vision/src/main/java/android/graphics/Paint.java +++ b/Vision/src/main/java/android/graphics/Paint.java @@ -185,6 +185,13 @@ public Paint() { thePaint = new org.jetbrains.skia.Paint(); } + public Paint(Paint paint) { + thePaint = paint.thePaint; + + typeface = paint.typeface; + textSize = paint.textSize; + } + public Paint setColor(int color) { thePaint.setColor(color); return this; diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java index 457d6cfc..39596a3c 100644 --- a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java +++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java @@ -74,7 +74,7 @@ public OpenCvViewRenderer(boolean renderingOffsceen, String fpsMeterDescriptor) metricsScale = 1.0f; - fpsMeterTextSize = 26 * metricsScale; + fpsMeterTextSize = 26.2f * metricsScale; statBoxW = (int) (450 * metricsScale); statBoxH = (int) (120 * metricsScale); statBoxTextLineSpacing = (int) (35 * metricsScale); From 45fa809517c5d30d3f926e76fc9e9ceeb95071ca Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 20 Aug 2023 23:27:40 -0700 Subject: [PATCH 43/46] Add more implementations in android.graphics --- .../main/java/android/graphics/Canvas.java | 96 +++++--- .../src/main/java/android/graphics/Paint.java | 39 +++- .../src/main/java/android/graphics/Path.java | 211 ++++++++++++++++++ 3 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 Vision/src/main/java/android/graphics/Path.java diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java index 563a0429..2e3d8545 100644 --- a/Vision/src/main/java/android/graphics/Canvas.java +++ b/Vision/src/main/java/android/graphics/Canvas.java @@ -61,50 +61,80 @@ public Canvas drawLine(float x, float y, float x1, float y1, Paint paint) { } - public Canvas drawRoundRect(float l, float t, float r, float b, float xRad, float yRad, Paint rectPaint) { + 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); + } - return this; + public void drawPath(Path path, Paint paint) { + theCanvas.drawPath(path.thePath, paint.thePaint); + } + + public void drawCircle(float x, float y, float radius, Paint paint) { + theCanvas.drawCircle(x, y, radius, paint.thePaint); } + public void drawOval(float left, float top, float right, float bottom, Paint paint) { + theCanvas.drawOval(new org.jetbrains.skia.Rect(left, top, right, bottom), paint.thePaint); + } + + public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) { + theCanvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint.thePaint); + } - public Canvas drawText(String text, float x, float y, Paint paint) { + public void drawText(String text, int start, int end, float x, float y, Paint paint) { + Font font = FontCache.makeFont(paint.getTypeface(), paint.getTextSize()); + theCanvas.drawString(text.substring(start, end), x, y, font, paint.thePaint); + } + + public void drawText(String text, float x, float y, Paint paint) { Font font = FontCache.makeFont(paint.getTypeface(), paint.getTextSize()); theCanvas.drawString(text, x, y, font, paint.thePaint); + } - return this; + public void drawPoints(float[] points, int offset, int count, Paint paint) { + // not supported by the skija canvas so we have to do it manually + for(int i = offset; i < offset + count; i += 2) { + theCanvas.drawPoint(points[i], points[i + 1], paint.thePaint); + } + } + + public void drawPoints(float[] points, Paint paint) { + theCanvas.drawPoints(points, paint.thePaint); + } + + public void drawRGB(int r, int g, int b) { + theCanvas.clear(Color.rgb(r, g, b)); + } + + public void drawLines(float[] points, Paint paint) { + theCanvas.drawLines(points, paint.thePaint); + } + + public void drawRect(Rect rect, Paint paint) { + theCanvas.drawRect(rect.toSkijaRect(), paint.thePaint); + } + + public void drawRect(float left, float top, float right, float bottom, Paint paint) { + theCanvas.drawRect(new org.jetbrains.skia.Rect(left, top, right, bottom), paint.thePaint); } - public Canvas rotate(float degrees, float xCenter, float yCenter) { + public void rotate(float degrees, float xCenter, float yCenter) { theCanvas.rotate(degrees, xCenter, yCenter); - return this; } - public Canvas rotate(float degrees) { + public void rotate(float degrees) { theCanvas.rotate(degrees); - return this; } public int save() { return theCanvas.save(); } - public Canvas restore() { + public void restore() { theCanvas.restore(); - return this; - } - - public Canvas drawLines(float[] points, Paint paint) { - theCanvas.drawLines(points, paint.thePaint); - return this; } - public Canvas drawRect(Rect rect, Paint paint) { - theCanvas.drawRect(rect.toSkijaRect(), paint.thePaint); - return this; - } - - public Canvas drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { + public void drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { int left, top, right, bottom; if (src == null) { left = top = 0; @@ -128,18 +158,32 @@ public Canvas drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) { new org.jetbrains.skia.Rect(left, top, right, bottom), rect.toSkijaRect(), thePaint ); + } - return this; + public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { + org.jetbrains.skia.Paint thePaint = null; + + if(paint != null) { + thePaint = paint.thePaint; + } + + theCanvas.drawImage(Image.Companion.makeFromBitmap(bitmap.theBitmap), left, top, thePaint); } - public Canvas translate(int dx, int dy) { + public void skew(float sx, float sy) { + theCanvas.skew(sx, sy); + } + + public void translate(int dx, int dy) { theCanvas.translate(dx, dy); - return this; } - public Canvas restoreToCount(int saveCount) { + public void scale(float sx, float sy) { + theCanvas.scale(sx, sy); + } + + public void restoreToCount(int saveCount) { theCanvas.restoreToCount(saveCount); - return this; } public boolean readPixels(@NotNull Bitmap lastFrame, int srcX, int srcY) { diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java index afaa064a..1aff5285 100644 --- a/Vision/src/main/java/android/graphics/Paint.java +++ b/Vision/src/main/java/android/graphics/Paint.java @@ -176,7 +176,7 @@ public static class FontMetrics { public float leading; } - public final org.jetbrains.skia.Paint thePaint; + public org.jetbrains.skia.Paint thePaint; private Typeface typeface; private float textSize; @@ -197,9 +197,8 @@ public Paint setColor(int color) { return this; } - public Paint setAntiAlias(boolean value) { + public void setAntiAlias(boolean value) { thePaint.setAntiAlias(value); - return this; } public Paint setStyle(Style style) { @@ -233,6 +232,14 @@ public Paint setTextSize(float v) { return this; } + public void setARGB(int a, int r, int g, int b) { + thePaint.setColor(Color.argb(a, r, g, b)); + } + + public void setAlpha(int alpha) { + thePaint.setAlpha(alpha); + } + public void setStrokeJoin(Join join) { PaintStrokeJoin strokeJoin = null; @@ -251,6 +258,7 @@ public void setStrokeJoin(Join join) { thePaint.setStrokeJoin(strokeJoin); } + public void setStrokeCap(Cap cap) { PaintStrokeCap strokeCap = null; @@ -278,6 +286,22 @@ public void setStrokeMiter(float miter) { thePaint.setStrokeMiter(miter); } + public void set(Paint src) { + thePaint = src.thePaint.makeClone(); + typeface = src.typeface; + textSize = src.textSize; + } + + public void reset() { + thePaint = new org.jetbrains.skia.Paint(); + typeface = null; + textSize = 0; + } + + public boolean hasGlyph(String text) { + return typeface.theTypeface.getStringGlyphs(text).length != 0; + } + // write getters here public int getColor() { return thePaint.getColor(); @@ -288,7 +312,14 @@ public boolean isAntiAlias() { } public Style getStyle() { - return Style.FILL; // TODO: uh oh... + switch(thePaint.getMode()) { + case FILL: + return Style.FILL; + case STROKE: + return Style.STROKE; + default: + return Style.FILL_AND_STROKE; + } } public float getStrokeWidth() { diff --git a/Vision/src/main/java/android/graphics/Path.java b/Vision/src/main/java/android/graphics/Path.java new file mode 100644 index 00000000..31d5e155 --- /dev/null +++ b/Vision/src/main/java/android/graphics/Path.java @@ -0,0 +1,211 @@ +package android.graphics; + +import org.jetbrains.skia.PathDirection; +import org.jetbrains.skia.RRect; +import org.jetbrains.skia.Rect; + +public class Path { + + /* + * Copyright (C) 2006 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. + */ + + /** + * Specifies how closed shapes (e.g. rects, ovals) are oriented when they + * are added to a path. + */ + public enum Direction { + /** clockwise */ + CW (0), // must match enum in SkPath.h + /** counter-clockwise */ + CCW (1); // must match enum in SkPath.h + Direction(int ni) { + nativeInt = ni; + } + final int nativeInt; + } + + public org.jetbrains.skia.Path thePath; + + public Path() { + thePath = new org.jetbrains.skia.Path(); + } + + public Path(Path src) { + this(); + thePath = src.thePath; + } + + public void set(Path src) { + thePath = src.thePath; + } + + public void addCircle(float x, float y, float radius, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addCircle(x, y, radius, skDir); + } + + public void addRect(float left, float top, float right, float bottom, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addRect(new Rect(left, top, right, bottom), skDir, 0); + } + + public void addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addRRect(RRect.makeLTRB(left, top, right, bottom, rx, ry), skDir, 0); + } + + public void addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addRRect(new RRect(left, top, right, bottom, radii), skDir, 0); + } + + public void addRoundRect(Rect rect, float[] radii, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addRRect(new RRect(rect.getLeft(), rect.getTop(), rect.getRight(), rect.getBottom(), radii), skDir, 0); + } + + public void addRoundRect(Rect rect, float rx, float ry, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addRRect(RRect.makeLTRB(rect.getLeft(), rect.getTop(), rect.getRight(), rect.getBottom(), rx, ry), skDir, 0); + } + + public void addOval(float left, float top, float right, float bottom, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addOval(new Rect(left, top, right, bottom), skDir, 0); + } + + public void addOval(Rect oval, Direction dir) { + // map to skia direction + PathDirection skDir = null; + switch (dir) { + case CW: + skDir = PathDirection.CLOCKWISE; + break; + case CCW: + skDir = PathDirection.COUNTER_CLOCKWISE; + break; + } + + thePath.addOval(oval, skDir, 0); + } + + public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) { + thePath.addArc(new Rect(left, top, right, bottom), startAngle, sweepAngle); + } + + public void addArc(Rect oval, float startAngle, float sweepAngle) { + thePath.addArc(oval, startAngle, sweepAngle); + } + + public void moveTo(float x, float y) { + thePath.moveTo(x, y); + } + + public void lineTo(float x, float y) { + thePath.lineTo(x, y); + } + + public void quadTo(float x1, float y1, float x2, float y2) { + thePath.quadTo(x1, y1, x2, y2); + } + + public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { + thePath.cubicTo(x1, y1, x2, y2, x3, y3); + } + + public void close() { + thePath.close(); + } + + public void reset() { + thePath.reset(); + } + + public void addPath(Path path) { + thePath.addPath(path.thePath, false); + } + +} From 689a633ed2b0408a8928cbde28a7d3d18316c78e Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 28 Aug 2023 12:35:19 -0700 Subject: [PATCH 44/46] Fix OpMode detection on workspaces --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 9 ++--- .../gui/component/visualizer/TopMenuBar.kt | 16 ++++++-- .../eocvsim/gui/util/GuiUtil.java | 2 - .../eocvsim/output/VideoRecordingSession.kt | 5 ++- .../compiler/CompiledPipelineManager.kt | 15 +++++++- .../pipeline/compiler/PipelineClassLoader.kt | 6 ++- .../eocvsim/util/ClasspathScan.kt | 23 +++++++++++- .../serivesmejia/eocvsim/util/SysUtil.java | 2 - .../workspace/config/WorkspaceConfig.java | 3 +- .../workspace/config/WorkspaceConfigLoader.kt | 6 +++ .../resources/templates/default_workspace.zip | Bin 16629 -> 24554 bytes .../resources/templates/gradle_workspace.zip | Bin 77738 -> 85915 bytes .../external/samples/ConceptAprilTag.java | 2 +- .../external/samples/ConceptAprilTagEasy.java | 2 +- .../main/java/android/graphics/Bitmap.java | 12 +++++- .../src/main/java/android/graphics/Path.java | 23 ++++++++++++ .../external/gui/SwingOpenCvViewport.kt | 35 +++++++++++++++++- .../main/java/org/opencv/android/Utils.java | 32 +++++++++++++--- 18 files changed, 162 insertions(+), 31 deletions(-) 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 4f9d1555..5850e065 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 @@ -393,19 +393,18 @@ class EOCVSim(val params: Parameters = Parameters()) { ).addCloseListener { _: Int, file: File?, selectedFileFilter: FileFilter? -> onMainUpdate.doOnce { if (file != null) { - - var correctedFile = File(file.absolutePath) + var correctedFile = file val extension = SysUtil.getExtensionByStringHandling(file.name) if (selectedFileFilter is FileNameExtensionFilter) { //if user selected an extension //get selected extension - correctedFile = file + "." + selectedFileFilter.extensions[0] + correctedFile = File(file.absolutePath + "." + selectedFileFilter.extensions[0]) } else if (extension.isPresent) { if (!extension.get().equals("avi", true)) { - correctedFile = file + ".avi" + correctedFile = File(file.absolutePath + ".avi") } } else { - correctedFile = file + ".avi" + correctedFile = File(file.absolutePath + ".avi") } if (correctedFile.exists()) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index 8159bd8b..46397134 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -33,6 +33,8 @@ import com.github.serivesmejia.eocvsim.input.SourceType import com.github.serivesmejia.eocvsim.util.FileFilters import com.github.serivesmejia.eocvsim.util.exception.handling.CrashReport import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher +import org.opencv.core.Mat +import org.opencv.imgproc.Imgproc import java.awt.Desktop import java.io.File import java.net.URI @@ -75,16 +77,22 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { fileNewInputSourceSubmenu.add(fileNewInputSourceItem) } - val fileSaveMat = JMenuItem("Save current image") + val fileSaveMat = JMenuItem("Screenshot pipeline") + - /* fileSaveMat.addActionListener { + val mat = Mat() + visualizer.viewport.pollLastFrame(mat) + Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGB2BGR) + GuiUtil.saveMatFileChooser( visualizer.frame, - visualizer.viewport., + mat, eocvSim ) - }*/ // TODO: Fix this + + mat.release() + } mFileMenu.add(fileSaveMat) mFileMenu.addSeparator() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java index 9f5c7653..5b442208 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java @@ -185,14 +185,12 @@ public static void saveBufferedImageFileChooser(Component parent, BufferedImage } public static void saveMatFileChooser(Component parent, Mat mat, EOCVSim eocvSim) { - Mat clonedMat = mat.clone(); BufferedImage img = CvUtil.matToBufferedImage(clonedMat); clonedMat.release(); saveBufferedImageFileChooser(parent, img, eocvSim); - } public static ListModel isToListModel(InputStream is, Charset charset) throws UnsupportedEncodingException { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt index 77990999..d972600f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt @@ -28,6 +28,7 @@ import com.github.serivesmejia.eocvsim.util.StrUtil import io.github.deltacv.vision.external.util.extension.aspectRatio import io.github.deltacv.vision.external.util.extension.clipTo import com.github.serivesmejia.eocvsim.util.fps.FpsCounter +import io.github.deltacv.common.image.MatPoster import org.opencv.core.* import org.opencv.imgproc.Imgproc import org.opencv.videoio.VideoWriter @@ -62,12 +63,12 @@ class VideoRecordingSession( matPoster.addPostable { postMat(it) } } - fun startRecordingSession() { + @Synchronized fun startRecordingSession() { videoWriter.open(tempFile.toString(), VideoWriter.fourcc('M', 'J', 'P', 'G'), videoFps, videoSize) hasStarted = true; } - fun stopRecordingSession() { + @Synchronized fun stopRecordingSession() { videoWriter.release(); videoMat?.release(); matPoster.stop() hasStopped = true } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index 3945d87b..3295911b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -23,6 +23,7 @@ package com.github.serivesmejia.eocvsim.pipeline.compiler +import com.github.serivesmejia.eocvsim.Build import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.dialog.Output import com.github.serivesmejia.eocvsim.pipeline.PipelineManager @@ -31,6 +32,7 @@ import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.loggerForThis +import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfigLoader import com.github.serivesmejia.eocvsim.workspace.util.template.DefaultWorkspaceTemplate import com.qualcomm.robotcore.util.ElapsedTime import kotlinx.coroutines.* @@ -40,10 +42,21 @@ import java.io.File class CompiledPipelineManager(private val pipelineManager: PipelineManager) { companion object { + val logger by loggerForThis() + val DEF_WORKSPACE_FOLDER = File(SysUtil.getEOCVSimFolder(), File.separator + "default_workspace").apply { if(!exists()) { mkdir() DefaultWorkspaceTemplate.extractToIfEmpty(this) + } else { + val loader = WorkspaceConfigLoader(this) + val config = loader.loadWorkspaceConfig() + + if(config?.eocvSimVersion != Build.standardVersionString) { + logger.info("Replacing old default workspace with latest one (version mismatch)") + SysUtil.deleteFilesUnder(this) + DefaultWorkspaceTemplate.extractTo(this) + } } } @@ -219,7 +232,7 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { fun loadFromPipelinesJar() { if(!PIPELINES_OUTPUT_JAR.exists()) return - logger.trace("Looking for pipelines in jar file $PIPELINES_OUTPUT_JAR") + logger.trace("Looking for pipelines in jar file {}", PIPELINES_OUTPUT_JAR) try { currentPipelineClassLoader = PipelineClassLoader(PIPELINES_OUTPUT_JAR) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt index fe1eb1bf..c4ed1d20 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt @@ -24,11 +24,13 @@ package com.github.serivesmejia.eocvsim.pipeline.compiler import com.github.serivesmejia.eocvsim.util.ClasspathScan -import com.github.serivesmejia.eocvsim.util.ReflectUtil import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd import org.openftc.easyopencv.OpenCvPipeline -import java.io.* +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.io.InputStream import java.util.zip.ZipEntry import java.util.zip.ZipFile diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt index dec7e900..68eab3a6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt @@ -27,6 +27,8 @@ import com.github.serivesmejia.eocvsim.tuner.TunableField import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField import com.qualcomm.robotcore.eventloop.opmode.Disabled +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode +import com.qualcomm.robotcore.eventloop.opmode.OpMode import com.qualcomm.robotcore.util.ElapsedTime import io.github.classgraph.ClassGraph import kotlinx.coroutines.* @@ -63,7 +65,7 @@ class ClasspathScan { val timer = ElapsedTime() val classGraph = ClassGraph() .enableClassInfo() - //.verbose() + // .verbose() .enableAnnotationInfo() .rejectPackages(*ignoredPackages) @@ -74,6 +76,10 @@ class ClasspathScan { logger.info("Starting to scan classpath...") } + if(classLoader != null) { + classGraph.overrideClassLoaders(classLoader) + } + val scanResult = classGraph.scan() logger.info("ClassGraph finished scanning (took ${timer.seconds()}s)") @@ -85,6 +91,8 @@ class ClasspathScan { // i...don't even know how to name this, sorry, future readers // but classgraph for some reason does not have a recursive search for subclasses... fun searchPipelinesOfSuperclass(superclass: String) { + logger.trace("searchPipelinesOfSuperclass: {}", superclass) + val superclassClazz = if(classLoader != null) { classLoader.loadClass(superclass) } else Class.forName(superclass) @@ -94,6 +102,8 @@ class ClasspathScan { else scanResult.getSubclasses(superclass) for(pipelineClassInfo in pipelineClassesInfo) { + logger.trace("pipelineClassInfo: {}", pipelineClassInfo.name) + for(pipelineSubclassInfo in pipelineClassInfo.subclasses) { searchPipelinesOfSuperclass(pipelineSubclassInfo.name) // naming is my passion } @@ -106,6 +116,8 @@ class ClasspathScan { classLoader.loadClass(pipelineClassInfo.name) } else Class.forName(pipelineClassInfo.name) + logger.trace("class {} super {}", clazz.typeName, clazz.superclass.typeName) + if(!pipelineClasses.contains(clazz) && ReflectUtil.hasSuperclass(clazz, superclassClazz)) { if(clazz.isAnnotationPresent(Disabled::class.java)) { logger.info("Found @Disabled pipeline ${clazz.typeName}") @@ -120,6 +132,15 @@ class ClasspathScan { // start recursive hell searchPipelinesOfSuperclass(OpenCvPipeline::class.java.name) + if(jarFile != null) { + // Since we removed EOCV-Sim from the scan classpath, + // ClassGraph does not know that OpMode and LinearOpMode + // are subclasses of OpenCvPipeline, so we have to scan them + // manually... + searchPipelinesOfSuperclass(OpMode::class.java.name) + searchPipelinesOfSuperclass(LinearOpMode::class.java.name) + } + if(addProcessorsAsPipelines) { logger.info("Searching for VisionProcessors...") searchPipelinesOfSuperclass(VisionProcessor::class.java.name) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java index 07c18ec8..16626f6d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java @@ -183,7 +183,6 @@ public static Optional getExtensionByStringHandling(String filename) { .filter(f -> f.contains(".")) .map(f -> f.substring(filename.lastIndexOf(".") + 1)); } - public static List filesUnder(File parent, Predicate predicate) { ArrayList result = new ArrayList<>(); @@ -244,7 +243,6 @@ public static void deleteFilesUnder(File parent, Predicate predicate) { public static void deleteFilesUnder(File parent) { deleteFilesUnder(parent, null); } - public static boolean migrateFile(File oldFile, File newFile) { if(newFile.exists() || !oldFile.exists()) return false; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java index 1423855e..8fb46038 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java @@ -29,8 +29,9 @@ public class WorkspaceConfig { public String sourcesPath = "."; public String resourcesPath = "."; - public ArrayList excludedPaths = new ArrayList<>(); public ArrayList excludedFileExtensions = new ArrayList<>(); + public String eocvSimVersion = ""; + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt index 05ac32a7..71dbf094 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt @@ -1,6 +1,8 @@ package com.github.serivesmejia.eocvsim.workspace.config +import com.github.serivesmejia.eocvsim.Build import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.loggerForThis import com.google.gson.GsonBuilder import java.io.File @@ -12,6 +14,8 @@ class WorkspaceConfigLoader(var workspaceFile: File) { val workspaceConfigFile get() = File(workspaceFile, File.separator + "eocvsim_workspace.json") + private val logger by loggerForThis() + fun loadWorkspaceConfig(): WorkspaceConfig? { if(!workspaceConfigFile.exists()) return null @@ -20,11 +24,13 @@ class WorkspaceConfigLoader(var workspaceFile: File) { return try { gson.fromJson(configStr, WorkspaceConfig::class.java) } catch(e: Exception) { + logger.error("Failed to load workspace config", e) null } } fun saveWorkspaceConfig(config: WorkspaceConfig) { + config.eocvSimVersion = Build.standardVersionString val configStr = gson.toJson(config) SysUtil.saveFileStr(workspaceConfigFile, configStr) } diff --git a/EOCV-Sim/src/main/resources/templates/default_workspace.zip b/EOCV-Sim/src/main/resources/templates/default_workspace.zip index 8685c67f91d8b034eca21c730a3d499cdb997467..cb37d8085d5b8b4a61f41907765a088e28e382f9 100644 GIT binary patch literal 24554 zcmbTe1CS?OlRo-u+cu`{?rGb$ZQHi(Y1_7Y+O}=m)7G7LcjIo{Z|@iXy|*IrR7ISq z=M*9;^T{VG<)uJCp#lGF$Pp-N|8ekN11JC&VCQH?uc8bE014LEGycb1++hGfpr;@J zz<&(#|MqO;|2o^$!qLgu!q&;&!qLR(e~H!lFIR&8|B7YmZ1kUa?VA6GYx`F`vVV)` zXlH2WY-DHa>}Y3gZQ}Uf`2Vc&Z|{At2KE2(ulg`VP`8~Yhdl+WZ@)lVQ*q> zVQWHXY2a$`AFs4$_R{zt`0E^u@R}mX%CQaK3qRAF<^a^hAtMaQ)Tny7&cD zAVmRDn9Fz}N>Go`ZOm=RVA>$0GyIypo-B?pS}{*sG3ca$7D2m9a@iKK4or-ZTta4FW5mX()84^t^dm~kMoHVw*MICnyC^d^ zrO6N00HbYy#UEzQ93k1@D?xLcq1k&_IKRW_tzDOJ;2pU7S2}@YOT?)!;>dGLraLP+ zIsLz(HBSr4!|}sVGedYQSGGpRf9QzBaMq z;)}oBnRENqVAyk}VQvwot@Z1-7t{0Y=}Hf*#%Llj^M->V^~FFUpoZ*nkZJmE*ct@_ zl67D`g?ib9cC)r{{o&tEFKWBLEH6BBx6s@p+)e03vWkeMTY#+>bW3uw18eVyb946; z>jk=ieyy+e7~2AR*j#aG3l^jJU0(3qLkJ^VT${i$51&HBH${3s8-bCZ!+WEMOs~lh zUe-Dr=jtDC$P6bykg&Cm0CnL{rvrZHr}wQZwjIw$?q|Vq)oH@}7FMz;SC#kzOfibi z#Y(p@S5c-pIBj=1&-*K;3gJ_hIAS?hqdOh!ey%s#Tv8(X&-<%Bn1Ny3R$l4MT?&YK zEQj*x82rZijm*%m?C2iKry;ht@!=$V3d1)__-{}@5#BCpzo0cC`NU?9>=s{WOgkyQ zqVW~2<>@ij9HF||^c+a|dN~;E0=|6)1iJ@rvadB>>2!j?YYThr^co`eXAd5GeNH+u$qqP(?^7Mqw17T*GTK z)I=Q6X(NV<0MHP)ng~T4G&h7Y?Cq3;|+52_107#Bq_f5@>~=8&dU2| zS4bNZz{Kf|PoBYdKsKXg{&Btir7Q&trWk}M?2h_*EMfF{oMl&Tsms5Snm6~?M6yKN z9@Xyo-Oj_7dzy`N}oFW>y?8;eLDQAtV2SETaI}4D=W5rXo!Njm2H9r2g)OrDBJ( zXjeX2;g{fh;C1Ewqbu{0niO1yid^{9+a3906xv~>-64n;r(E{fk0+25t(4*TR{ggQ zt)O>8igDu`&>e-@eq>Xb@)l5GlOmHJyRg5F0`7W7+_+;yYS~p1uIP|?qxzqrw>GzI z+jq{EixePrpnXw`#UVrp^YjPuQ|=Pbk+wy0iCFZ8E8N>0D8$>lfpUch5f2p^-%Kfp z>xvq0AIO3p!XSE#``>Z7S2;HorlqGXAc@>m9km#IM|E?stWxoe&a|3Y>p}2q=3QE~ zD;Q(=e&NNkNPTp62a!496?uM5mHOuc@$kqcPONU-!t)~Tu}a~{?uYyA&KQ#>+xRfh z%sO*|RZ)!D?&ibP<^%sMGFh*@0>j-jScz9UN-IsLku-@cprBE%W>5O~8N4E!&T&i$ z_74A9w4(+kDt5m_=b+XB-rQYSr!I+abMLnqv`A8y`UrE~M;VX;ZC9NI@s<`m;i%Et zhi*Y)j}O>Lh?azx^9n8|AVoUE=L`8E7rlDj+K*M3zv^7K9`%{X%O&`nfX?A7*ykHH2Jp6fPj3NWMA2Hid?aYig;H}@QmTl(Cdq+{I!a#{eTF=9A1hHiorjOc%L_zLB6@IBPr4JM$%0(#^l8^LtHw4<{4yh>8g0Yy9cCI- zAs&c>i7g^`9bqJ|Qg%t@Y%bxUKuST+uqIBINBwY3qs^()FVqQn_DUKjsg%;7*FUbe z4!miGRO?D3lI8pNb_YMRa0Jw)U!+-0l~Ppd~FS0%=_0B&|eBSv#^M8>GA`w4p^cO^D3cx}+)@=|gspDdyc%QF62Sj@Gfy@c8x#c={sNYB-3D;i+3{3a`x%=31^Q68|6 z-_rwOegldreZkq4PEeaK^VB6n$lDs4F=CS*y)fsLE<{_!|AnfB`LeylM7R!go=N*P z{3hDBW+6QNyYOeZ8`eX}ZMiuI8a)>a4NFode}^Hi0F8)&8dzx&jd|>?ESx4X{jv4} za<$gj{{2I-_(=kC!E)VNcpZcaT+JzJ^2P5xTkLUJ7)jddYGGiN{Rz{3uNGuCd3r&j zi*_P#ZMR0mN?0l_m?%oes2W)7vGuKe=E+{MLef>O{Xu);>vI1E?wE5LK(V+08<;HR#Y+5%clmJtp5-YMa zVuC)uLYWS=)4NWx!0(vy`G`X6<`jI57hixy5DkLct_JL6HrAeF{jLd)#ue{7==}Po z&{ezpafQa7+Evd-eTf8QGI$!gyN?!D@GBbt>*Fs;yh%L)=Z|4eRBdOlcs>fL7f{73Lqj=%xW|DTIu;ll$jPFz7))j0j!vnVb_1x9u zFq51G&cL$bZ1hJV;e8Vc;Qc^kJf_zdInxB%&lhwmD+$rgu5gn|1y5WU!}owLeXW-p zEhUlMR;@N$X~bot*z))Z$Lbe&F9#nbFxphd4cT* za=f_b^5vt@W(U=9MdQqb0|&cJK3OQ|pVXv1l-1*5Vxjgm9NVb9brzy_Q1f%C86sjR zhB~})gv+TJ8L)2);)IyO;AEezJuG%V^llsulG$?dXSW^3f#>Mo6MFWq%vA4uQD??D zqf84IC%xBu!W-giH1&52JF)BJ0(G)OTkoGj>vQRE(OmXP1p}w}j4WuiY+qO=(LKS4 z;ZP^taiUW->3ty&$Fo8e3*mx&eFG|cNe!vou!erds*#z`xocb<{qXjpG60S9nW7cD zk1ut=pP{eypC%(-x{f%Fjo9(p?L$Gczh@ffmv*7d5KlD5seyw2gG39G#SJ}YYUGY) zq~6TnP=*|2z(Sc(1nnlPpwasaC*ltliI`T0q~aCkjhcy8!v`QjB8UvbPOoW(aHDxM zzQeenRgXcnT*0D=HY>rA28Pb7>vt$C)jSqx05m;iQ$xp7**e(3p@wROZ3Xg=4~UYd zpDp`JYt|CHLLeP5 zBv5xTG@j1#CxREk#u3wXF-%4NPvs32r-QnLFK973e z<~Q=2eCYRppRYUnM`>u-=jwME#hc#`mKQ&ahm4Q;@0v8X`&2)R*>|_a_iTk5DWfDR zVuBb#InE)Fp}p@vXQvq-q+x%=@a5;G7;A)CTWZc1C>nhjOKsWujD8R*EX0t&C_4JY z@X&2NWR8^M_Hn`hXPqtHXi}xFekhVkuScyv7Z|XZP`B2X$ z!VXocX{>h%N4`uec)u!z;u`NEuhYQ}avPVywyjCt8z0c3c`owys8TFaRuA^)uZ*O} zj*<=O+sB6?>H(&R!JXh&%4KYqh^gal9z+pmLa+m(YuxRL-p+_^%37!Zg7TV(eNKjR z++J%j8V~Xp%@t4i*@5XjNm`7MYe})>j*#?LEj55Dk3SkM;!oM0rDm~kzd%QUTminhY;T6V*{sTt{?D2vU$e+3>+>MW8)CpEXI(gRT(uedB|M9hlC zRrqR+;6i54Ycs-@r zl&?>1swdhyZKWu_=rc;p-dxU z>^Zsefx=eiIL243v#LQa?G6Qjdb&Y1cL=P1h=^^Kq$n)bU{&Ef(soe)I&~rD zUu+gMs=V^8rWrLvhN)6pZW1QiUM_z+P7yxv-w;>ifx8(ae5NmZvRJo_6Q`ntSrFn& z5bdaayrPS#F9QQ~OQP0)q5Mul8GnM*G4>)-CW^4MTUnA2XL49A|3rssQeoFI~xUDNObOJ{K9S^ohS;qGfWM-Gzdb z<2kq0Ui&>5q{ZDFKmVr0_MyPwN8PICr*mIXbgJ1g6 z-G;)e<8MjZW2GiK0s7ROQ#0={CUXAt%R)q|qt=cjuXbj2FW;P=JKD56IjH#5Kf3W{ z6)B9q^ZO1PU;6I*#(sWt%ir%C@K46_Ka*=gf0JvUdotRw-~hl1A^^boze%ncn3*WK zSvVV+TiBZY&y-s8-*nnP_Ae>5Vl@N1LUH6z-uaI(#iFDvj#y(y&9kO8EWAE;QL7*@ zk~MSYhV|JNFNlTh5(m1@jBj5RxZz!Pr-bQ9>(dHPmD-Aq4yU`lPp%h0%_c8(y`~jr z(TTl_hYQqR3P#~zNN^+E8Dy3-KGOr2uYEO(=RIXgsqu=CVnq*fkj z-9A=}_AhfnjEo3DRBHYB0zQ!NbE+mZXaP_41TcmC=T4QBL(pF&Ip2V-#?N zfoh};`DwJPqtY~IsW_1vmI+agHUeajz6uuFG!~3UU>ZcZFy(oN9(ec#kcay21O?2V zhZa&$%QEAVnjV@HGPwz52b!uC{41NQCdf+W+~3nmGStQm$sTmM;ignk(uke+CoO4E zmUSVU4noy*enc#yx?(~uYxvOH;o~e5PU^R0VW;>%r(;(sBa0SSF#?b3evP;Bn%b-w zMDv6XDBSM~GLu7}FSpc7YHU!S{OyC+;I~mS>87nmD z74=0%e4>c%4M;~J?e=1GAWqv7X=Hd}h$a$*@MVacO=5ct92A{t1K;N9=I3@LLvb{_ z&EhgfR;bzyZ5$cx!6RpYCl^A%u;Y%NZBKV^<{~+j#r3o7n@#6z&8de*DnFK}a4EWF zcrs7_;CwkD^Tk9YWrIzG)T1@VC9Qr9BJe_IPasfo6jfla9#f2*qhjVa$8l#tF7DAI zRnC9zT(uG~IORYYj2d&7bFadw8~;B zpL}5{kA!=KYgrurDm3Kyvb83~P%K5(3cInVI#jf^X&>SlyUM3nBR*MkvGQ0KgV^WG zs0=)E?RVCB9voe`OvIJ~hQxs;6BbWJv#LCI2IArGG7Tpo{R6nDxRLA0XLie6yS zIpAI!06D4~l|1$G`M##FV)u>b;_ie|YqwI>p4%X&ctNQ~n65?O^o%*unBex(0V7XK z`9(EgShO0Nt6H?`$3gPpWF-tk=V~dHNu} zdCbgKQ#D}8G#nt@+0iKK#rQjF-^2Yz1m$E7XL>-NH!`byWiB!+tX(d%OXIye&Y*G- z9$}CZmg0;n0jVUXQg|EY%Q*6-JY^%hu10Rv?^W|ha&=$q5gNS==iA%WGGJ?sBt~|N zpp?tVzW@GrCMT!vrD|}3Gv1%JZ>~*;V&P#gTQ@~x!YH2SV4GHMoU9&-xuLLupRq;! z#ME7^JYgp|oZ1u7bUzDL$L!oQXjPoHw_i(WHI^Ic&xD63^~G-o&o+y-;zedMp}k-+ zzRHB(X#H9`T`|0v-fg9*i|qr`55ZRTa&@UIXwh_))wd~SH|pEoIRPW_eqnJwNk@5R zckOo?>m2l|o<6?vSBG)DVzG3!2>UlW2(`~9SEo`KrxzRN(m2`R1oTpkcMwg4kv7G8 z%N?B?SIj5UcqUh6SjbL8YcmbSc@m<jyDZI;EABijlCA7D`jMgI zo7@OxHV`6-9V8X=b}CTR(VuOkPcu{2tqa+%CUi@^w~cRw3zkcp_Q;|Sr%%V{NJCK% zvK*Vo`bsoQ#_G4;_Q8&SJeOE@yv$GT>K)wVwQle3p@Dqg&JZKAODMix_ZM2&=(%>*n zft%?~;=e_z9jLi#vkfafxez}gq0FxBFn#b&FIi++X6}J+KW! z>P$_~x3Ak!r+H1xu{}~_z^-9g<02Z4jrezl>zeW{OHIDZ52WhiWyKI_%NFE>w5v6y zeFK_fcr9T;hGzY*NHM9sI(0_;>orkfY``MTF;nthqQmjPKbq1AHGlCE@%EiKpf}5* z`WHRNuCdb*xfRkd_b?9z6JiF-{xAJnGW30wB-RWVd_++!Chr_xQ!cDpW#mTjQ}#sO zeFyyn0urYpQXkyW=B9rnB_R|+{v7jNa0}N}&8Z_`KCt>W3DbZ@>aZ!&f?QI@%|-VG z3?e}wzEUbfP0Ug7|VeXi2Pp{Rvc z@kyqsH$}uFV*BVw5LQJ+{?U_G;vEj^$8Ie>cd)#w*bEBqZb!C@-NtlX$ZkJ>4P1LI zWbSPx_eDCGG5F{YP%mR~*AaLINricQW5slh1;dR5*SK+=KXbS3;5fg#rFQkPbOKiU z?8o1_Hnvd_<-(fEMOGfj!f%&x-0XI(P6}3TWC(BtU6(T95ucarCpoPbvE;nAI(^M1 zMcoeO?Dv7NQvE$=akOI$$ZwW$q;*!Yt{HE{Ui!({vV93!e|Tqe?HWfnHC)^OVYt@W zkzSjs^E>OCfFp81|}OIr(;QOc*jAoJLtxHIg|Ao z0BTsogD)|9IR}g!Sb+QPWV*v&M0=s2PA|E;TcwF0fh@)&0$uk0Oq2gGq3uKd@MXJW zD!QD%_ye>98ldE=1W^2Qma6CdIu!aI=MAMmtl0sBb*3Kb`J<0*OXRYXaS&T?YDi0) zYQ>yQU(xD$B^cti-Sr2HQhjZ&^B{VGVBX@e3eK&>p04DSSwLFP6k*(r0{Q&WpEN;T!9%bbfz%arjzM4<2MQfO%dfg*L}SU(vZlrBV6~mj49?dTx|eh| zx=q4XCcSTAR%%q)MQkBmfC6+su?VG47A_${f9f1i=jo2eB^P>RcGm*C6Xr+L_HT>s zYKTsJukBp`yg#7r^$F4gGezOqdLjrk@XVIBEbTx+l z+uH|e+WL%aggHKoN~H00wI3rCsGjlaeQ8H zhDuUpYI&@1aa$r8I%4rdvK{QHlF>$)-_PJsWlqJeyHrhPVSl{)T&X;1+|g#(IQR!w zFEF%2j{F!H#v4nP1j6vzzbI(^p5DAXJh9Ss>xO%#v)Bj^_;?j%(EM1px-FvatZbgi7EAdX*jMS5L1M)=z2%@E=!3xbB8a5mzmxvAT{e! z6>vi95*Kg={vDqFnc`geuPMF%dnz{Kln`N42yxiga^gqCktxfBAZeP1u-mb5#mu+u zz>eL+5W-d&SK~+jvuNY(I$FD(U#!=LOY3H#iC^p8k62;2lM2^F+0K3m5K>P1?y54Nrb$x1h~#q z=yX=iX(c1>f~SOL^An>11V6=;;-d) z24FKxAX!P78;$D$>BYT>%DO?q^*{J??2m&i4eGTsk&!C2_bX@+Ytjh<_G$-zYj`_m z_>IXl=ABMj#8G${z7|m8%hVOcf(^Omsp~WJ2k2`OZ{(POObon0M$tJZdO4OA&z1|l zQbSogxnGZf*>mw~{c4W}#bgXmgwUs;M&p0R7K4(>j%CaWvwbdRO3%8_c>okg^IvZh1ilWxJqYWg^tK$v{@k^hRk!hy|Mz={)QwW1Zszn-DU(z zoj=EA&qqsJ12$50{yHFYNNpqS0`57)$de3qB&5?8YtAxmU$}JGc=FovH_!Z$6?|e*m5n_=%)puX4>HnI4@JoWpo4;XUN{k`&ul zf>}0;AcA`ZkY&Cs`w&LAH&Bg_yYP0fOgSEUCd$~gkc9?}Omuv?Pw*^V^Qy?44N`k5 zN(nsD(l{$7NXW`zO@~q5n0Wakx^cN38mb0&JxAb!AD zKy?u#^EAX7U7|zuopL>YxmZiD27Fn}z`dfA4Wg#jsc0z&%z6>nrd4PKC^agf0E(7$ zmvLK}=&nEbkvSPrGPlg$8)p>ePk;9O zF`o9gF*8+pu7qV$Qz_vMU=1@%K86Ts3Oddbmm(T81r5{vx5+HzORya3c;EoQs$}Yq z7h_I*_?mZw$(#~Ysvq5;ct8piH$2uL&CI8QsBy-5$XT-~q**b^S(l7jceEuL8UT+( zFmJLz)2_wq5Sc!8OF{(z?jD8bj~x&lV;0PT^>pL~NnIS)GD7p&`0ebc}o zx%g9U>@%!6DE%$dZdVg|Ga0lnX>}ui^!DYkb;L}j7|Wc9&Evc}#kmZK@%bX$7QhyxhQPIJ)SZ+v5c?tZm1J0 zq=~o&;MxXNLkdBBAjJ?2rf&KYMw(0NFfkW5`=t0JeP+Q0!&O?X zN#Meiw#5iFIppOks<1%*lYt1z2l7B#eX}mOv>$XRs7f_&(MCCAY5D;>kcmpu8COqj zbAJ#3Esp51$$Yh|d0UsT@>>SO{@eNKVFOvZ61_wPW}zm;BGtgM**9XZlwoI@Sk`0I zS2tGbh}fFgfU?Mm1_wDjPFs+JEkF^iU!$y{hCr?~36i^2)zQK$TR{m++=T3E_s>y( zlUaRCezCeittA1Fyc#OfspwhT2&&Q&haX-IOg{^f`|>yA=&gU0d>TrbKd~_oa14Gs zbij_#sp>;@z^^Z(@chLL7x>s9+|QMz?YFGI7HnzvsaM|h$x+$$3AMESHMyu;`9f{+ zKF;Eoi!sEib&d^2lw0SE8F?lvP||*MHy(l$<4vZEo4a~66oS>1p3SG>2b-4>J0%Z7 zHB3^P+EI0h%Z_JDwE$2q zuwIs#_2L76=cQpY1Am2dl$PO6$ObIyUboV4*a1PzB&~_TTXrfn)ZPQ!1G9jA1sKtD z+^eX&^^Bm#L3stZ4PPjc-GX@gEmhwYsLAqfx88aB*y4HClsO zy_nVVJ@D0+(q~#%P8(|xm^B<%=k~-r1w}tCBojcqe3`|x=$JKK#r@Ru{CQ=U`AL}h z$=tjE)IGCl`ar)X(K>PRAcg-hT$wA%J?y6C=X+#)tX>uA7;pUZq~4rc12jfNe5A>p zNs5zOLXPOF92ft4H?x$EB1M-7<0%*eu7VqDdH97`FkRMo7Tmi#v?God6uxm@fqqfl zT{oJ3Mx&Aj0ppI_ffoX450fgmtIcDETb6t^RJyN_zu&B(obNp+&|k z5NKZ4w-gv5^sI*?Z2?>MVSBSj>Rol)YgD&JJ+jS+2s^CQE@fW1lB53iLGQo$qUm40 zh(JA7Jq!f^fMEduT>lHcXklY-ZK7=cxA@cC&f55YGDW|?Eb$-v7p54Zu4T8uj_ym4 z_Z=`Rb=6;J3^O?au2XeE5mO(Fw<_D&XULdG%6Y7c-du#b!j<;jy~DA%^05v5)p;~bKb9$w|E8he`dq`TJTDw`>7PVPEh zF{Qw*k&Z@%6;Z7snN}U{HqTq0KoK3krf@jU$&e;P_gwCb324tnS zfs3@GC}HFR6Oq>WO8NGMBuu>%|Ee9bE~mQ?Yuw;AiKo;~q|W4oOvRQh39`&Sn@NC( zSXBLZILwzeCUur%mq&cT9?1Ka=9qI4WhnqdWfZp>*VT0AS=_6$rFbe&lTptQQE>T# z01cx_kVY%1b)ZtsmV80kS{Y5K zOlCSY zPp7*&j6BWA!(*ZPX87HQ!iN!O4}QdH?(>9sGjbGZROa^9ObOC*>BHFNo+sxwF>jDt zw|dmtAKQ-5-N(RRs{18#k)Ycm6JxaaBe|uGXKxOJ{kA0+9An)YG(+l}aj6$)hX$;= zk?&zgpN43C(N^I(awlw3=8@M0ubjcARS~Du_=D%J+?ffSDQv#}ijZm3KB5sOR1r!ryLzlvWpRetoj0bx#E74K>PWn@(a!&RF)x8 z)fC>r-m4C3cWV7)&h)vvPIc=!Wcs4Zm8ZQ_YFk1dB6DgNjSbx(hKC`2u9sLuOE~A* zgP2_p`=&LCyzw!g8b+-L3SRtKA!I3ypMl$6-fwkvNTxETGT@;rCN>x+xF%h)r&(-S z3+;9JkfCjI=QP@c;{E&UNfy1fM|+6r*VCm}m*04G#6bDCuY?dd^;s<1C$L`9iC1=| zvKgi8p|pN zEgYR#;8&R1$*B*!*PlzpPsZPteTyPVHC2g=33csY5ROFZ^Y-7y)<@mA!8ff zA4?NHR2|+#M&JV5h~+qoLg#nXHA#8sT6ZFa9eK)a7?#e5gHgGplZ zzC97=uMN3xGg8qInI7vx&UEY?8YPs6yQ+_g+OC`(*{Yo{*JdFGhM{Px@d=TQ~X|&Xpxc%#`6^MekikoB=P|B$0Q?1 zrkH~IIs1^~()srVdd`!MA6w?;8HOn#bLD(V{!Q_W#Ruxyjqwb@(z|V>JL8I_)K*9x zj{X;DhI?$$@O7Z4EV%;@ommBb5U@qVGgy^}>QXp$ND%#9&5VD&&Ky=gx(9fsLFz$) zkGk(GcUgulyT&B?#7;faS9siBS#gJskB`J7Ql$=Y4Z7K=dzTb@(#dGux?oU{#~yT1 zTzGC26!EZI2%5O@`UWez1sKl^oJWkl@t34|sbJ$YIm#Z6h1UHZ!QqS1>3rNvf z=lDEcD5nS4H^0b3miY5Z1p9&rg#?A41YdikXhnA=(xhu!!YS9i+Xzr{tUXd{gbd zFZyD75GN*Fpd2y z5$y_G&Q2KpkU|9Ff@BFRyng6YBWsBN5k8vz@s0+>fsV0M^b8^$GJdg)nYyf4S_Q4= z70Uit!q?M;vae-2b7tO8QNZr02KF~Yitt=t?UhSBB*4{e3@r;Is-^m{ATXnT}bnU(d-S{~hlumfz4_P%4Fq@!F}`xV;* zwDeUHmjqfTj5cxZC6qn4EzZg8Mz+qg8w%$`>xJDfPd3Jq#{>EG*y;!I14-HyHze@p z2WOZMzAod_ufE%J8;(suP&SyvRSP_<$Irc)7c+0+r!XDlUdqp(C1KOeAdC(`>@&*8 zqer=i%j*PKmE`~~y{*Q)(pRX|jtxp**b+N^Qn@1}+@-NAjY^O$+OD~KxNux-;>Tj$ z+9GLRgfb6m-CdqrOv{skJV~*RFn5a_2tOx-#S&w!Zas;rVZvPETAAnVNfcwZQX%gT zg8vR3jsAv?>8W9+dw->dP6hyg=l>i!3fMbZSSuTt{UbFP{qrl!{|p{I{sxZ!*uMmi zSDH3X8?4A**Lne_B`6%l7G0|n*lgKKB%H_DG!`Tr5t$Rlgvg1riox(jB$YN_?^nRy zw;hcn6^(&HGdO<~}Sc%4M>-fIN*VH8VbcPO4 zAp7<};sI$PT2xrGox$J%_UvHd`_N`x#=hc0s2m6QFyge@4EM&k4%{57j@qI;pohGD z*KMiR9&Lyh@#5&giCmCIfP{l;9bAdct2@%TV8Tu&VSjf(7d$^2Zk<3%|IyD6Q^}qs z&+|Kd1H{xHZ_apvM*cDI4+$_$9B7H4lS)A#i15%9#S4s7OsGwQ2cZe=z}9PYgG{vu zYK0lq#R!4vhE8Bq&EV6_DrS^Rw2k#I_uXBy8UkcG=a zeOMaBKk;0^V9q^^$B7>T>={_6tjHWN1t2d1l;Wy_l^vt7Gc6I-(6JEc$Zs}&M6DVz z;lgdN=~PPPG>zQ8M+j_hBvtlJ1p|i4wsJQU`$#RG?Z$d8eOCyV!}dn8p6gH<2++g^A;A--&R(O?SZ*hY_2moo7mwl7IJUl z-W-?-tXM5#D4k9VR&k@Zrp&m}w3xye@$atRj-Y{6R+W5=0t`(^7kD3p2bHe1Ve^PJ zJ|LFS_IX7=F}GtcTh&k;Sx#MO&ApBKUe@hAQ&Pa!T|8~x*9o_tT;8jT+rR-u$-0v$?rQPQO`>7f=p1Gi`99=ODDOF7s)@ z5!k+3_I?u#F){B5E3lNg`Hz_l&)Y8M;92}Uz3Z{aU$j;4lX)sX=ql1DQoACU+ zzPv2P0@;jYIfO)f@=gKW$X1skZt;ajz5@kF!nXe8)w_7w4TIJaS)ab$RoR!#y3+8wfE?gU_ws)sFSl`ULLk{s;R8KPN zPHjA>Xq*6OyyCZJ6vpk7L>zAm=uMKTKU-g_kcKk516Hv|KL&NTBL zU5PAG18j%J3Cr4>DwR5Xlq-(Apw2aEPkv8PerXUdMCAKs6wAV4r$>>vU4kIEq{L1d zuAG?NwAuT*P$i}$2J(>#zF)9Wzu_dBgTV9W6h#$?+`Y#lV$3tf3SdgY&j)03dwEzJB)(dH(dNkCp1bvBhMi9>QzoyWSeBcci)z30r+)JtES zf|_=BgsPDdT2u|<>8MIr=rE08;w-CdtNsd#jHvY88MmM%mY2x(7V^uD#w8FG0Vt8_ z;4pzwS!yf;n|jp)&>@`t5RfXO`Q*^OV~s_8k{Z%xD6YW}0}2$O1O&2|U0`33maX(6b*>I{xg!Y5GQ;JS-;0I4RVh{!>d7mtLmjH(vTB zv}Zpis}TE@VP^-bgTsi8>Gutdh`a4KwhpB94H%06h6#jrI6+ zK{LpdI2pR>eV_6&&Wq?fKDX%)dodc&z66y1`o4|)FlRM09#&5qP^sAFnOK3}Avu4_ zE8BWTecTXG@KX)<%&JNVU!JDj5-rdUJ<+_r?A+Ws1E&u4lzn|4mE`WeVAm~Bz zCE>39`(J^82Kda&n)0xw$Fb(JZ(@km*#$RYs*tf#E+NjA<&bel8n3sEv)WlzW=!6Y zG0&Ish?cmMhU#krF$4_WEg?^xG$NUe0OeE^jS8$~T+%$P@K|DUEkZAZ$iS`>kj4Po zhlfaW>(g&;1!J(6btBtmeaT;990P-LB&r9aUZjhV1jv9<&zWkEM%1s)Ds;!j28 zff2bo!LjJ-Nz+ilkS)zV$gST}FZs^S1+g!StQ6s|>fg$?R{WWRYA}g#iC8x)0<3&k zZ-ED1fh~D(^p^K&M@~QJ@NNLs!*FGps6kn%o4)(%j*ln2D~b3hJtwx^dy#w1-){H3 zcXYbxR3f%4Rs?(%I?$YbLQ;H9^AJlal8oVooIplYWRJ6w1j-n%&fb6c&YxxXJ-BVK z&rtj0ys|J};l0V@cDerU+8u^-o-dlbnV-iB+n+6Qoxb^$lZA%jz2aj_JZBa?JjL0! zX?1&7=nrALecvqfw%mFukI`x}zMWOis*DytIIeJ@QG$U3r7u=S8a@pP0@qhjrn}*q zUw7S~Fc($9EI;j&g}(8=1SBDpEE|!?8Esq`qSHS`jilR$E-nxBDN$C87bl&+OLi{l@F=H}> z&QxK^LFy;lR)u+UWTAwDYo#PbxAA#WDelf~3g&zp&DpZB5VPAFV2(#iTDxSav6@FHFD9RPEU1Z%584Y>2p1rNP=_4)Y}2`~Uo*JYETt?`TpwF>@-> z#Z=;rEy3@f@l=#=&hA2LFw2ssW_(2QE_Iiq--ULmM68$!>HYnZ*II7Yf85V za=SkwNz7O!{t=xY(6;kF|2jJD`G;5jXVZ#*ZS6q+Z+YdvwstUa|69TKw>iT9vd2TX zG}`~8#{=8{_l2Ac{?S7Is|kjG;(_tMX)5ub<>`rk8FPPyTly;`0C0%`0LcCSUr@-- z*2v`lYUL_`qU_o>3W&5c3rLBebS&K^At@{^xs-HwN=XVxEGdmr(kv~Az|!3*Aqb1~ z(m#&hH?N=ZzTf}PKReIN^UUnr*LBXGXXn1}>zvEp`QHV#w2_m?Up5dU*Y)$a>!%vJ zTFWM8`4RqQe_7!@7^+5^c_sf`XnLvzw!2xeiGO8ZzY^ib=p?rDc-`HZR;BEVonv+K z=K!HIR{_j`xjE3G`r!!3lvRIFtRM!WQ;M+}^wjfkZ%o;F+tb%D_HHNG%n=4=tt^Qw zCT?mC6#~3z>>Bk}dyxK8mUen2PF(^z56%MKb4m!?iX|4%T^F8yV=OJe)#9l(s3Y?% zOX59-XR#VM?g^v>q}mlGaAJ0uiiODtBw~^)Zwt~+s{~Nq3}PpK#=@1-qqS52%FMHf zR+fZG^_`M_%^(Los3KQ|A(j#JuDqIApZ7FAUSB;rNT3a-s84+>cv6?xscH2MqvH!! z^#@+bYWy$PKJqw^V$%;10A&z;s@%Tg##&ka@k6(|M9_k`ITOo}9K(unc >cFkU3 zrUR1LAwxQZoNBcFF}pme4GZzlkrQ-H-b7LiFc`A{US^RlQpzX}64l)*&cjGweRmkv zXX+^3ivp1%4Zekpwl-1!QvDH1HlBSzURA3U6advH6rA4*clBVDT)P`YSjk~hfB*jO zUSqVUM1RAZb%cnbkmFANy$Qf@*Iho7h24kAFNJ+yzu88`6-6Mh(;UPyyqw4pU@x3G z_ai=ksjGlM-3jvqFQXo;m!eP{xW|2X!Td^zV6lcEFj;{rlC?fPG9(%;;Sdmues;?AY!`{tk7~3-{cFNc%$Ih#+vc=XycgUyw8ize$MLrZtbdWF`b1MX2?7Vn_hvSYp*itm1D(m(Cp=@p@P z#AXTHRa`V{9(f+y%CnY*s@e{OpK&d4?>fCF7)Vt_cH@K_x9{Ll=GYiYdRiC@1IRP+ zieDVJdh%16F?l-seKDN7zU<$ut@5^O-|9v53-U14P6&x?ayq8UvrCP~F zgW9XOWPz9V<4!$(PwxF%S@ZJoH+gF6l$O{a*0u6T))o!>T>OU>a-N}~WyCmOW`+&% zE#7h>VntX(q?ccH!HL=SMLAThR_@+JsdRUtMAy5v8x%$J0$3m|i|>L*81+KVsVTDL=- z3MS~vGDfRva*bBaj%Gu+`{k9mnKX|JjR=J(~>Mr2T&)1{6xF& zN_j{vyjs~##`l%p4p5SS5SPU`I4*iL$n+@K4Nr%=*19-2d>E4F-6uum1GiYV@5N%mKLj}}d$!#-` zFy`6a+{b8GEjy&G%1)L)Byho=n10>UmYtbKKayvG+X2PL6CH(o%`j=RaCBzSs#`f#Z`?P4m3 zY5%Ca1w#dI+v~_8pjT2gOo=?slD-zBK!Sn9vmA+FMl>sd#l8*}dE3^UPss)e5tN;& z&2AV0j2iMyG33l0E;rjyB%`rsDBWJ1sP6Yp6o#l^Zd&B@^x3S`v8?dvs5iJW1^Za* zxmLhYq)mdXVl!;_#vEMsp|u&B3oBs7hL&ZKRBoL7N+)g)X47ep!wO=NW|5j`Hx~`W z%?;~5iVz;(D`1%k|IO#NO+sY~XHsg#@%O(nJbC02K5A(d%<<&t2BLEq#SyCox%2?t zW*|DP2V(GH6ZZsEFijDvHb23OdxS=9vYR$skO%eyL2jIEGuvo2HmZBZ;B&mfEPoMk zzTr^>5tc(8&;r*`?r*APjOSn}9DdlPNaHg53UZ-vqpeP?_)H;)!^Hf>q=6!vYG!Vp z28)~1hpt=h;tIJ8T3&H_v}n+0m~n6VAx(dsYm;2$nj}PoLZfjb=QWi(K4@F@68(sq zE<^wox_-W{-EznkoXRYT=0Mtw2ZhPZr+t!IAAuY+AkU}C(*S6o85^~Z6s&Z$Y8j`& z3h(8B1U+MyY_InQB6D`~EyL9_95Xb!B~k@wQJ?U_PGj4Jrup{|3d8g>pL@cbMbOZ0 zU}j*@Ns9YHv%v(N%9@$dL0%A?O4FO@$6!o56UkGN&EaGIKqp{U_Nkib z;PO$?8Qlxt*lIW2mciqzcfNXQ-fA-Um)?3A1AGn%Plitifrv>UqLY^C^&)xh^;7nhr)}dj z{gL=O`{GMdvXc&hMW^PcujCT|z`88AN>LnUHx$fciB)Lno|d#a2%hX5^E08NboD=m zDBPKzbIRDSSQly#@sMbuYZ_?l;ACju8Jyaf!gXG%oBu$^6fPU{0G1m!_2`yaM8+Z& zl=C(yBe>FqlAhxQ-|Ol)!jHLeE^Ci3%!ohIHWYZom8d7d+q~uE@>}9vl#1wXuV=Ao zi6xnaD?DMW1SPL?QcISY@8o8xWvDd2A`B)Sj3+8u()3@Hq1Ee-G~<+q3%oMxrpJ0G zU=W^Wx>x0aZ^Rx+vclkShnQAtN`8%vY+LnJFO%dsU2AVOZ0Wh)tCSArsjn)lP`++S z16K}WMrx1_FZkN646(tPH{0+5JtRm|o;h36ykryQrhbfSy+0{7*cK9Yi({I#I)7-QO9eK>sY#n0V z<;PTGx_kv)Z3c2;ANL=+FHZW9@dP6f20G!NqejZiW=6@Q?5dgT$*@mrFk{G()tm7lAgwZ4 zcvsoTWW~ zcG{k%?L=eYvlcalHtNS;bSK8|TLUSNabya4UAC=m;rO|aIN2>Lo&e0xLP6V{Gi_1$@EiGPxF|59v|rJ^f#iM`8r}+(@1+Gao(~5X3+d@`mmVc9`_l$6&>np3rzuz$3e1A}EzA9=fx; zz{NP6(DvYFXxCs%0=z+s;Iw{MW4M*=9E=yLtCK`;QH!soA(&76(YFdGS>QYzg}$G~ zSw9f$yu^*fW$;jNTUeQCR^d$-60x`%C_Y`H4g?nT;0M#su0rC7w;M>%QQ=);A0@KJ zLiwvqV8WAt)hETxYN*o4=|)&7VYkCq)9H!K#EUd~J}@YYh_bQp+z+2_Lg`LhW2^DCakGrQ|D}p&5?-aG zm0phgIuG$mBd{3$N+V<;l$_coks@YZE=8^%A=%HF%4fwFg1^+s~v49l$zA0Sb+*l% z%}TL-7u_UXJut^9ML}M$-8UmS^m!?zpwp^)w$FARzSh2+8d)rNoalXE_@Zg|Kr1pl zS0So$B;^dTUtaC?);yR&r9&gF{pCj)M;{50Jr)johwo}t3_*CD`;GJRSK`9bo0e9m zc#LPnh)xOAXINAbN!|L>ls5dK`{8yc`$;?y0da`QeUkT6 zXX&lDf8*3aUE%(|FFm{%_X(hWOPHsqHbbtRm=8t=13sw#nmPe4jUhr4uXtty|lxeywN7*OD{AvhjDis73ZA8-Pze- z2#wD5L-M}Q&7m>=8{3rn^|4ecd|f^*-I|WKB;O=Jpfvnco78009D?+x?_I@19RRblhGa=@)0;s$1j_Lpy8SI{S!}zJ;A3K)>N$?Sg8x zUn8qw2)Mro3KUMKhLR}y=O#Wite@{^#?W(;f1t>8(3IC@Fnm~Mau#Q|OSn{3YX3Ft zc?0`kQV8{%$wcG3tImQ=!d|U3sLIg6y~w7O;+SOSCHqi9afzhS+Lx2tdghER>`V$o z4EIjgYQ_k4E7%1K?=3&en~bs+)KcBxebTcMZAX63xsHy&q9ylSA)A9r&R5|XUYcaN zX{57Qk(UBqJI1aShV~bsT|+%n%c+LwPaOqJ(4LGCUtKv=n%SC%InUbA@Lfs<17T_g zc=@*4QLeS){$Uq2Dt+fDriH`PTE{RGf>5sbg^9(8d-MKqByw@Mgb(#BY%)hutdhHA zQvUsT8|xU?MKa^ITFN-y(|WDW`ebX|WZhOVMZpJY*zhhZ%REeGUvaGFj`Dy(UXHD z&9fC}{Y%`5+$A)zIky*O=J0jZ6ZmDI0c33Km zSs^-FtH>=0Fn@Q@LB(Mgrg@Nvm2^cry)n!Jphh!{zTtb0(+_by$b8s`#wLsrkk(md zp+{E`p`PnEQi7CcjBPvREtq+y?cs6wEUia>gn9r)11$2W{b^oS|8*H*nQ~BbiL3JX z#!X7(KRGLt{>x+e4$_7Ax7+VN%c$RdmVc@R{w@+e(vOiu{uSwG&*guP`*Rze_&?%? z|2FP-|K;o0zingxHTs`p)Bi{8=|9H)xnU1d|7WaUotS@|AAuX+XZk~+aKXiuFT&-uA?JW1;qa|$bae4e9h!H{>DzxywZxw&{QQ>vAd6jt{==*N zt0?v(D5d288uTxT>>B)=MD{nY;jaSOkCPtre+U1wTy{{&fLdCvh{by7LdECT$U8Q5}j?zlG%v4TYAD% z5XjDKKVR)dX?oke&Y3nYiO67OA7|(iTeN1;*|@z-rV~eG2n}ui;YvZGRSF?}@gF7$ ztkUcnIKIVL_qv4r^5Xcy5Ds{^8L*Q;$JE#AzhK$HIY3RO{V`kkC3L^RlDQ$`1hy&2 zJojdN{Q~A8;KP=1;siNF5GpTb4kXwO-W8S~ zwPw13PO?>;&oM&8>5Qbl;5lGHO0kAiy%gRbI;2MpX?7$*$f&0}z{C?F#ikg~51-$YfM zaHm68^9WovZ-??knPkbJp7T<_#@|#}6vRMx)j~Ew~beL zDH5!TqGBeRxDprB+RJpq+q@n1+9Wk;s5AmBL!`=t_jjvJ^h?fYSzVUQbkXBx&*-#? zYM;d(uw`~#xp3sVEo2$qk)v_l$;gDSCi6upSUx($0yCLy?+o^Nq2oexc$XckS-{wUwU>bo^(#)<4G6$X!^_ee1zMp^7S)t0qHiL{#a3gOfB z+&+Ji!0KzNo{dDLgu3dxbzk)G;PrbD=vf#VX46`%c`DvvnV)TU?pf-n&0T0W?Yc6r z1_UxA{^qJrAYZT>3^}aZ{%70b#W`jUmxkg3En2m5Pyz|6m5>oaS z6j1Y-0mP6jt|G22w=|HvJe??DIzuzxXYcEHSvg49va@1M7zCDZIbQ3(m>XV%NU9cr?LE#@j$blsT85_}V7Zr8_} zGeP?zX=Og6RcplEWxY>(wJx>fadVfX? zk3;$)4~{D)whduWz=ONsx4zk}M{d`>jP8T9v-~9TULB}_aql+2Aw)ly2)) zKI}a|=c56ukDwfW6{F^mk699{0**H2_>pjj`2rR-l%-Zld%OyT)eu>f=*;#fpwnLs zM+~3g)lYSe;s|n8vs}L4jqx%HS}>nKOI*F33C#JdKOAg*CM<8v9Bkic374%solwH; zDQORJdrkoL#j-|{IT2XbsQ}68SHB=$B)8-raVqzj1%nMB$#lyw2i%$zr5A}W@6!hj zy->H_8DIcfmibf-BH#Flt4gI%YDcF8Ga&((N3-?62&LGF6fq(RVMoQ!L7@BfwwnDZFaEd z!<|&6S#duW4%3)sn!|9a@AQ0F)Z#`(ul@eXm;9GeJ9y|se|{Dy+tvUCmV7z0D@OKQ z>p3FYw|$W$jJXs(;UjAhkwU0IHu;yrm_AoaIW3Q}Sizt4c}e$;fN$MLx5sChiaY1U z_UIH4FG0;;5B%?^GU!xmLCP|v%}{e<($u6E4e5Df_PuZ=!J0?7?VvR6%aT=(=Vk(e zVT$Y~C6mx@$*&iE&y-i^bK`Q0((6nNosPzppPmmGQmip4n7UwO(NSo}nx7%GaNb-Q zUc^c;yB)(NB&wDfvM#IMWWeeWXjj`ZNw2ag=ztmn4{+CRXf&_LZ0iVDuXBA&)&m}q z+|lZL)y@G+S;XiQzZ>6^k^EE$>8=}7A>M+yB0-*Xr>)z5l>36IXN{oLrNChv+Q{Aj z`AFJV`{aZIs6QdbWP3}3a)l0ZFyLwwWmk{d2=y!gYo1or2+$LJY2S1(oSO~!v7zw2 z^o$sbk_<-<&p6qn#xknti8^PyBhf9SJr`>&(70O~O}Sc+WFJc(4b)X2trx6ntX(PZ zLl~E6*xO19BTGrz)+U`k67fbj{Wng1G#!%2f9|4Q%D9b~PMx%WPI>E^r(1$FsheVyIV(GiFT~>~ z%v4eHf8m_ART+*jE8iQ}J3Vb2!3NnM!zZ?qWQ&dBNX&TTjsmDLA}RIRQM(omn2~mo z09C8_*~Gh$-IZJ<{mP1`dV^xmdFUY}y}(+ODe3YI>f)2ow;Ep_is0LP-O+y5 z!`aLCg4U&_aCVrm?#xEJNtl`-+3|Jl&|tS*ttvOMW~mv8%Bdo#R9^BSey(E0ClDan z&}IM6lQ?dXG}{LymP+$0b!;x4>9jUSR7a&*ZmPSphyCMaTAmLWZuuhyGgoh|VAwcM z`STwtYynUy4FPbA_j=HS)eh8Q9Y5yy5u@KgZyWJ@&tTRn$E?6?1drS-wHIx+#}Z)@ zU!sQ5YX{%bQ%*jQ6eYcBOffg&L?StKMI!?x9M)7;lc2vdajc#E@R-Pxk6x80OB6ZY zy(`UH&{s&4R_NBJ&+MlyhbDTXo9hTvndVgxvrLD`>4qsCMZ^^N*?rzyD;Ce-MqM8Y z<4YQyNfnIpp^g=Es9_u@G+o2M-h&*%DKi+CO4yd7O`%CCyCL?9^Mdr1~0<~w?Y+#F(Mncq= z*2R$Co@Lw9QI)nuR2$%6&sd?0Qh44iG2T?Cj4)zE!senVWlBR+JA}-s+jAMJ3V{S2 z;SI%vOkJrCxv0~rhwgXQiP9~yC6}n;GUM~HdA4TbvQC3HOV z^Jb178Arq3{(LA~Rv2jTzg&$0m1XlH)!&UVwjk{wH@=O{w@|B10+kX2jcfpCKmotM z5BLvguF3&P885U_H(Jg(??AI{*iN6(eR7H9xT4ZqX%VquC&l2Kjm65m&N9PKT?JfA z&R{>xNr#+DPZi>)Z-8%s=9Gi~6xiMT!W$_FZo=&^7}n~8?grl31Ac|;0oD z>TCbBeS#eTBm&ecf7)fZ ze0M$CDKlaqpCFtD?x>GTF5+HAMl@>Gl@C+hJho}ccorz%LRP-%Fry;>Ya~T8q=I9I zs-qYqd%ijIjB|o{rLQ}Znv*gAxXR{;xyq!M$6=++F#7A1z@a9tYgT-}##yCKxB+VA z`z`qb*F)Ta90ZoFUv>=@m8DGMPkH|ML$aQAtI^u{Rmx0gZtQKNA{`6Q0jHT^S7KFo|oH+|jy$Sk&W}>@x^Cw8D%4H%?w1_};BI z!jzZu<>2UI;RHlQCEXOn1nkytQ#c`zq}^j_ogR-&P5R*Zj-$rpCuv@ajs~Xmkj~1) zxl19RKVhhi@n=pkoW-B{(mS*%n&1c$bn?R!ZJ>GGILkGpzamH8DXi#t(5(y8ZQ#ot zDK@ithdz%Hs2bxR8ZcLE+|DpYni2b_K>!}ycYi%^?40E-fNB~4aa<+Q{a&+?}3KZnH zzyk~+@ag%!VO8vjE0u^sMoUf1!lKP?iq@ztlBNaJRwanp#9SGPTo`ftmc=36p~qxX z$vJ>Z1P;Y03hPF5*hqPuBT36~PTjsR;Kr{U&w9l#A|=JAA;RJ-rN@%r?TVkv=LIiu z5S}vvQ&gf?j}b4O%9|hQ*M{;p4acuVW2umOC~@i3)ujz(HBV=PSapq#r-Uda)o)xZ zNEhOY{edpaW7X6-E+4yZ;_jWTq!J3u$2`l15$cbB#s}x1>MWg-&w$&~PPu;{W_@`y zq*)XqV1LmNni0fWgd}@dnCgT(4^)Ublh?zr)`#awQ||8%atS5>32Bul3J}Cr@j7=} zO}EGHGq-E_=F}ENsM+;ydqt8fpaeNU-3BLq#j3GEhqvGlKXUE%4jh7wJc+rjaplDo zc)a;AYEybLY;P#4Eo0WYQ$Q@vIA0^F*&vxOMN7q_(qbB=U2av$v0E zIk<2|tNFXHyd=|}k$9fchcsx#r(d}{)MM4|{~EaWt_z=yqzum|UUikBEMcsl>a`4&menNj^|kub(jXSiZM-Ou0!F@SCb7sQ(bIh3+i; z^}29O1g+YL>}5X9AV(FQQ7&7kIBR$2LhdHe<7dpfMG>$9B)bc|3o-o=6~Sy@`F7kd6zdl!YLH+{2<^4*oX@pf5h%!{c6L zOX? zLvkoFsv?ytY*!p;JF$bHV6jYSQ63{Lby1bMn^07fG*-2YkzY{RoTyhR>QZb%Un(Z@ z4CC6OVq9e}QCJi$SGVJyU42?A;&Xrvl8`R-L@ydqoHK~%pmIg}p1+Wu}z*e2!u1ub~M(GV>!_&7e;BrW3-ABKU zviKch8PF4?y<2HASPF6O&Fwi(7raSKwpO`Muqth8yQyN*Z~t`|YdW{A%f{yTfSEXp zQ4W8UeBkDw@(RHvYo<+@%WFm{Y zzHR-z*RlnXfsk(;$kQhIY`sXDicz(?lv>IbjZ9#Giad^HI|UES_f_n+k!!0o;uLQL zaR>&FJ;4Acxj!5jteMQ`u6l+P8)+Dnnhns(#f435!Hot}n`e%TwjXf-TLQiLZKB0S ziXU307E7W=JRvx_NonI)I+|b;|BS40-F}N zrbE!9QF4|{5e5BA)*;8Ga{UEb_OqG~TgK)Yy0NgSc9D$Gj#SRVtHk`yST^66J-tY0 zYT2fu3*jYL*fgRHH%`8xSwOjNLn~g&Qg^0;-{Kp1$TIhYxizZpT#Dg}Nw1pb{4V_Y zZ_8k<=!j?Iw>NirmMz=*RNCa8z42G#Zx}J!rK|G^sq&cCb<%xvyWq%`jzA{6Xc$!u z)7g{#eokq8@$4-S;^6`Ztxj8Ljec{Pqw2gbkQDOzU*E2$6v!wNb(`WlI&5M+Io+&~ zeC`jqJB%$JJg27^&TQGdj$jZPz~vPc$*1%_Zz~@zU)}cJDwvrP5!tfKD^nONY1{7M z-);P-m`UT=+L9A=ieQbo6nY^xy6*k#&dCZ>i^>9C4kK2U;AnEP3&66Xj8{gd$E*B3 z;L+}9F~9{mwjQ6% zC+~X!xL&xKDOVIX1FE3K^QMPs2u~of-)zFS)QO+4fVh+MW51N&;)A~hpp~<0#Jnx> znwSPNOts$KBAw=QM+C?bmbw&7C=2@`FT>{bV=h%eChG=c;yCbLYx&0Wt)nE&A}2HY zSF#CZyUu3luL56_*Bm(0e}z*-Z~xvU*gY=zXszESp(30ugjFMsymPS5fW z9XV3LSX)cjJ0UDC-{y2;3Z5{i2+$vg;X!P?03-@9N$jTnb-ET1AA|o_a|^uPMnnrf zEMcQ~3Y)ayWGMm6SOPVZ1A7?o9d~mggVx&|k2^bavq=a$+_qNxi%dYa%*TG)o*WOgB2=jn%Kdu+NigAj@R1NjO18t-Z^0i(!6}-)n;26XI%y7trFRw^(eLbWuaDVh}4Aw^Hn3a|~JEu|W9! z3IE^wJNv);yWuX0G>^YJdp-jI0MS4DI}0}lM;mLSe{^G}o|EaZOTiEOmKYvJm z;ER6jfvMLd_^Py6HJ+0WO2|VB^QU9~UNWx*PaS@ipTS7x{r=!NiL|mrzF*yuXn)M9;Bn=y$&$J{XL>=|Cowr#3(X;=wdcu+3~%z<(S2Ydm%D4p7nY))LsM*t|tk9`-0SVNSG0x1hUc0n(Jg(7kYMvyx3bYRt zN?4rhhsZNP4#BXvU%o0%MrX)R`1;W$h>^V3Uv5eASC70;C5*gvnLrm!3)GDE87RCE zD{2~yh%eF!%QaG=Fm^5Um{7Aaq?OjmdXu51yCpYw&Ze^{-bK{flj*C$%_NtB4W~h? z&q3|R_MyEilU}XTRYIn9F6z_U@$Kwoi@Nw(DdtvEWKa`}GUA>o&II2b)P( zA1D{-YM*WYdsoFa2!dR2qginENfhE<65F|Uztu_O%8kr5j-c~)1`NW>o9!UyWjj;) zV>{c|bg0%&WOaxE{3tu{V!-yUCkU6D>bCBF|MYF78(%*xwvO|=*JkvsleKRG;{15~ zz(t3`&AxpadTvs&Y@SgIb zz0hl$VQs&}EVnG)U2J08X=^zCa9+=^^CxP|JO?S(rVM`vr&+lR_>;f{2g_mgTij{pmgS zW>)Cmmp&dtQ{O2olEX2bc#uDFEe1Iu+-|pBk;fClZ`L#3*VO{npeq@cJfdgU=jwDK z6b6RJPt;HnybnSU1(u#YbD~50(io42sYIIHnR&JQ{0&rS&s?R@bmOqDep|!RgJy=n zS1>EN_!lr7?5)MS@ilynI*gJKjO2&y^~@WPHQg?}cFiqXws)kjUB`hGL0gQqNFZ~vW$=F zgtlEjfKS+C34atanGK44*mGaSmSzk$(>r)Q)i~L(Nhg=nHRl0dU>lj~TlB_Ky3q2# zFQ(0gZ|PzR6~D=ob<^J~3K31H*;IhKO1tp?00UBNrKBz*@_4=Nt2~zX=Wj<3(-gSQ zAf+0QFI}>X?pz)GFpcx&L^q#GcAkMddrPePZLf1r)xBj1j%Z+$W0#ct)7MK8lb#00 zdFgi%Gk#BAfV`yqU=p#szHtX$pCmgZa9RK9KDn|LGYyUQ z_kTSo*}n{-R##9{ybuL0FUryRgmo|4h|If$g{#m*OP@3mn|OtUt7Z@sKK^=pgoUsE zdcXd9`Go#?G$n`M6hs&%GaEtN_2gTl>%Jh{G8?AfWJ@n{ zf$!!wyIcBP?dzd^-`G}kAN5uyG-Mwo!fK}zNA9L?41NUOda-pY0+Ln@+Xd9X7bI>C z`x435f5YBUQI{H3omFV~9bKj!izuKGqe{fEsGU%r353}3ReBZ z@DZh^V}KWfX?8mwy{&6^X-I{rpvgkQ(s1@wr$P;V-Y2-%i3nIhK|!>hS(vs-<%FVG zD;}CqLtzW*mjRd8tsM#f0}!TA3D6KCZxsKtbV(S+po*WWv$6m`(g}$CBN*HQg^{)8 zSCTAoG-^75A?j^YAWt4_;{Zf$iHB4~pf+XkUBD{~YF(Y=3Z2EHF)B!_fariaK^zpo zKoqVO3J9TCDda61QiRe7qAKNYGYD%+PfIbNxH!-k?j;pCrE7!QvfQ-dhBR>~T?i^0 zH*s7)h)V+SrT)lB%dka|E?n7U1w#SAFR zqHfM$&KBdAis0Eod9 z?h#;&@&r${W#XY+;0HEpVfPBSoT{?1FnsF7Y?P&FOH^K&C%&7b-edC2m*ktC3(Wca z%C&@+sterv8|HE*ia-9amzUw*RTcgz5L`Kpvaka{L+Jf}xEZIuK=hlWg0B_voWgSG zI_$Q!oC(}(eO#;}Qn$3tZyUy?toDR&9he;p>0r?f(d&+2$C3t>OWha3NgIy!nZ$~h z-L2QQETamrN-+mK4vAJJ4I}vV$8Om>!z2(2wxZ^J}wqfcg9R$woJ<1qKXsp4mUNC}DAx2fPKs&@cLkpKZeqIs8Enq>juo&@?$IdJMrv7;!{e1e+@$`{!du{jZ6#Iy>` z;7^CVzV?{K1Q_ZeYGM%oM1YKZ5YVKwt&g6_$-MYXxU6Y{Nw;%!x4+VUETjd@EYS*Q z6ipq+Lg{hY>N*aJ3RReEDz(JHE`^qgd_Ds|ZR7)0_GU6dV5 z>N7u&8s`ff9<8K@evp$I#y)v|Gq{I(5aa5#ev$E_J-PwQsxL*}AD|DtdBx)7Au|QC zqA#51`+Q_#Fts-RXOK52_aa_W&(#Eiw ztg^{92@#BFBANt|%6X2>Cc@N^s8^$fQg*GwEiCUI)sUSdiakvVLk7F>F;)3}7?8qr>mAhpO zeiO}2KtU#(avWY8^pS8k&~d0sW)h#*Zi6m1s}93n5n8!)I*k20%n(rCKQu1*hT zkaJB;Ktr2yarrcUWAL+BUCIMf4LeHFJE;wE*z#^>R%Gd0lbx_HE1Okkm;Xk#A3tYK z958mnKWG4BxX-qCLoV`xMwmyCk3;U+5&YEty^$c~A^hq~u(TIzabJSl+M)NGKYE18B@*ITB{xL6sL;pTDR`S8BZ4*-!06&R5b{R;cq}w34$?v$~)v)7&&;sF7n_|E$XQgK-rR+58j~)%?W9w*Jgp zVC9oOH&4ItUAKn5X88fVVfkUB*RYYXU$}83U^@(J)A@Fmj{RwMF=Lb2kCQ>8++TY6qm}F8>Ypbw)blA}eJQ=gky_qs#&4FGbC zI+1DanNGXlx7WQ-7+&9r7LPs^3($Z^r0SaL8iLJv{lwN-`T99VQESnmV2LqTK@#)Kw+~cQHrsY^B{dX=Owu+1L(tPNV{GkI^#?`2Tt&!a za~$ZP`Oe8k+9~I0)=J~Kb25y103uh)G9$}o-2tZN#hu00R$6q91Pf@!;yv`DevvYC zC3REU#}i_70yFKj5Q@n0@tc4oQCTJ{C*R+=jAg3Y%bp7_p}(DJr4$HQSlls35U`GS zOv@j`Rn+dePw3aD645aiw#RZd49O@2&^*Nmtrm1_gOXL3PdhE)A!}L=(+@dlOmT(M z&q5Q3ZvvZzsVbpG?N{yfN@Kd3shNiH2KOl3?1|t%F^m;nPvN@=HQ5=ub^hEQk{{d> z0w#uL61WB1g2>*&{M-fPfo|6{j7Ath5CfmcnIP{BIPuw%!wd1XhHx#y_JfdpFUldX zL&BQDo_N%OWxB_44Xd6^=e&+J81;&JBbi<*VE6$ukxP2M=0xW$g|1eNe&Tm=U38dkVcppmD0q&Qs2??RUNE<>t51;@AA)K#axJ|o zlI%e6p`e0_-+-Jt3#O{Oxv;miIGbjzr~7vEaFJYeVH-G#Y}n^ww3DrEz)nhf)IE{wiIF`XT5lOU|OhE6nC(! zN1(3HXK3zqjFQRs1IHa8N|-g_;rqZZM#O)_eZKBTy-r72dA~w~de2`0PhkVcY#M&7 zbj$9djlUPXQyi_ufUM6e0d2bK!2$YiTbjpi^1S$@&(YvX49eeZZF&q%5i!=!Wuv@S z{#LzdvUzt~?w)dDYHfzA7{lm@;0!Rf@Y66V;JMvZ?UuFj$^~{G!*P23e!J6xUWX>z_-p zi2*A;UJ)oRk6B1!V9#?~gU!muY|f9GwTj6-`REzIMfEpo6ze#k0oG>jCO7OTTHC$< z9X{Dq`&vvRvAmGtKB;dwOX8a6o_byp zRpetMS@zg}Ln_sC4`}yb~?C9jM`rR;b>#U_Btkce~vy)=tHS2UxpqL)O zHw#_4ntbk5-BGe7>mWI(uoxXq$ncU}oI;y+ZOHiq8{HjrS=NowM|1++>5+VfB~I!X za|KmgN+G^2WJOJ}-!7TY`1Q95D(`B9bndg-qa*7&al(NPY?!ZNhqFp$5|&(#Ip+GN zI>*k#ZcQRY1D;htu?Q799X?t99C!}CMKyPYTaK>bp_&fK@NPOe3`B10hru9 z`=jrG(h>KxcnR)3bPGm3Kw|8~W#lHN%><*Bu1z^X@< z^=>hYpVRr8m0po5?qqgl@WA7loFT5EtY$ z$sexENIHqMiXZgHQxhqr7B`+56=hH{P*?rj6=w2k3l=)>Z=nBTnB@_FK@5~tpGT_&8xUjK@9BVz867T;RjSWk!avMZ{ zu|#dt>-UPFC7w>xWVE>wrM9jUW@PI$(ZCx5AV3eJ7DTL0o#6fxgV29z2wSS(ChcrJ z2-s_4jUy%3>YMO3FU7_!y$Q0S8SXn$nNZ2$!MK1R*WMUAv*r6MJa*5Ob<&wgZGu@3 zF(yIWHFU&ZLOb?dT#6MZG-|ejfE|*UEYO!bs!fDh!;;vmZm=iw&t`E1|DZ+08_*p6 zJc#!2>-+)XU*&OxQJ(=G#m!gb_RpP`yu^cT5iJz^98tvGlr=4zQFLIwMVcrf%@pKx z3W1>do5?llE0{nsBg7%x+N0qq^da3h-OeP(q%Hb=G86sIw>%ktTRzx}k%na}UigYN zLM`YD;~}r)W|b{EA*mgZJK?6Um65QNYFjfZW{MDNhUO&SbBWOLCFN1ab8NP-w(2E$p2K>yswxQsh>XL!ANSWfm>9%$}4k6c@NbrJ} zlj&Dn59r#1vCODUY7@B76J&Me0*7EGVON$+IJ&*MpJhyrhZ!`&4%=gcK`(~1dftv} zxs&PLuTnmvm79{4gf)awcJM_56HkVqq+r#7yUpOFJzQ*WUvSor%NWsCoV|-4aM@yU zYHaChKDjKH6bEMx%q;l5qYXm&RUa;_@+05AzS0{_waZSQB_B4DQb=7z&*;oZ#pgTw z&>Fif-cRhwtT7BVEa~9~N^bSuJICw*A4g-Mh^LZim>>s+EKN#dg+TxoLSXS;K*2%c~2R$Eu6XD)SENV3 z9se?LMQf`zP@#k8w$Lq+-Y;!9$^ulFd)qWuR+j^h&+D!A(<-o*75xcd#QW_7F&2PS zVGEdDzOLmtYPK~lb~VnuJj@kEZp-Pjdaq?b5~Z|UpGn-FVT z{0R09h2a|{ymtm_o5uq&AJ8R#_#uW*U#t?ujj>bccAh#r)lAy?BP1Mi)`LG@eqOrU zKtCVpL7pKqS=KceQp*U8%Z#@hC6;pBTx0EDvw`RC#mktn7% zTLeeH-whHI!5I*RL3!Er0$SKk`>daQpZi(1# zYR0gW0OT$cQl=97fKJyEb z$VtG2A$Qa-V{xT+S6H`7PV~}fJ03&KR!G*ZP_5oy8qHYn3o@{BR)kg}DtBN=F_X$Fj(d?G#6qG0L3h5_75A=8*;qD>U{mE>!WLhz z$mc^)@v|%EK~a@KArkU9?K#2Zy8ya{{YQP$ViEt@xv7W1P))8e-VX#}_^e!l*&AwWZ7>cXzn zJ^bBWk&Ys_+j#^>@1(DPkc48TL{2ljB%IQNcG%1^cO!HZ_q-slt&p$T4ag3d17o?U zTcyB=MlwJEg(CM1;%gr3T z64|qL2G9$)$|MA5u^Z#RDgE9q)Yy@cUob><)m!Iq}&hQ%Nu#Rz-(>L6xq4|5$gdaV4 z#e`n%<(W3{jzMROy{e`Metcwy-@qBHt3**4&%nA|l5^gZD2FA^(>&;m34Qw=_Z#CR zpA_gNj04(ktt5#!lpGD<2c!~w5cHhg-~}uROW+3JE$_Ra=Rd=eBdF_gI!g@6}z3HTz+g5@SjrYHk8JVf^@uWo)&ZZu>Hk8yyA0>b*)t(rEgSrdy zp74p4D(ez0tb+s(Au}(6+bj=;b(q-@T~9EWa*yMOcgb5iBheO6s*>}iOI9%9p1zN1 zUE|MAuuVSu3^QnMpu{f{8B}TOhwngAA&;W{S{2zKuGI*R&<~pm1%34KISeU}fAuJB zbLSYWtkIo0B%xFk+NvlVqtVEPJe*D4E_kvJDc7!kPjoYNpXunxk>^+sC*0KzFNsN- zeU5IisQZHg2Cb*6k*xXR&v8}?M)kpTHP?M`q-3G(7+8yll2&Ik?EtAJ^*x}Y{gzgd z0yV6&8Shll)K93$gK3zJ*{u4L_%zCF=FD(py18Q=;8AobpQTiRzR{yBdm!YYH{+;W z4IMiEIoXejt5WuS^Stg`zh))x#Qytr1wiV{*ZSsScburEP~oX_QvXBiliPkEwKF4$q99%$8hAvD7&sp_w|i}0r_zQwGfUk57&k$pVIozATMLc0#IsguK=E&dzmU4$ zA*g8dis~dW5p)W1`0-5QQX~^wUEh&fP1&;tVNBDU5DPXV3gQFN#ZHvGWb+VKl}X-N zT^`HKiWKP-NrwfQuQsXmh#^o-TwM^R_1c(Fg;Kd04Nd6~O$&8(zo}!0xEhIg&$CLLO*WkLB?NsfSe23IK$x?BfK8IYi+a$lF|mG(w0Htv&%td@>%o0If+JFMLi zCE^NlD8|uL%^RY zSkBO&kqa~4SgTnF){Z^2^1t(@LoG4x-$Sr{FhztnMmt1?Pnya9#Sim?d# zPjwrZRjFIQViZF+04ab$8F#{xNfn^;0onhS6)Kwp6XfGVXJ`|aKB6;ZK5&KiCzDB5 z?p590PzqDLU0vC|T4xp`nL4@^B*WR7E(W!pqR zIVDyXMM^-hV)e^|7$_eS&ccAHu-hhCYGn$)Pl^f>zDJ2KpxuHmbB=tesIU6dZSpr% z*z>-3e!OkI6F?}t+EphlwH2v{o*kl%`^=)#!l z=NCaG8q^wO>;C zjZuOi=EG&uLw=I${Uju$z{lK`eLdnUCOrR#O)=odg`sAQ{_7^(t;FXo1+2?QtZN7J zshgGcjObn$#BG+2cMj)U?k2iyOs7G@=Md~`*_E`u&dKSN*;aPT+J8jUH~a%JW`Tfk zWo7k%k;li4tBLqeiGZ&OXV-eDbJ$b>rye7iIU9C5k4B9eT@;d;aoKXia^SqurfaFH z{je6oI(-?y7(+@_=31sVMv>SVxiq2=R)aia=T_XlmpX9w0HKD^M= zwHA`!&}|Fys`8K$8fx;XJfF?k9UgOX=eHeoVM zNves#1p?_RWyCf9j?5p0uk2puGmK6p8pY;!$=3T@vnsLg4xQ?KHa&p zU#UUbL7zyuF!q&5>F<~c*eehdic*_?>`%97yNs~k6}|zA1H$T=3+KQ{&%s%jl-R$V z%yby`1o*{r#FLqL@x8}MjPQ7u zpk$*%3}Wr73ny#rF6DR5Y%M5(BstG|5pCG`Y4WJ|OPS6WSspFK;{cqb2dl}aU3oWz zv@qGZtVR%v+SI8R`OTz=+JGZ|#%AL+JDjTTf^SVPoIkm|5G(5I3OAqlgHQZY^M47n z81Buyz@DDQqUWZub8>oGE7v|1yq2{}FkD6*ZYJPW8OSKG#QIA+Yiq0Q-P|m_oVRq-?ehV;tq0x`nb9S{slMmBEZgt$nJ& zB(s80H&JxcK|)pC#qFI`_0w@Gq)80=+Ae*`Wc+hfc}A9Cu07c9=F1%9s&Te~OrxcT z3iC*%B^^#p?n+KRI5qoXFZqY*9E<*RR&>pUKbtC@3v6sAk)H=swPstn>FreGR(4Ap z_3DeaF*gBTRD)nSxo%4dbvB8&;kXjJ=p0#|UQp{QAhE_eE-e5|j^FiM!R3HVrP(eb zg!R`_ITjRtxmC%R7q`>a6tIz^;%E&zVL7Mmv%qk~LS+MTgyTVV;JM!Hi&%+?mj3QH zfFoIN#F(qM14rgGT}X~iwV0QQ^e%cm1qR4W{(+WH;PNb_Kv9h$9L)|*7WV>S>uKiPy7e4L22+D6cY{{45 z@KETVZ#a1=Kp=d8|J|rM$zNUTpM&6E_y61AI@JIA4iMn)d<8UmSZ2oxpp-%P8T0CY z+kfZ%&ykFO=Y{@l@Wd+bQeCS`aw4vUA*?*_~&j!JNrmTNSvHr{cJM({5w*H+-{8u*oKh(MY1NP^D@84nnwXFXR`=6rZzr&WO|AGBK oNRV!Z diff --git a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip index e85cf7a0eee4ecaa5795aa26ed282d076853be31..e1d353c65b90a92292c0f41c4377d3d0f54fc76c 100644 GIT binary patch delta 24928 zcmb@r18^o`*RK0!V%v5mwr$(CZNIT?+jb_&#J25BJaH!0p6}<`-^SVhKIf@gPj}T* zweGI!)m`_sR@E+8c0PCm#wid6@Q+n1kEii3H~+Sw0Pp}V&L#|YMppI=)<*6|3=Yl~ z3~H*-08sGx3)6pun#yAEtM9Fm`Y?aj{a?zN zdAgc8+Z);bS9Jef^dI_S|6g@ojO-k3{|{aw+Fvm!K>XiYwyH)J^#3T_f2rB&xeL>O#6Q(+sg@nkWn%>6bEcj%UEV<~Y$7hZ zEcdVs728yY4x$dsY3oeZA`wH8giNot&xz;d`iYAM8f-=(z-Z=d34?$VFxpmT3%?g8vzAVBxgZu z652>flG`y*=EEFggruw7E2&mB{c6BhN~)|k*-gHh4C+sbgqcXB z*5bM(UnNhmCj~7OYc3hRdS4@8Pl7~uJjR?^`ieWne4GNVf!iP|$Ly}f3&n^r2J?{8 z2?>Klb6xtF0q0UFWr{}Rovd%Fbz(nQHzjMBcKrsZc+|l;E@I%$gh5TDrp;lxgg(*_ zl7;_nUT@7~!(XG=H)aTLn*VJ81$z<`uX6HZ&e0~KRbWuCp}TvOYqRDI3y&_r`)H_x$mO7xUoR*E0<=W(~2El!k~ zdGOp@^TXlf^Eq87kP|nBcd)YUNZ!2W^;Njo579ljvpz^X+HgJ79)woU*P5U zz?Fe8pfMthK6W0$`2<_-V(Ys@GaK%KMbzO#aABX(ja9{KESv__*|mFw=C5?HI8wB0 zABD1DH+90EHn&jb?6?|su;z9CO0;MeZL7cb><&EA-Sk7nYBAYuX#PoK!^p|#;mQJR z(PJ;Lr^($Ew9Z9rk*q~ndBk76l3T0URzZ{p8xu4t${2HUUwLBr#WjYyni~hCoUvRJ>T}p5psTeoax-}IW?f<*)FORKJ8BYe zrRsEbG5gg$*|V(QyoO2eb2WRZtWnT1RDhDr#LChDAxFk%t=Y?UD99df_KL_kZWJ{~ z_&yfARmF}+!d5#(Zu1E%af00k|2Ow~A5T)Eq}bwDl4l&flV;(b?J z^jTkRlG$nkenKmStTMCYly~7GTzZNG`{(;YXfR_M32y1?^X+gnC38}-Yu82Y8Wd_P zbC7DPgF*40xfB)}ss8h-{A@9B$0#(_AdLj}$5kf3v<=17y&IO1lET-yN$D*5YyM<+ zA`374?XO9*VPjN!*XVuALz2fMR)l*X*;o|j{E6gxNasrLyn)FS>ikJ9(kZSIJx19u zB5LF5gLLr6s}Iam0HnkkPLmnp^8QDzdxWfvwi_|=%;h;ZR@I9H3w1m7C= zk#bFs3b+_(t`S^T1XL98D3=1=T&P-oDhUwTPU%j@glza`prHm3{hn5%4JK6B@keu< z|80tfS&$Y>iEHeliproqI@Zq8`#x%;8*Du1n4!(z&ciB^{C5lc+Et6ZygQQinKwpY z`m-%_s!N;EzHU6I*u%L9uw6R@@z@qg`I+$4H!GM~oK+@CGAaWI`;gD^=r!9JW(_9U zLl&g%zo68)i2Fmv<)-66u2Xg|+8Brl<0^l90{!uUljp6l8-cPW0TZpd0i`Is)=!{P zV1M(J^Yh*X^zGRb)yi5ab_c#%j z_v9S{HZB+zttJMf<;YoG=cYd}bFGTFr5vKDo?_)@@BodNi51M7m2rl125G5)Eg0fb z97w|6Y<^6C6;$_kHYH-$aCaaH?;ygmtC3~tfNsQ2dM3-9O$@yx?7MQgs4PM!DrLDS zVh8Y&oMP&88pX24^^LW%U@IGG-k-bS^f~#IZTHtjlidS7FLwkvDM{y>@XQLc>>J{` z#ad^#nMLt%%P*%lMdnZUv&u(pTDPX{cdm{H53*xwq@L1z{}@Jg-~Q2zi7JteZC=cJ zd%kaM^{KK5rBobN&lyY@k#P1CR&#_yU>x>8YKcdUO7aAGZ%h&56FaxEc}1pv!+0JQ z2DgSIj7|rR>u(U)afRcCMD)q_SZe&fY{-9l*b@RGj%NUcsrc2H1f_mS16Ap!n#e(3 zom6GX@BJyRY7mvNJ1_XytK0LW#{4w?JS)sX#>{h{xEnjh`dLz!=|!kvZR3vawVNCd zb-v$+pG=l35F)ccS!9m6JD>aZ>5lyj52N&&q%#HDxt|b60`luz%oQgpnwe20!m8=* zUQ*ZF8T2+Exnm>0&e0JVI9arxaX3%vhAnV^+GYKo>c}ppzP$uTcxUo-bv2lWBy%6i zxcB=uzsz;;02CRGCqdyHs$cji znZX52zWt>jkZmE5>t^+KbMa@&_w{0wJ~x+-4an=abAi3cip}kPIfLbg%k~vO;k!#t zxck}d^>Z!9Ntv)MY9ns_w=rv1d8_dqxuyC=SgzTFx%y?QwR%T+RvF&;qp48w#D)o) zZV>y#(!kyIV8+%CLE-6zpyTvzWAdQ2fl(OnBwF4~RU+Od(ODn2t2q1&leJOrXHog3 zd~Jz8+4$12%0ae*Uc8L}axdWXxGBy&Tiw8I?%O1R z>W0|lcWOa=xkMe9asef2dWM}lGqfP_B~9B$F}Tk0HdB7(aZ z=hDN7D%aAC@sC`2y7_$ih`|*)=I&y;4{PExX!jk%(S(H4m)aB7E_E%adl_kD(J4t^ zkK2~fbeb4>Do78kOV?s;c{G*IIFu)Whj9+Lq^>{6P%V2)-errJ2&MdGt&wv?8qH!{ z1*?5zkq2RJHDR=V^WGZjnpv%P#r+yCr|Fo?3-i~J0w>;T`(A840EyyOI~O#eVt`y` zuNCXwz7i1e(WW^44rW%du%Y=hVS*aYlDxaR7freoa0S2>x)Syy+L~A^77=U)vem9h z_pc1lFSC4PP+zKKtsv`lYL0eh*dk`=^gt^RrRh|ms(L`5BY#`&v-?dO<&gC0;>vXk z`|_I>6n(8n>BiBFXmxQr;P%;QFPlzZY?uU_$SnRaJ8E*-DD%!alSG~qmLctk&^<~( z>B4pu8assgs8}3p7InL*wC@ZQ7;B0t3g=s1Ed6}ACGPj}RkBr9e?m%+TcI$&RF^21 zZ*PaL`k#oaCHs(q+a8hi7FQ=qFIS&q3pntbX>t{!(+57c-3x|=tZloKZ*4n&w4C-U z;t$aukDE$r11%c<3==Xr03$U1q`{?sk!+kT&|won8peV6zaz|Ky8p@lJ`Ksh2NRex zT5qXKRvxWd0$i*Glp4d^ulw@kBMp3iirOOanu%5o#Os=fc^uR$Eu3lBZnet#)Aot@ z52pK9+DQ6K8v|7ynJ-WPzy&4%p!eUy2Jqj|_TO;y|G|v^40ZoOS^rKu|3n*NMlN3e zNgRuRY2zR9PsGuxX%~MWj`9~cDo7{5)pIO>&0O2zx!IQRp5`?s0nO7h@@QvEYw)fs zKkzsEtN19^t>PLmK9ttz5NXn z@kdR~ohMA`n`>igFuMD~d9f>hjxG7BP~|2_l589%sS0=Om=>~T|p%C+n-mg>Vl{$mtV7 zgs$mexO5*?QaeY|t5W%7sBk45kFdJ}m*6SpQCTIPrvI<#?U1%SczT7sl{8O?Gd;XC(0X7gSz?BMLk} z!{;~&=J~kbKAZX=Y&sVtL&Fwju7fWtuR;dm05e{fOHo_29*t4!e3Tlb8d61SRTp z!8T=Nl1PxtmsvQ>y25jCjF#xSj@#Pn&-m#c%#_^~#Dp5>%C4V&uGk_GdWqD-^a(9;ysBABqjK)xrVf7 zgl(8l9#1ft+3+3piI#h0uwuQH+cWQ33{LE0KQ07U8f3kCW7%c9b?HZ)&6Wf8V7@XQ zyhbJ{wlfs*wlwC$#?40t)c=s9(J1cRo>+vTP!*C8Hv>wcp!x^ zAqWLor$e*6af(5Qu2tSmoye!Ac9(->(@55vh1Anvbymd>_`lDYO{izqjePiE*qS3FE(t94)0h$BS|gOE?I7ppRn z%oA=;&q$<+6ihqQHVYuX?Cy*swc_nYP#=X11hNo#~c4FqPaIDH`tsnIug0wDzE`-;G!8E#NgEQ|(*j!@)Q=73dd?O+xUh$?Ftk*0+11=dV<)R`p&%6OR= zT58%8@|uUHNL?;AQJNZ1U>sl6OLRBo@G=9%U+gkaUN{q@KHkZB&#OyfQr2N%Bg(zq z&B(W399TRPKRm(o8ZYZ@VFFDN&`peyOll;qv`%{@Q)V=VjqRwNWLHO zCDL)W;ID+w1BH|G)_xeG2uH%Ouz=yHtlku6?VC@AsU%iD~(#JI=uE4osqV z<6Y>Yq;GWcJg>dqsP@NmKGs8M?jIX_VHA-seI6|Xr$m$@tl z*mKL`$!WX8A9RPD+NxddG@*XBy6#O^U}7dAHi>!mcgs$0_Z{td8oP(4a$UU#{Ml>> zWz8;ZUbJQ#UJp$e{5||_aUO2!&)?2$r}4trf&O2t^(I{3g6+F`>t)|WRceySZzz?; z-J+JPY(kloA3>f+7r>n1dO_>{K-g; z0@X4>UM^|vG=Kh7^^QkjN`Y;RjDA1!s?Ow-0-x11Z6mlpSIJu`hLe35y2Z<3F~3oB z1LAp*w5Bzk;_)ER(Ec{*!Yd-&+}~+NIJb^dlQt7&myw$@_^`?*mI{HmwMx6V^C#$ArXA&M(oE z#mqllpgTFz#Ms0gd1vj;c=TlLDtWJ=vcP^RR@BMmPlkfK*_h(%GOH-nJm)<1ap7!k z5^!aXiPywI-VLt%KNS)B`N7^3uKbXYAh3BbWFkVqjv`akQ3Z{Bu@Ik3gmXk3vFNX9 zWU=xO8|P412NRK3{_}+=t{@lI!lG9dv$=!E>Nm1T|HM`g#NN55uLghJ9A6a)a^QZI zk1@921KP{N>m2Lz8td~Y2XWScS{t+yS^9KYjQd54G@ z$?wBv0P-MeM``rem%Nf1K}B#FycyNNAOn?;)#`hp72hUP8cgP>1}ok8Szy#34R0i# z^9FE*^jNhg7U>LUjlh3Zve#d0_mB7|dv~dA=d#I;@_A(tXkLcORch6}CWXV1t3t+ooJ(s(#ub@8X-b5W zG^ZSbP(oI1_xX1D&6kpVQ>tgX(n$=YYt|UEuLr>E=PsI;7^itjmd_xp>=Gx{yknct zAN7)+jF8FL2?Rqa!C~$=6zU6A@D#+5Z90paB7YD$#xbS9CN&n%0Z(-?Pf$cUzaEPk z2Vf1cmTaAT<|rNnunvJ9ID$w9W`yccW6yPkKmWVLH5)g}Up9tH+g`Na9r4kH^gaAWTy2wP$ zjMgIjD=e|&o9#ON5OY1EMsZecDPmBju?u*H!uUrn7jYZWsH|t+T2Wlwjb2<8czuyp zj-Wtp6cAp;`b_Id461lVq#s+Wlm*`n9QNGHbb{nQ(2~T#Y4n;2GamfTx?Z(RUd!msTcps=W^#4^bO>O$Vmp5;sh`~H$zi!9r)w_KBxEyKWmO1Ka`F02zt--Zt_8SEdD~;+Ab};y-O{ zmO6s+FFD+2^R{pz+^iH{BYe5A64|laBv89t7H#6kZp>NnV(73$F%#b0znsB>YHX?n zn1mQxkbmR96aA`ouMb~9s`CT2j&Up~VZqvoyJ*)yb!IztqqFoi>3`mE@J>sE*l_c< z1HNq#Z9lrb)t3H%0F zG7&z0yAE>+phgmrp#{r&?b^dqJEo}>XLyAM4Qq5H+uLw(5;b6i3S*Bvurm7O=HL*dV62MuOMoQtA;!<65) zVZOt>R~9P=*RCSdZex7SbmZF)eZwN<&Myc~yim#|tUs_q_lj<}0}lJ+KeWvmspgm-S$`pB7PVo=J@)$WvR+y8 zpSb(=@t*v+IcxI90jqd!4*cZ>0cs9*z<&shOU>ndLZrQvOkwzX>5ZN1`9^G+gOn6}$6w_9;GwWF|v20Z0lZ57{yN-9~Mn)Gu_|98qXqG>_1h?$%iqxVYwy7H>&{LPO(PNpy##`6e*R(WU zF`+T^WZi(3Szn+yS}CqHo0dUR1)@e}Lcj*e1(R{TA(e@LL^z(G?aE*k**H@oO%Ux4w&W%U3Lb z?1#@ciGv!`zY9f!?#^7zO{L&J_q}~<3#BEdtz$Vm^X!v*5xgQOIY)P2`v(oaA zCjv@2KPRw^@{htIYQcW08>B1KQYO`{*`slHQM6L~qmlZ!!x zO|)dV05Qz{&1uW#BouFV8x4odWrZsTWxE8h*FPG!=kY<$n5}mFpW*#qUE;R`fb3l> zn~7$z(Z3{vYNYhq!;QRaNFt$s10MY^$9w&{VHoAgT#P*ozE1g>7Q_slpMDsQ_%IpK zJqMPje%(a9Te4f2j%a2Ks#WdqO|ByBlAS*m04uhi&>l90lz{3H-Z?di5i2uv+u}vK zVMls^1w+2GWJI>N_!ZKAg1b2_)gnkZM8aNVe=^>B6nthOEr{9Yb=47VuVd{M|D;fx zv){Z#=^~~o`9!$e*2AWq8T`I-t{P`G*|7z~rhK2OqdJlydVl7k8Z!da7wni zHC#K3o1AD>`rF~C*#8ukuX{apEdRV<1vcR_O_~pw*X%>1S0t@3qLaZF`)3qHYV8Pd z!SllMfGq&NH*Xt7l_k!S%8)T4D4rDL9Q$ps3dxchoaONQl%3@#(o@8yv~-3DH=&xv zWpFOY3KFx8C2{4=SNf3Om|U;yS#!-nVOUr~9}1|WQIj+4{f@@1FuU<8Giryu0@5bg zqQA9{eQO?l_xhpuF<+d4ES@Xoi;o8X=SC_LgjQ;Ic-YCX*!d%aYrdurbHvw( zVv0Y8t=(Vd4TZ%G)78Qgv9kJ=x?yn%6Kz4EhPyvP+}E=>HmE)E6#U11I3jpeVU=1g z$$?4+?@`lGlSXC*4xr)27j0Fz0a%1ENbNfb%BG{3UG)pMV)g;usJVI)rs=L?c8Uat zHjYbBOxz_Tp@7v}A{LG&7NXy~a#n1)wSXeTH6_(9cfjJB;7^pCuh@cqbp?C7s0a0z zq5wNWqG(%pdY%1H%G3OEEUH|Vc;Q7GWHvnMTNtd~%AT$&6}%N3ZAC;z2T+91LJXDF zqBTY&qAq315{69y-p)eQrc_r7>w8fTQx!fgdJ>DW>fortt?+nE?UZ?#aOk#nKh*Y5 zndd@R*P^)RC3dQa7tJqKdmF**Ar06h_$2J>RUvi(?AM@QK0$2-@C?>>8AmRp^!V2R z+Y$JRY_#AUv@QPwP3MObe&A{n0c!7web0W>e(RUV9se!8ekQe;J(~@oK$RX0cfW{? zK+6K;vYIqggfTaW2{pyToHU^-=8LQEAA$2H`2#OrJDfALfq0)B%ohY-%J@B=zn2cD zk-VqhExxQzhaG=aVJjhjWkS=VjTjF5Bc?6y|!Pt~(ur%2wGN@d_m?Fg&v0Y`^8dWT% zx;O0kX>qZD~w zP3aSi5^Nhnt+96Mg_0xCFMt3Hvki zPYZPiPQ{zib1FMx-gH^;AF+o8(j5bT{(L`P4Oi@JQ8}@2DbvSN;g2gL7?|P$e3b1o zwrv`$SE@kk*)yv-r_WP!=d7k}CyQ4mw=KYG+){3BzL}J2%XS3udOjda&DtdW5nmY8 zb?`m^JUZ?D&zXbyU(iIP8Lu6I1^~dZ0RXB0BG0DJ(}CdsD`*1$Lgqi>pFlHIQ^#SG z6T_dd;45%W=5nCe6n1J7La*jGRcvD%{+fJOzcEt*Irp(TMr#S$Do@5&&o0-}veN=l z1Ks)7OD?R}?fy2yL2yRKyXC`T9a69-6~Dg!d$eX{tZQgmW<-s@dK~aH<57RT+g(0e z){@eFqH0=+S1S{p8auL9O**4C!efEIGLb4KVO{BPf}1fzj@YfNnUYuxE|(T*X&9j> zXB`953LOiDIG3d{E#W%~YPwGfm%x;w4)TCV1wmlh& z+yRGKpqNB-<3t4Pr!E#vj&!$ILeW0x+qU+&YsvR=0H)d)UM-%x`RBHZMcUE(hhojGh}(CicN6Yjg2>bSr%B6JlxXtk?49k|GUS!=`|*o? zZ|*M={$P(D&1m5IAN$U*y@#Mw^@FncD6k!|$#FV@(fsn}vsb5~0sFGwT;n}jw8NTP z@#(+M4vpCLqu#=gK8(@(V{9Vw6izr~ETgW9UbsWdYa&l+2!_txd9xF_(>VMCC}o<| zWHFxNC(qJ1TYuZz{$J>BA0Zid$z?} zWW7h9^J~hZxjI<2rdDNmbl}vxI6VB-7Xy#)@;Aqt8|avL*`HVd_}qol^@F(!xSzZ;&EY{BnYtN zPmNMb=!w8{zJ18Kjqoozv#4u7%jprcMv#ytmMRfzS%NIQj>-X>%R>saakW7&eF=%7 zc;R)~vVHAR>v|ZUi~B5Hvs;(37F6G#pO5kw^}V{o%&CtTKHb2H+Q`AmFMlZ!2%57v z^p9@_$tOO!)v6Z!(z+4G$mLXd#^8^twH`Na77M@<3baG3G|u@GOke^aus0gxYT{^Kcr;|E}Z_A#LfP-5UrCL^6>a4czP@(jga>T5DTwxT;Zmm^Y5taidjv~i9 zX$n-40oRK2jv~k)FzCGDT$jCos{Pd^45e$9=?G|dCJk(RUeX>4?0A3p<jjC?}N7p5+56dWT5;~Aod>LXhyh&@RyCT(L2?1qdQf6INc_3 zvM-s$6hjwg$l6PMXG?}_jti=Xp^1gI7PjaVP2QLlvfoeaX{{utw=zwa9N*4Bt^f^; zt5qd2HoY#vR>s9DdU)f_lxk?aC~d&K@KQzC=$_^R&9pl6G_RR3oilfm@2UBWxGRQ% zLI}dwnxaH0ts^}F0uXNL|(!u#2 zBi|0?R*}W-VgHzA<;j&&(LCiIa$UIo{EdHor8k!8=RHH9&`+sOPG5x-wi+NtN~C-s0_tw&OaVKL^} zEyJ07GS;vm931Sm4^t8!ksl3&CK*w2*ZbKJN@vtsR>`8?lK=PLM>|A#lFS??8Aa|5 z`lOMw<0;^0m*g*f!Xh-eb@fnCY_hiP`C9x-cnwGKqtiO_^N+xn?Tx`h>V5OXo~-sC z*QOr(5wzU1+VazDSi#wI(T6|E+U|+W1>kbfJiefds3T0$9EDkxl|iq-<2XB8$TXPj z3<5@M;)QKEu_c?+r+~BUQO`%a_aDN`w2pl)wCL1$3h-1z*U1FYvWR!5A+z;)#0;<2 zb|{&Bx#f0sd@SCFHZOW!4xsKFhCp(~B(JZJd2|WOcXz&(AXN1uu$rn6=WG-qJ&$0G zg(i7K2xL4g)hz2CrE&}4Y6j1E9x6ixPOWb`~ z>Fi}C<2@j{3Zmeg8u0y}a{_zF&AD40GDzQmV$wa=4o8_I$Os0&;VFy9<~-$W=b2Dl zt1LLAN|{dbzP?k+Edqh*4*zFE2?NLzGoFdJ8@0en);sn5unNk6KfT034mwR7V6KcZ zl!J;EGB~f0bc$X*1TT$VOEX9gMQ&#&OhDvNq4;2VqADL!LmCt<$v>h;bEI$RAY2%j z%Oy{svY`{dSFqB7E6U|HFa|zhoDXFJy)CE*I_9%ymIG8poZcGXkRaKJPmQ%c`6RJR!0;G5qgJaz3u6La~n{Rxb#&V#D5nRE$s@X=JvPa%=I2msi;#eejW zgY)&dv#|`~~Oa;#Rx#Pl2_D2QPIXO@4 zl`*}DyWcF)-*$~u^i-?sKjV5qmOsnllffE9(I?M+L~`eMB)M5VC^q=^!r=Yrd~gO7 zDaP3f_@F)?+JTUy$g=Kup+VQA+~Ix%`b>|h{Ws@!TwB6m9I#1iR`}QtAN#Y<7QUj7 z;d&^2-#=J@W#KcepiE96oU^LOV@LUiD;tE^)s+AqgYD*m@)zjz&du-saAgjLh!4L_qKWcC|0nFy}d#YJ%d zOSH$#Df>xI$AkVMNa~|Bf~CtOJGYX6z8t68R0gg+Q6WmDg4 zfO{;h=tEgbzzXAnEyZB(^+TBU_&-s9KOblsku?Ic8JaCI$;h~VdNRZ{aj_P+RGZZV zr(ccZH`d;}*giEJM#Z-`qdTP@(Yz3d~E`N(DagSf~jt3yv=>gtzxVo`Ml-Db` zvU+RSS4`Iu&jS=3x&DOhq`tX4d!{jedzX%X7_anpW!I-0fM@;Fu!vL_x_|hZf%uGT zmmc2j2wfLj?sa@YINvA+De2i#n%-~`9e(xTKcC6_41zQ*;UkopJf8zbe_28J?`C@< z{EqQKMVndn^t8zkLk3++KmxhwW64l_H>2xEdH3hIWiGi``27cD7c5Z4T?L^0=Pcd8 z_hmTjE#4PeiA1{-7W+&y%$u~I1Gp`A(Zw``V=z6eqf5PN$ziB$^RyZQdDG!e%BIp- z-{(4nQ6yZjG@^!kBekzDJ#7(~(K}5Pf2|~V%a5Tn2I@imtr5eig<+67BH(kSv{mm} z?dwn4=mVmJbeIA!L2&PiK<+c|-u(QDbN80$gon@Zy(7Fc&5xbU2dufSO<>|ygkYHX z5L&nIF(i`H`k_<333`+aa`9p~8rGf~H{X($C_*Zx`)e<}9c@7Q+w?3SrF86$oYB_!Y915LzyUXX{j?5bKEr&8}ae zd5`#=){~%5?0V{3*%28C-A}iDFS8a(Lw6F?f5?nZnm(0MGDByNhHZ7PwDfKQzT4sn z&C#ZXtkra`lNMC}K&0nXhmvm`)FUf&*zo^^TE;lOyL|vRwdpA<6WEHPDwEGqJu;ok zNG+s}E5KniET9Jt4=RFLefcfq=9-o%H>T7kt#Lafc~*5cL!4*#upv(9>FjlVvR@f5 zZxp!2l~Vd_;Xn3g1U_Q$NnU~j>+9@15?1Pn8;uHY4XU-!7kb0{+{jLfN=RgyVj{Jz zYwHwc!^td!+w@8MVx|iQM$bTEF-)OyVbCNzJ>Z)xxFba=+E9KntH6It;#3YO^0;D* zRfdqR_>?xci9^pKG9|ys4dE++tm3J> zj9-er6Ua4`#DNdH7iby+83zIlwMjPf%s?jxpP{1ZU6XvAD@x}oMP6v2ZCyOCM!_9< z_;pe{V!*JNB9b5tsc6szpKv6g<#OYga>DJP2)8qB{w(!9NAr0X>soWA4wP&CY3Dr` z2(3~RTCS2<_J>gG3aZi*dx)_p7QmEys&rIU;yKuqCW1uk6mi%BMxr(Za9#AixBY0s zL5?Zh0AvqqY-aq1e+o76rhpp_?Xt(7w~im2#lMntI`Zkm2b1T9tOJOMhWoZ3TQZXK z3I%aEb>awrMtUT;9@GC0wMv=pzbn-acq#mqD#h$8*lIaJ0xwLQ}J>x*o_kKEt{|^AQ%lAkr1kmY`5u;3h})F zh$Bl1;uKz=Q4H;rNEqL2sBU%j#Y$)MWvo`HDgU&Q@D%>`6%X!cES3Py!g0Qi_;Cw$ zMV`bJa&sBc&2L&3)&h)?vQ+fkWo{+3=t^4V2d`5(C>P*)Gzs4V0c+Cfq|c_@1PFC+ zh*NoGXw;-VKrnm|C8}#aThLb4Q(?4t(*l&7xis>e*p!?LCY@WlvMeotR}#1{MUZ*- z(p9Kjzos>j5&&rmm%W0Bp>PUJ8_za~8qLr=C|DukR2SzA zdmh?w+q}o!OwmFPJzQ4PBmkphWqboEn>p4xFLLV`SkRz6p9NW&#EKA?BnM571G+T8 zS${BZ(g0KzPoX_Q-bFXpti)+0giav35|XeGwYc1yFJKMV(M%6)?x!qbw+PS%SClvJ z!m1p$c8O@jvlUEHqz(Kis7YW3NFk=xq?~hIl!{S`!!_OEt3r^-`Md6gxv)c-NooPE z?9jA;GKi9cX~y8N4Ko+8vJ{f|#Eq(1L#u7nE(prVVa04|<0Pu%D39`hw2SyCc@)I; zZ+Y1$dhFsRw1c1;%?gP^5s|!Ef}+UWv7)mW zX=Qz$Rb>}dY_whr2C?21)J^C8MDEH`-}fGXmTqFafXVszxku$k*)uC1Sf27)Z9+HZ zjBO_9sbL>?aiv8{7Di%dKd4`_np+JalLCnhj$f{i_nRoP)fi=Ju#0t}R_R98t^Sew<&3*CB=TNk{`zr1 znIjTg5~J@WF0{BP5%IdhTpWSQ=mT06O?89{<;hUI?dr}}KDkON*pg-xmwSJX23jl{ zV+%_)jq0rlK@>I6kWatF}ikUdwvn8t1bq7^exD*{MNLB7Qjbif4e zikzz7*9NA3nnV;XWw{~51>=3JF7LbobN+52)(#&AmE9j))!iS^%R8S_OZwH%G*)jD zY`}cXVRoH!9B|_N27j!mGkKx1j-%U&P~2Ew3Vpo%wWHxs?3T=20WBb0K~~(fA}I9; zS$TSA%>^DOzCHc25L0`C44%yajk!3VSvAFB8C+i>9{Z$Q3PBI=YQeA(lfhde$TKj)joN0FGFxYQk^f(JK~`O^gl8^)~tzgPB3LTX|&|k0*e)s9BuJr zmf_}=QXsyp#3T6H%PyyjcKrfgv<6C^0N) zdg{k8%xYEBB4Xb1I`Kmy?_*JibpP<0<&~#g3zO|H796l>suXyOPNrhTcpZWT-M+ZV zG6q@D_b>m37BezD0065<0D$CwksZ@#=|S-R6&_pvg5y8ppWv}n!^ojn66J$`;XPcrBsqsG z&eU1^tYsY=zn@dwCK#M--IBFwW3J5ya&f23iM}iA%U=zCWRKG&aVE<4w8~qpzN)j+ z<#zvr=NVA9#ZS|yZG%;E;^^k(2ECt#Sv(XP(hP(@!|KM?z*R~U#vqc`bQ#8+ZBVY6 z<83yq=9ah+a@(kUQsIKXbxO|>M#%!(97qcKb<5&(_Z@Qzh1)jg*yX`LADY$rqk<*#I=OKZh+1g=G# z4_jGq=!H*E1a)ZWNm#_{eP|^Ey&^Xut?i{fDVLvE@k?91N^o^+%?w4wlJ{#yMUKX_ zDaDIEKf;_kS{AA6?xZaP+PWch%Soh`9tb366W5mzaa$*V*@>87n{?5RBPn6)9X{zXEhtOMu)5X|Vpn0!fVPs$LTLm(g z&>hr1RL&l<|3h#g_>M#KShNWgDd>rU1?ujgn}8#whls$BP}~;y0WgZsB?Tl-=vd$) z#LScNw;b<8o)L|ck^JZJVzWMRe-xxgs+hjOOjPn79}XvyjBT-I#z)2&Vqr*s#;Cbu zj)$Nj@!1~`JA6HYyzUgJ&K5U0Jf2V4@A0i7uxdF@Lup$`UUBB6@2 zxDsdWxN_7yHLIW{t|uExX|DmfYT;AYnvIarDHrNc^th*jhl3%E-|gr2n}<%CzX?qa zinSR-D0M54 zu49;Q{IZa0o#bTQ&Bkj(0`fp0t2*e&eZZw>Uz_jAj$FRa)dE)!vsXV|KJ&AXQFo?< z%D(`$oYvyJMAuebhg@Z|9%yim^nGUHL_Ghvtt&f{ZdPo5^6=Vr^g=p%%RR+-7Bxq6 zl`>?Sdu1YqNzx@}6v>8GCkD94toMs|eGv4hVNCke$M5Tkp@!2xfrqyXR-?m4U3Y$y zlIj_?7ICH?k=r}=NNbYUM-QAbBQ2GB(70qREML834ai0I?qVYrB;HH@HF>gv;8s|{ zZeProq?VY8myl3ga%|Acv2}VcxpmCSQCBl)%{&q)+SS=C?!)vm`T*$Vc`b%|GLJhm zXviOxQ@J`Hl@s2fkln5I))Q}3J%oTb#0^Jv#*>I#mRBvh1N&(j^<0^@ncGmOum*h5 zCQYgBk2^wVkmG)Ry<7oouam{fZxfdDm^cpH{mka(*1z~~rJQwC)!Vkm_ogHTk={tD zbO=(?D7}%CmXHReL7E@k-QA5e(kas2U7H4xO$Z1G5AQkmT+cb)`=0lWJ@&tAe%G3F z{pJ{R&G}LGj1=MVpidFRf9h>k!v{%jp#?@d{MsLfW(!d*uExq>8Ziq7L;&ncWy@-l7d3c0d_b=YS;VeB3{LEXz`j`vbi4*VQaHQXgSfxf--eAxMB zw?rfB9~6#SD4{U~f2)0XGBog}W-R5c0rNyFyz)G2%oLcIx5O1Y+B(_TxziDJB=cl` zLr0NuLSGf`V(D#t>@>l#92Jn zEyhx~BfNmPu0cAn1YKFCtT^hf#;b$!4Q28RFDM4{bFzp{L8dd67Nfa3<4Q&d+W?A@ zc0bv9pvtPs+m~FOiYIndCwJ}+&n&TB3Jgw|B%3C!Ao0zr=R5)Hz|}=cZ^uQ)G&+Z0 z%@dNFjEP%?TwfeRzSfscLZHQQCpvDGVdnPDJskk{;8!kQ*BK!T>jf>c#ZTR~=hq(w z&mxuTt^naMvKI0Msx3YZt9zsz%K?Z@RyMHu$z|9Q0RC4V^uNi2ADV3SmT*C!2@w!T z;g`}Ng1$kV6dsuRw>;Q#lLNp1|B?sy_+DGAibXCVt_dEK)Iv=9Gx$>v%yEr2WMbD@ z@2KHEa3n)T@S@UCFrrmhX|JekxOOcnOw#8fuZZedTwIwX`iMKDq^YF2Z@RhlbxON@ z^BuVvxo)|7-n&)1nVy3lR*1#TJ$$lX`NW>9)_3x`L*pGus?6{J?_$S(LhLvA4X=7# zgP8ON)vELtXG2uxbxl6b!#&I#lR-jN5}J1cEjwOrw0$q8=Dik*5$Utt>?8%bY*0(C zp%GfL2_9BpVJmWV@W)XtqwQwD9=vUc4oinZzRqKzW}y-{4{2AXNK66pu@&(&CX9R^ z_1dUdx?Qp#0{8qwUb8MZ&c8gSDx7X zWK?)?8@vw26^{qlWvI&@XUqlJcC}*!$jS5(56|>#LOH3$pN_uohv8IjAZ?0MJe~b& z^f~7YNjO#>M}${@EC_h2O<~_--|=ejl~05JXYO+9Fu`Dj^p^@=n-xshFPo$$&2h?* zRoJ}ugkk1B4McR33d0yHC~qJg-wUc5kyac!l?UsW&n$R!27{mHuAUBdEPLKNHiUm` z)g@2W(t_kUhLb-fX`#6 zu0ily_SDJ@o4tPUfs1q!z3rfmFKX$nu`7I+y%gSOMpr#l`f-CU-nA@Uij#U#i?*MS zEqt5klIEppR%bxnwV?6)Mz&9UQc$M6M&^A}R)`^uek^a^PC#y3@qUqCTCMgS0lU`)5=Xv z@hm-{N=sW&C>L%5$IMQ*=$nIDxZaX7WsV04*`3{U@SzB-ep`i{w07bnF)!Nstr7wE z)W0=|((p4i_SsBB!33KldX1OXX~(L%Dnz`-C^51tT(w^0-=z;P9;0~S8f8w7h68&@NoYJg7TgFie%?#kFn2pORvz?Q3b_^ z)-A@*7!4a}uY$=G-pH{K&RC;0b7@;W5Nv(&$O7=Vc5Q#!+`gakS?w#crWZj$z6#|f&oT~nb=xWc)iR&Jw81+1Bq~kg2{8zZ4 z9AZ>>&Zix9^n=$fkDGjI*+2$1=Yn#N+}dRlt4F`g)}7_Y5@X~bQu-Y*oc2U?pZ0Pr z%T6@8S275s#q>o>)~?Vm?;I>0EjYxRnL3z4=ol(`X~MJCs+HPQxrg_$U`teEe7=DA zF+L$JT^z^)4oR#qQft5*(X$>Scyuq{wL=LG~+?|Yeg1oh#EYy zDLwa35H~eqJHu;q&g(V2E+}Yw_4fg!CHdhGxQ5RQs*ywtvJLJpV>aq}9JF-V^M(49 zaw|pbG2;pZeKn;4EV~kZMD2CtcdJGJ!v!hdG|#;Jp!A>#Dy6-$wZE zRyd&$8JNH8@{s<+XfxMGjKj7($nR)-%$GATN;421XB*5G?IDxSznThHT#wI7V3aZl z%%o*}QOq56`Ph3(CgI5j9rDF(;9K^R3YwVs;RLglN>C=XEGK}Hgw>~sd-B=zT*>dkA z9M`O$=7q*X&o&m0zJyVO0+G`3TukWi;o%m}ke?E|KWcTud2uz3`|=0}gWW$5 z7l}})6X!7fV0$VNp-l8f^E$K{l}{qBiSl903yht}%0idE`?Y%yA!+-pa3Ht+{y zvNeNSO@o|jiyYyTtgI?b6`cn}@n~815UmW%arnw80tFOPa*Kx3DAoYs*vytrbx4zA zd1pzb*_KTVUY{Iy5u*(>HqYzp@msi+K)f!!D%rKt5b?qZb=3CR>Ygzl$$0I#5M2m) zLxUK!j3QmG>v7lv2>ia3pF#ded`1^1r_`o=UdQQ{c!Xj=17UwntC9g6mLrNEHK71j z`vi%a3WUC~=7@e3dk)Cg;s;fv(n6CogNZ|&=a@(Z=y+dj-jKp%SYyV<(=3=ip)Gbp zY{2pfX&SZJ?QEvk)CE%ME_RR??Iv<7+KBAu;E=pC$g*^V%4oim7BzPILdTFEmvr~8 zF^_Z>{xn$(o(9p`;shJTEO;l0>8kTwtZl|bWUw*oae+PY5#R$aFn)s1!pp(P5!E16 zuS+V-DEdkTIWL>hI20~}rH;$8@p25eSfdAac$6ct8G)NQSvKQec1sDXWD75Pw{gXs zq*n$*imA9*1VIVbX9#ny#;jCvzg0Zz z;j{+qqb1en0>FKYFJy-ilp|pfrrc~2(JjlQ28nu0<+gqzb0(MOvJ@CjQ2a#3 zYgSR;tG1o%)7+#dW_&XenlPKEA}6jiPK1t-e7+`w&4p&PaAQcpXh7DR_pxMZXKo-9 zFGg(pWi`zH6yg=@@?`^v+Md7#(&+5`v%NZp$zn--8~~P!`x(phbk=uls~dY{{0nlt zhWKV_$(r05Sg?q8=zX06?CH;trlZt_p(#8uP#@)Cg0up z#_+I(uDI7v+}E7ipjXdM9lLs2?`<+6zXp^EgTm2 zwfC)7qq$Nuh8L}R5k^=pB3fX3M#=|nctbrrL55l5{VuaD{uSXR>N?9=4J2ienhWQysKY)I)6_U22OO%Mzhm@e9AKzg@Oy)x$G1LPb@by&RKeEcl^?JoYmza%iFC(wk8UqP9 zCVn8UQ7@&iN3^eEpiyxN3Xt@oCbZD58lsqcKObJNpIM{*NV!1Xq>3py!kQ6-*}19F zmy>>!7|8>jg|4ci{v>}ExqVGdxzM~2_x|Or+>^)EFh#C(R*r6F=<{wqVzG3Y1>q=Q z*uW9lIk}H)Cp4JIERLG})ng(t4K|O^{#9KH@28q2$Rb90Rrc+P4FeKKXbI+7;K^j*&zkI4n7cKHMq<2#gC-F=;Op(Y&mA*l(%A+1QBM(q|NcbAu~F zsPnuBCK%vWd}J(rPgOaMEln-Tqtsc%%7^3ReNe3kCGnOC)lRlE`n<>ddT|o8i0rz9 zEW#l6Jn;=hUh0q5P79KqRwoFgcd$t4=RCPVJ)}CbS&4`{RWQ|yA%(_!BW;p^jEb@x zYocpc%*5yl_+#mWu%2XypcogwtoJxq%<}FK&xGx$dzp=z((dxPlv$`OqW0tLxmkFV zXRbhHJNuN7R)XI;Ijcvfdkssr>r%esb-jCy|0v#JEzs3S)6v)txH)iN=K1=4o4(Gc zJqPY{oB@PKiI=0-CGqx$aR6X4d|^L+ks`kj)r+F1A`hXE<>^4|Sh+sl-b%cD=yxnm zCO0y`S|Y+(U9vMqTj|Q0XU^58cb`IjEaU-(g0*W1Kl8#-Qda?K8xICT^7iL|Yc9Qvn!ajg zhTF(!JMS#B89{t;ty%Oi`JGp3QrXqTUmU91HgYdjQz%J%6-z41?fh{+#b;jZ<)QKR zw$PMml6cwoJ|{6RiC*ai+BFzYvt1nDDP+G;^>!C33S=P(lJRMSkzr8Y&=x{S`-BwJ zSnDK1%J|-Yyeq+mZ2@MkTy6kbxmr>jd=F&W!)c?~hPDo>%ku6pMQM6Ft4vJEQX;ZJeD z&EIFAR}XZLlp*1U?V@1DGIk`$>ox}nytirsjMTM}O{uZ=X6I-7f`y-wYTKz+emSzhf@n>4@ zhA=q%piaJLG7Odn95Q>jcmt0|Os2u)LFVnW-7aNa1F&0kot>fstVCK>`ep7#9H37F z`aL)=QF)l=*1^z**mTiCnGSI5hfe~D&j_882oat0hpXREW+yoFe9iAk$M@%d6x7oAa2G%3KjjYchkWjO7@=gm;tVpRvbqnJ_8~$kiAomQd=( zK^N>L4PH9i9oKJRUy_~UOvxcMvj(^heriePT+ojk2omAI4$sB0U~qK~&SCiE>7kVy zG#hibG3svb$*qMx*0POp4z1w~uZn#SJ~eiG_Q*6S;Nc{%(Bz~zvC(eP#=G6DmFYU| zM__x{V+{S+%U11Nh)_S8HdrYkAW+BenLk?S;CUDT3qmfAV{V)8P zEa&$=)(;LZWq&BNI@`j3`6*5>B|q`r=sAVfhPp@1E9;hnL`Jf_v*y~n0-EloOf9cf z#QKm<-lvv~TJ?zToWz)jLhDN{9;BBJO|N7avrzH{8#YIK`wtEOM78~AEkD6h|AR{X zThM}ULbuApY3F};1@MC}{&2w?~}T>egS`SIp!gTOzz`G1e+fAUstxGw)Gj{iz^xhe4fhIRSzU^;)% z=I@M`8@oT@yZq>nzYqESXqFNC7q_Zt@|~gn!uSS;ZydMC1je~}b+{4nli9x?Mf|-- z{6`UX#J?)ywWE!#g}H%C|U_`u&6PKg@j1q5uE@ delta 16687 zcmaL9W0WS%wy0aSZJS-TZFW_cZQK1;b=kIU+qP}nwoZR*k2Ch#YoB`)V@A&W@kHdG z8B>{_4PLelR)cXui3S7&1O+6cE}^1kTjDDV4+J#B2n0m;SL@_x#Ast+VasS~;A)VJ z1Pn|FxYV?9!r?;sy3(6XFRK)ES$G!&O*-FG!UJHLUrZ% z{%k8uHQ4HL&9HMzK!qrMKgE>Vq_>F5!tY@*A3vl(s&D-hrV=P#sTABB_ih%?F3YKf z=Uaqxr%&9cC`l*^X%>YRIolBnPW$yOxnO^)~^srs9GzDfPmnc@)#{m16YhXbd|Rw zz*^Vg*%0dP28cS++oZICkz^#ob0};jR~&G_#{3v@$WAVkXL&vasgR6gRoDi)^0+4h zriNF*l4To=H`)YSI_<2F<`vx4}y)G5uXimv-%1X0B|WXO$Op0kD3I(r1a*MCFu+|176Pb4!g*< z8Qgw*R`=y|XP%n^wxMkWI`{4L48$r5U$p$C!vh=$vzfMzAiy&N4~DY+D$yr?;_cCIXon=Cb$3bHU4t-|t?idwy_;LuaA-v1prZZl=w(d$GMHXTHt6^U|^k z7{r3~z4MoxJ7B9&EpDx^`>B8f&_d?5=083P1<*9VGLN(GzK(3iXb|sY@;I_)ZJ9}m zbBndeafo@Wv+^CeUj@dAc(^Wi;OGsIZ(fvWVPcYK8hT*ulYoRY6jA{y9-PPuBU0@{ zOa@8LvwGps2_o4xGl+g)5Blv2zXDGd+tOK}oOOz*=~F>d4GLgOBVk z8M_6%FVu3YhNtU4y{sg282I!1HE_{9>sMra{Mo7Pl5e|)d_l9N+9g95_K}ppc&W$L zk@j@po>*!rV5D-Dw5zno1-vS2&VK1qo*%J2e;RWWx@voQrU91`V8s}%fN0qL$Yf{m zYuNZGxDR^&sC<0O7!D0Gs1tGXi_>=aX3fXsE>JhqPb&Ati3Sw^Ze4)GvgU{bK?_Lb zrcUkM(feaA3b^VJ#_2~9dN$>#HK{uANMp7i8E>dBaAAFEN~NsVi%4iSiB+-QOkX@E zhkqeXcn+`zrXnrzqCHRzjdu6TDtmp zObxfIqC3dzJr2?v!yZB5LS$R32BKhC^^AO;)SP$7t=el51ksNo*Cog5e`8jdRw%i& z#~3*HOxtp6gavF}3aA`Fz4nt-mr179jYrfq4Y6lTbDxYkcSPCG>13rZFfv?u;f+Ki-hQgxAj1R zN-lnEP5c7`5YMGyC7TBtE+~6%j&q1>t}%BRA*M?kBa=30fGoXiHkfEdpi2R(jrpZ) z6MnxXhA(%*1m5vP8KoWLi>U^osTMbBO}QgW?(CpY7=Z@JstvqhXP1jQy7%a`o#*e# z)|eUSe)l9-Yf|2eLBKYpo8mH_>^(Uf61TcmGibX5Jo-}pFlmDf8Sl%>1n1c7hr&@T zV|B;Mnr%5l#`tn9l!mjEAtZia45p*TqU5Wfwau5C%W z^5M(^AS4{B%xP9U0qc?Ua^CwyeR(!Jrm!Hp#=_L$Y+CW*eUBx>9-WM>4?z(XiE*U; z5loNpnvvC7DBbM{K{b~v4ItE_2aSz5oWwE{i`09 zYm<>6E)1cMfeCYAqVe#-DL04AXnG|hNykiA1g4d&_d<;o25)nNIZw->{6oouk)|3n zz;@oI+SZ-=E|__djTr_H)?Vd2OCJ zGj~P1>A7UwxP>};-VeN!)=J}HR@FOGK%L9u`XPLv;}K#)3t5)L2%glmSI!8K1~ZCE zuLG@nVZQ}=CmBeUnx9>q8^vw$dE$@EIGR@&&g}bca`JPW1-ar*@4!w0DMOpF`5{dB z)pnj#hee6$02&Ke*hYM)Z1BX;dv@~N*;a3Fr>90Zbd0a;=@$*I5V(w%5Tw;x9r*rAJNl5GA8XvO$xo2i^|;+92wSxyc2EwY z2VSQ^zUm!SSA`rdlF(mN_-#O!GlXh9PQaF$pC> zPQY7B`NAo}i2Hp(TycXdxspj9tlL1Ih+G=%ZX8!e)07qR*=!$tA-_}XZ)dxulax*% zI=&+(p6H->BZ`j|izq+>Fpk{pEYZHKkwJ@QO24d+x+xTw?OJ%(R$nnA+(d+&q1c@6 zXn^j_svUfh%5bpe;+h1?>yvz28oJ7sn0h?|{3$zZVKU#FHP);8qzP8ESm0S|2&?cm+^8cCW}mh>V`Yz81M510S<9}}Tw5?l;p z?n^aC`4eN2K13-!@4VdU!w2S((AQt@s@CO3T7u7)qadYO{3vy|qs+}H+o%n%qjSx) z8WSKTq#(oVpy@E6Z*Tp7`n6XSKx9nkTWA}sr(L&UIo9o`P8mLU#IoJd8Ete(IB*lA z2`xrrrc(VDx8edB{ zXvG#}KN80?ny-_bwMkA7YNfskz#d+WgtC);yWHtcZuG#$PF7Q#*3yxIve6sig{L2kc`-L=D}4pjc!;op9Uy4 zQ5v*Mktt@2Ep6q9p?$fq+>6J!Bk;JFY0))-3Ru!qRpGZhZkj3go- zKINalC}EYRg?RCDYaw@Uej`o#xLyp5EEJ4Gl~*uKLXE?3{xnAr0ZZIDlGW?>%Ft#E znrlC7NP3jzr)sZfNek|%NSM740sIO@Z-_f}iRLc)$dlcsPu7M+l44LC8gB*9?ZR8C zCjSA57=EL&Vc^5GEl9INEOVyX$l@RTI6|Unh`X=HUbgc%#Tsrx?wbMwy7%1q@wC2u znmZ4!WBS9zA-Rr*JA)%@Q%M|I@|M2zCExYwH$A4xhR{{#TS$Q!y_6^EDa|}?b{l~2 zg;7xk6zsp;G8;u}m1dXe)q@f;fP1X+gRwbpDm);b1$KU_&VD);6%c-Lp9oHXO%gc zo3eiUOmiL55}%4eUEH-UyD=PwXK!`;XY1#M=PN1jK>7oWp$HiTzTnjyNh?%Hf=5cs zEJC9!t_xRbtrDk%G*+ZYIV9YfN!*z6dzU1kJz+=XQz*GWiiHj&s0wOFayaBmv=0@C zh=elfkV-cZ#56ixAPvZv3y3NfLs6(ul`|8(Qh9bIkJU`(`g$(Oz{8Fu@C@bm(Hg>lIIc9l2cHjaTT(oNpB8CPi1q$7kEfd=>f@VQ7cEt7cON@4~(mW zc^k%KSK=`=DBaZf44Rs<#`4-H(}C>zCP$MZ)Y6*QZdT;;u|)_by!g6J)_x1*OM3R05wxv^)@BD6k zK~c!32HQv9f+T&xskXyJv=R(Ebnp5dFbE%U9DP&m&W|tjaQ$x5pxJ9Uy0_QMge4kEk8FrQT$}KwC0x^r(;kS6{&Q9t=XZPWo zMK`LJLU%=&e0#jkw!vUJ4~1Dx+F>KWQh`rzJbyV5;WZVt9@(vi2bL5JzKr2)g_NdF7zZixB;s;IHUs`7vKl)I%yKqFkLF50Era7?h0HK-TH0>I>765mj*fYhgo*{r;33y%Fx$0IXpI9AfMU;>_mzkcnu+q{)B(hN!r%KMmFR*l0+@}P6F($q@1sipmd38ZKwxS0>5*kU* z)8)MDP?ws5{N!(kBBD<_-h)9B>k1}*pin7>molC$@O1~fqzexj>SInjR#Dco=Gs^4 z*kr`mwH#ElsfCtx4(fs?v{`GgBbTeGRdUT-|M;a3ycFC~``)LgDtU`s3i1f;=uwgc zkxZI%eRGD_iD(v`rBen7Ql|%`w3#a>{`|8BXUpJ`dC|}m=RX~5HNy1=Ef2IQu&i8o z(UxTc?&6AB2G>y>i=>B}TKNo*Q?(%UsHk$o9=X3RIrU)#RY$h5fdI+d6z@lS-QW8%Yw1$!k~G-$mgU z7Z!U4>zTWOG1Yo17PWRgtZGp~BS%n!(d5Rd^Md_*EYPMN?Vuf&bwIZOf)Gh}`b^2J#c zM|z=d7zeNO5-q{Dg&3sH7|KC|c_gX+4SB1(V#geLpXcnnPI(p2p#!Py6!>70lqpw8 z#rT|gz;&TocaD+uqygYa-#EoI6*bo_loQ#O$)10an%f@D68Ho-GKzJilx`@y5nn)r zP9e+j;^i4z_?PL|w-6*Rc4a8}ExbYoFY%6B+M?^vCL1rC^=N3%?I520v<}jV3V$+v zef3mi+qADsp-<}G9eW}Df|HYNrpE=&^97aDYV}#l9aiOjf+bVd{rGY}p+ZHIs@;&>*5i=q&hBD|7Vx~!*=BC` z;yXFPa^=Y4cZPt{0xc^qPdZ@)ye_}HeRetiR>RJa3eS>XTAsvOPTleh`)UGAaT>OG^$psg)k1Hz$9+m>b-Uc@elhpZhYTmpqvK)x%J;H5)m7QcBt+C`wbYW~4JhSV& zfj+4dNjqM53q3LG!ciy*^lv-*)}+ zeeg4qFR8Bk)xnGA%nwqL9>HS1I7Dw~6F%U9@h9d+f2h931$_y@s%BM7{I(`&WEn^| z*Lm}ZaGA>)7NSI2>{K$NF6e{42%R&GzEB67s2zxowGUIx4*tvYe1{RpFq+WNUev~!gI-csq6O>&h(*Ufcy96Fy_SN8O0D=qU+$nc>W z*6M2f?lEyu*%r48OVGGcxxe8U93OJSIWUR8Swa`>kCRpZxM;$^fx3X3Eo6+KgJKS< z$IuBoZnk3J^hIzB1&I58-!Tst3Rr`UvDj02VB-816gR6I#Ts89JjX-hg$0yIY>`U96Vc1Gz;AeQBw1HH@ZoDx z?qDARU55)=|J}uP$Cf}Kj&MDH-Sd&sYec|+Kt5U(LVGvFPwDTF(_AV~S#6*xmP9c? z7`)?+Kg1EOZ#ImvX1AGAQ<{Bgd*+_Raa@hE@gCLu+rd-Cf!`K==jkk(b>ObY1&A@z)39 zfA0Vt|Ly>XI;B#*{s!jr(3pULe*EVSFu5BWm=th{Yh|}L^z<(MPALAq3!zyZ@2l2q z({M&UAf*T`Dwu}*bJ4N}GG*vVaT+U$|LdLaIKsvn^=@U8@v^cJekIH4s6$T#vD@=A z%xAF8vQ_H9+u&EfUEPw>c?f(m34gZps^|^W@=Tjmtf;b8sUZcMR*0x9hjrG?N=T1E zy(j>x$aF&Q#+U)m6m9=Dh+ZrYSWpf=xi;cBf`#yjG@xdU4k%HLrT!=s$k*4QGLeyM z$Jp;~UUDD^cf*N7Bv+%K4$O;XifD_~B46+(=Eqpd?)wLumQ1~3sX^!rMJ)fGgMKVw z5%64GH-ZUSy$(`FKrS>fkwT+=h-t7GO$>nL18rcBIWtI^5f4LBR6q`%cj09QlSvp@ zAehELGa!k#ha|Ac+pe_0rDYxk+?qMo!n;nlq+~3%!Fh6yET|H^7aT@ZlINSm+g}06 zxTsIDGFDD+&`ACtWklwC*W5q6j;BDx8=v$}xcL z3Z+04w+41pq)8RpM(22~(OBElnwK|w!&RK%JaYBX{Ke>cg2%{?+o%PwU$ee-;ONd` zP-D1&Eqk?cli>p4b`iX_Ghwg=sFv7rp9mrqUT$x@dggWC!gYSbbu8Y((9-*!spJ&yBV9pSLSr?>Y7{ z1b?z15@dM_8XPxVuU`80+IG*kJFaLPmpSbsFChKU;D7JTP?vh!INt- zQ@*#L`V$YLOzz}Hfofx1kqkh|Sj)ujj@ZjbmYNW8BN0Z1j<+tWf$2-@cC@g;{JH>m z4NiU~FG~+acMw2-$2J>f2lIN|bVeMFi@w@U|GugcvISpGx8@T+y*krm5Ti0OK6<2u zk>bA>fy%e`?w%DN+>^z6I7lJU?#jrm*%Pd%!Fb{+fn^wjclX;Ik_8M{7=vEIE$0xP z!*Ox86z#-S3pD64OG7bJ9<e2&HJ!%$sHG^Ve2Jl48BgrGFX^SJR<03m??br4Z z<9FG@9z@J$0%PuXJy&pLnZqm$_Fqo4j@RwdC>8WA`9SA6hNpWMe&eW|>v$0s(Ptqx zcd~>?Ugyet815DZi^o@QD8XE&p8t7=08wtCrY$7#db#PXI07jC5p2T@)fT!+C#M;U zD_OLT>R1_gH;?t@#8N){)3s>~iELz-?U0!C-P=PIotBEgec^W= zJ$6S~4M;jpPCTWy`gkAlfMovKM_p9CHw#}{Tfc>@OOziJx~Ti`oLJtBo`S{rJ7F(s z&QD{Qm1Xo~9{{q@#d#T~fT;d?D~V;NsAwf-NXer%awETpXyr7L()(X+x2WjlU+w2# zt$@fsLvu>R4Pm4q3X4J1&_h$caoyWW3Fl(@+_%eTc5i`IhOTppO^YGM4UV)zH^eSM zi`&JImELapxAiS$&ynA%#KxQ>BsgshlBivbfQFz4(9LIik3tYx_0Sz)Eka?^me5bJ zJi}MqZ8c4q5%n3R`kzsyx-sZtcwXK2Q(~hS4+kY-FzKlu>#mXXX$15FxQImNNixD0 zf}4BjUV5v+m(*aCD@{{#E6ts}-I!^PuP1wGb!^ck@zLreObfd4Wf>qi^~|b+(5j#q z0Jw!iMt8jMA9iJkZS($ps+LK0$|&!5sv?~@ zSYj=uO_(1>JU%xLWP(CLA@t;45v{?jz`}IO{G=4j^bgrhEO3en&*XfW)vvirWN*Ga> zM&_&_puxh4a~_WO>2KB38F*L|=+6eqJBlE%VRE}s0f_x<5UzbxK&DL%!)cO3fk+{g z?vUV2a)nQH<>Fx65c}6_;CJ(R04|kTI9PyM2|HC8`eL;g*72{V$hYWR%SFY;r+iBR zzcL+>#j1SIzWUkh@uK%%oMol>x0MCI@`aaABCQ;NFpzq_?ytva&yoEmXb@|}yeDy- zIuE++t)~NaTOJlFNi?nP^IC`SsjIvZTl!~4gWK8kgAKaEIdNpc6;k#@0C=fGF@Oo2 zIQgAAUF%XBf16}W;G^Iub@EW6AHN)yey5uSz`%DG;0%vEi)L8?Bd}T>zlx~e?5epT zIEZK+Q{gt@4nD~D&)XXzJGs79Yf+H$W6GHg)3!-P^^C4*oRAVDt>a;gXk1m0y^Dec zIwhiLvo-KVn!vzO;JQf>mBNZ!;LH^KTi7r#H`eGE;luh!E6B+z8XBnT0CF0J_O5^vE`}2u+sT zOrgS54DIxIrIKQ*NvkdVjrre^J^dicL`Xndrt) zC7+@pDNllY8r;ta1oYBghJh{`SbLOtO^{xify^schJ~gJ07pzCwiREh++QH?26M_q zNrM(j6osF7Pj`8!q!1dfI5eQKlmcjsldD7t#3s8#qiC(k1f3%?wQ=CaQx~wZ6q5MF zqa_WY)tRLet5RZEQ6zNnVij{-7nyIcRsdD!IQJ2Zf~k`+L)IZ?Ua`h^)l%7b(L$!T z(_#u6I~eOQ^0l#zURQMBB9N!4}J=9&V#=%oZ5^xim$&qOk-@;q5S ztN{c|YVZz%%Bl!ReXcj;HQlK!dcOK8lJxnd%4lxviK`SaklR>%rG*L9$Uy1|p>CtSaRMVF3f##zi~3r=#G*vc<)8QJ4s>;*`KtpSdXpaS{)V16nG3g$~x8Sc1Ywk`73*35NBGbNO1z~e!3z1=Tv zz0sC;K7#+1-O66?)fG=qn5tgyj73kKVFJk-df2zoVki^!g8EyXeClBz{j zR@qNf$FVckgnm;G!u@&(rn@Xh57a^cEYcj3Vk~O+w(y7U&-Hi_FHx>M3W5N zqYp2g649?<+!uG&y#Wd*6pPZ-8Ne*V%{y{L_F9TZ@V&#Rprc6l$)Iga*E*duSR_f%W7f%5f0kfkHA3lT~zWWQPlGCxGi1&)Ngdbs$Qt zTCpkK=?;gW*O%Q7IDX&pX0Ki~D{%h@l&b2gYNCxf!-SR?#kyH$aa-}hAgNJzVA;Mj zn6{OE=@{YJi9dxoXfG&$IW}ZkOTRtAFh1+jn35jMu$n*Uv_vr^*}$R}jp9_J$!CF3 zVKCT8)E;9{hQdShjv}yP2}jy>8PZrKY=Pk|OD!#|qS)lELdM`HpQgCFDVq1nemTm} zNtSr9fpZ-b4fK<)Q|uL{vu6}ob3i2S)TJiYP5S*TO$*x#EiLo_Os;qbgtb^UonaUFl_%h}hUXv4@dx zj<(Iq9>UZ#?s$(G*CrD%vFEo&v)7F&sDv=QC5Ub2_3Q(a)R#^=tP!EBn-9_sxaiFB zMbb_~;z_RqnnY;;6|mxtD~<*w(Vb1SEJFkXyHp;IB#0karb;g-h@HgRoJ?JMzitjF z_iu=S6GAeGJc8`OV-F(js*zF>*L7>5io3dnCCii z+|qD6&t!6bE&R!4ak=8lM<-D}JRY#43oNcJPG+n2C*=5>6>L8U5<=N64Ko}07|?}<5t&XU(yd6ub;AXZKZ!mz zJ2tb<4+lOsHu~0$!|Uc~+JUW7j(QYyb6;?pYG&Ww6Uu*zk*XW9aGkewtgTCyl^ld= zHE}PwER=3X0#MPw#H~Y5o(9p>UY|Q!Tb)j^*D-tnJiOeb7u-1d4L)E}-7Vi2pRKSj(a0V3#?MVJu1u|EoYc(IwF0+CqE-qGTQ8x6o=~ktI4IfAAMaV_ zYm_COtm}|y>++bIdYmKW^86t2`$^(w%=iS}35$>kAMl^9`p~b^(3amWF<{>EmLZcl zAh8<(Lk|@mS)KH8cfz;IBQ;pib-BghjhEecAYZMEbGVJ(=O2vOT6_tCc^fT_4er2SZyrlslP)YRO$gG z_@yHrY42}f%T@q(c5CqU-GuT9pwe#&-o6=tO6-k-)yp7G(^eKvQSZ3^n4-{XRgO8d}bDQQ(okZO?1xjdv`xR`X7@9iS~UuP+it8 zO5^sz)t$SaVH1so9{hqmv$TyAWjLCWEk~(cxiyBv-Mb;0VV)uUFGX}xOY_N|6Nbis z88Y`=&y=(B$U=afbm>FiHMvapp;vqPbFBl!nWc;jv3aiS4M;`xdUKqmlV3ApoLk(y zKH5VxLl4t|z>bQZ3D=2*cNJG*=DcCyQZnSM{KtcnsI!aH%2)mPjjN8Ds9u|(-gdH? z&y34{zH*wszzl4~O46B2ReSNKypuFwKxrW=jF{;;sVJE~^~#w05k9Ib@S?N}tC!>$ zw!njYOhT?kNNX&6;Lv1H_O(`8Ic@ z+5|kM0c-Ttbyc>5m&2-5uofb_ka8h9Y#L&eVw7%m`x(5*dUSMRd*s?51@&bB)XlsC z@CN{z=gy!du8DoM!OTZ=#S6vNVMc&eH&+i;#}%$aC5jOl5l7*}m1;tPpItwV0QM%miJ|+2 z;NJ~9dY@@F?|E`{){{-3@b=St+ZStk@WSe}VkB z+4B5rwgA-axUnceKxLvpKrH{uY{4@EQvfb`t(?}y|H@|Lm6s7L%3+Z7vXGy|uCvE9DOriqvtWw2^=_aA4l8J;h zU(K$_Um%2%n4u03Rv(N{U=J9+7`7+4ChRfql2{nGzZ5A1Tk{~EO|-0A2*Lo%wn#PL z%ghJ-(i@fbn8f4`AfCh4zqvdF%zhZH5l#!#kAj~^3)B;4+_nw|WmgsVuSUof1x&TJ5papPUq?XZKOaxM z;CsQ=?2l$dX3&};gdC%4s^$Znf>=b|*)k9qc5A^L~Px?0RZg96e^=O9e75ZP~i8&OYBMxvu~F5g`P5pZ#OPa9BisP%7fqb zpHfI=TbovJk^QIEkWGMC&!;W|bv`=Woqei1yW76k$JOTANhL({vf(%=^4-?H1RGF_ zs1@9fKoC1&DY?DDOi zZjaCv8_9%G=ZMMFLW;^2nN6;G@x2M;3nB|6q zpz(APq`5z}F7>StYC<9^@D~@1wW5cBs6!+Raf)EWI3UZ5d^^TYGnVtY6!SuzZw;UU|wrS z9XnaU+sNJ0e}sVkeS3x*o|%>D>UHUhr6JzWd=k#$%kwv!o^1B6U)+DsymPvmIw-%h z&0pd+ziY1@#3RH!jnLDH-$}UCgGTnJ(c;vYH@hQyORxs(PaT&(Ayr?OX>lj0=Vb<9 zZeAKS5fnfid`tU08e8IUiF2dk!YGTe?KQ}1gJRnP)AIXMs|g2jUJhQthS)|-?G^$h zdZLL2%L($$C-jA5gf)5xoo^3}{t#qG?n=*=+?=mq@N%UCXp}FYyy%pr)z6p_csX?S z0Ef8`NRFqs0+pBcg~eVbHpXIOPp`43XL+%}c^44zPAVeqA9(9~RetBrl7(Z}05Ms{ zBWm^ef_gRx6F0Mb7Fd~4JbJ+N?srcwL$dt-hwIe#k^&}dZeFX`4XvSuK||%M0!XZ+ zIC37|a~N+QpkV8P2Ce5*M!H^!#vi{68EJcE+q`k@Y&AC$I3;;wluCq;=I8qdJ{V+B zLK7hBPTMWm)fwR|cC(dBbofU8@(V>IMn>!;-ABqLEpVH|B4a09PkGk|`pO3NiqnW< zpEV$chqh%BTfmUfv#@ns-r@|GkE02cpeoL#ImPlvWcC;gWf1w7X_fxS23YA8;GRL> zTu@CzPLl@;|u4s$E0d$JkC@@goQ4 z?#$!A4G@*%dvuqkTOr#AT&;F18}9`PQJsE*rn9e*L}ERG>hnm?{+>WPD0ZFV!(;}; z_wKddt&fp?P+=A`_v?DJkR{+zbJauclS>IeF>-f7=Cj2wLK;CfzwLmRm0tN{jPgGE zt6PjFoQGrU5elr4?s;o_`7cqm?u~LORm2V26E5!mVIlx7|2}J$DiQsBR z>s|OR?khb)z;Omt!2h5w*<#)0^rVq*Y}p7)HQfD7VrHc!B#;%4Kb?HoTvyQ?d5{9F z(0F794eZL#eIz7Rsi;k`vJDhIfX+A%YPH@U(qrX7c0a~q$vKJ}+M#Ujh`<2Mqg5v5 z$rdkTBRqZ`(Ywc;9^;yQ^cttrUBgJ8Corkg*A3l*r9dA>`L!r>KwW7N9bz6d76=0j z3Av1^kA8HkZ1H9rEw3`1Iwhi&7uc&Q9bwQZ1mB-d-pqS*4yx9ye2sUpbe-zyDNttH z4#nTr3@u7XTYQXcvT6PS2Mz?RrD%|?`V!7^R|rS;!gV&)zH=pKVr(1Piiwj~WwC67 zs3-Q`V`BW2Rg(cXuC*KMP}4SyFVBUmpNZb6{FU%H!fN5lbZEY@ZR_t)4;t5tZBTtP3)pA7=;)I;=wqnung#wYsNQVaJ-0Dd~8GA1_ruo+8JqgF#>H zU8B;k{5S7kK-=dOSU7S?bDWR>K8ZZ^a4K~nmVv8j=uE4j>fMbrs%=S(1D_rV^^WZ3 zAWm7laR9H*qUfrrh+_e;AxAku(PKjus7Y)&WD3xb)D(tmxiTeIqgHLgz)(5B&_Q3> zYiu7RtwQ1FX8hBLE)Pe!7X`!FzN8Ui{73c4;^>@RW5pEok5cI1T1tb_Ep4+~U+~+3 zSCiXZ+CH*^SC}@*)d|~sQzH>*5u>1ROvH#PEJX|%mLl54_}K>FH>FBQGxAUFSXp(e z!`Q%U$QbsLJA25i!hlv5Qf&r|4(t4~&Ejk!&2*j;K1_vnwo(vbZPngR@k`zH8eIHG zF?k(5-&PmduQqsxL+YFQIE|`C_yEJmv+fB|v5>Rw_?lf>ZaL7z<2Pc&$}vHil|0PnTSOCd4q50R25N_H--L?6xZl=OD7Pb6e73q1dp9MND| z{x_A@5w+jmu!)T%`J1`}b&FJdQQ);c(^>WOAJ1|cV%fC-5nnejcQduSidK7Ow?S8* z?s_)z>9IwW-|k5F{=X*SxkDPyaE2Tut`KQcw7zyGa}g4wDrz{k+5%M5A&lMeGMb`~ z15CfA$L+?{hjx{uIMwl@znQGh*rbW@oh+N(plYeJK|>}>P2?axEy$4INWkO2*0i`t526GCW7+{vA(M|O0OI&lUZivuLRdfm zlc`ly_K?Atb>AKF4~1NkYLEKPx=N_>&C2qIBO0Ox0O$DjLLBDAnhs_EF8DO%B@>c> zSMB%Oqn&7Q=Vf33GIYbQye88$%fr87plK(P&5+@uH0dZY_WwU>@G1fQ1lKh zE}woAvD7uwFqRAKpKXF*hAx~JR2wc>xxaK*I%`H9FG+V@B@{Q)^z3LGu zDqa z+&6|-u`4sO-$J&5QUxLMBDn}6CzcZS85?*YA3Oz&v6d952MuAe5GjXkgDWQwonD^2 zv+vHTfe~0^IkQvRIVf+{i8a|LD27flpfkYS14*+aom&oPMnEHC?5SHOk_GUZYnh7& zw6=RH!v=jYC>3wHH96Idfu1$&{d%89>A;`ujnAkm%OPQXk18)K z%%?=^R#U){vHVDAJDs5CI|bYJUM?+)Cx_yRNB`X9ndclX2Hkd)Y>w=IhDlosMK5e6 zKS;d)!QwaM+ss(tGo8kIt%rGo!7N1th<gc&Iee!bFPJdMu+8nb13_N zyD?RdGJal%c@zU)lOZ|*5}n)Fk6rBSrzCg!U>-9J{Ihsp3fEDkqk8pHfCGrnC3o_= zS{IiSR(ttP+dsqNzG3gk(ep&a%gZbK%zOY3o<`DN#X`Pj+?{J7uA!6u+y=}52ulv! zG(N3r4TeY*3)9l2`lWz5l@0e2b;ltcq&3D;pi!pe$c)vD-&lnbrl`WMK zr^!Mq#VDaRxO@cr`c5j|m084qB)H-fGoy1P@)hdvE5dELUrL`@J+7x%9V&FnO>fdI zcQ+X0_|$qB<*d}`7{o*~6(7C&v!y>$0(ApFP;%fLD^Swju;X!;p~jVEHvBjrZ!mV4 z;lIj#{gwMgH8bYVKvAB8GB2ocf4EraG3^QoN@PnWvG5cA9wRd$;9pb(%xNVq*y(V~ zA`_bqk?bhB>Y8NOn#rsShKFB@K6=%{$VUYm#W++IOjJ8uC~lwHTTz2ZbD#Dg+i?ie z<QQZfT_P~6E;z>Y$|7GLVLp$^#9>KoR_`PyntJw0YcdyMVCja$5r+ksdNIjeH+m7YM6fHY+2pWM3;sp7I6 zTqXP4@OzX$xt-(;V1_>Un$xzJyF|5X&COJSe{2&ukta#>w8=T!%UCY7{NfLPkA$2| zz0;^_u?A-v>7KHQwi;67A0L^{ib1P;qQxS&j8r>bc->A$Q`O1)JF)V+{X|5Y6#S)4 z_JYOq`>^7aBHmJWpv}XVHPBt_bRCsW#{eDffksC*jF!?JKuI|;IrHo&{fp%chw)@a zeAP`bizbZ+Vstt|kPlnEdP}wO^+fANep46y@{_+ICmvB;i)bmSc2fm?CXv7Xr~ugm5nsEM-jNHr#L z8MpnD&`|h1prRf%+<(rf z^obb%(2V{$s;WP2x(LTIZjH0oYXaH%BmNAghyso?#3GoPT%W(XeXU-UOP;MO5=ca{ zXYeVz>=B1I3~+nmN*JN~k@|C!!@!TYJ$j+d8#XY?8V1?}VdFZu8A|4~f%uUf)?hbMji z2mD`^g?j&Eg#`pW|3KJf82<$UTl(LS|DEajcZeQ5@PGVR;GewL|AhS77x)MA=l;JS z|5yI|O8$3T^4}qZX#WZMCrZivKUM-)BKrS^fd9?V0Rf@^J>#MNep*W4zfb=MU + framebufferRecycler?.takeMatOrNull().let { mat -> + Utils.bitmapToMat(Bitmap(bmp), mat) + + outputPosters.forEach { poster -> + poster.post(mat) + } + + framebufferRecycler?.returnMat(mat) + } + } + } + } } }) - setSize(size.width.toInt(), size.height.toInt()) } @@ -110,6 +129,18 @@ class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Visi } } + fun attachOutputPoster(poster: MatPoster) { + synchronized(outputPosters) { + outputPosters.add(poster) + } + } + + fun detachOutputPoster(poster: MatPoster) { + synchronized(outputPosters) { + outputPosters.remove(poster) + } + } + fun skiaPanel() = SkiaPanel(skiaLayer) override fun setSize(width: Int, height: Int) { diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java index feb7a13f..38b99f08 100644 --- a/Vision/src/main/java/org/opencv/android/Utils.java +++ b/Vision/src/main/java/org/opencv/android/Utils.java @@ -1,9 +1,11 @@ package org.opencv.android; import android.graphics.Bitmap; +import org.jetbrains.skia.ColorType; import org.jetbrains.skia.impl.BufferUtil; import org.opencv.core.CvType; import org.opencv.core.Mat; +import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import java.nio.ByteBuffer; @@ -69,10 +71,30 @@ public static void matToBitmap(Mat mat, Bitmap bmp) { } private static void nBitmapToMat2(Bitmap b, Mat mat, boolean unPremultiplyAlpha) { + mat.create(new Size(b.getWidth(), b.getHeight()), CvType.CV_8UC4); + int size = b.getWidth() * b.getHeight() * 4; + + long addr = b.theBitmap.peekPixels().getAddr(); + ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, size); + + if( b.theBitmap.getImageInfo().getColorType() == ColorType.RGBA_8888 ) + { + Mat tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC4, buffer); + if(unPremultiplyAlpha) Imgproc.cvtColor(tmp, mat, Imgproc.COLOR_mRGBA2RGBA); + else tmp.copyTo(mat); + + tmp.release(); + } else { + // info.format == ANDROID_BITMAP_FORMAT_RGB_565 + Mat tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC2, buffer); + Imgproc.cvtColor(tmp, mat, Imgproc.COLOR_BGR5652RGBA); + + tmp.release(); + } } - private static byte[] data = new byte[0]; + private static byte[] m2bData = new byte[0]; private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { Mat tmp; @@ -104,16 +126,16 @@ private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) { int size = tmp.rows() * tmp.cols() * tmp.channels(); - if(data.length != size) { - data = new byte[size]; + if(m2bData.length != size) { + m2bData = new byte[size]; } - tmp.get(0, 0, data); + tmp.get(0, 0, m2bData); long addr = b.theBitmap.peekPixels().getAddr(); ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, size); - buffer.put(data); + buffer.put(m2bData); tmp.release(); } From 2f58079fb411cf86390c5bc7f88684ca2de69863 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 28 Aug 2023 12:48:17 -0700 Subject: [PATCH 45/46] Fix EOCV-Sim restart --- .../com/github/serivesmejia/eocvsim/EOCVSim.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 5850e065..fd5b8000 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 @@ -50,6 +50,7 @@ import com.qualcomm.robotcore.eventloop.opmode.OpMode import com.qualcomm.robotcore.eventloop.opmode.OpModePipelineHandler import io.github.deltacv.vision.external.PipelineRenderHook import nu.pattern.OpenCV +import org.opencv.core.Mat import org.opencv.core.Size import org.openftc.easyopencv.TimestampedPipelineHandler import java.awt.Dimension @@ -90,6 +91,8 @@ class EOCVSim(val params: Parameters = Parameters()) { try { System.load(alternativeNative.absolutePath) + Mat().release() //test if native lib is loaded correctly + isNativeLibLoaded = true logger.info("Successfully loaded the OpenCV native lib from specified path") @@ -153,6 +156,7 @@ class EOCVSim(val params: Parameters = Parameters()) { private val hexCode = Integer.toHexString(hashCode()) private var isRestarting = false + private var destroying = false enum class DestroyReason { USER_REQUESTED, RESTART, CRASH @@ -265,9 +269,13 @@ class EOCVSim(val params: Parameters = Parameters()) { } private fun start() { + if(Thread.currentThread() != eocvSimThread) { + throw IllegalStateException("start() must be called from the EOCVSim thread") + } + logger.info("-- Begin EOCVSim loop ($hexCode) --") - while (!eocvSimThread.isInterrupted) { + while (!eocvSimThread.isInterrupted && !destroying) { //run all pending requested runnables onMainUpdate.run() @@ -326,7 +334,8 @@ class EOCVSim(val params: Parameters = Parameters()) { logger.warn("Main thread interrupted ($hexCode)") if (isRestarting) { - isRestarting = false + Thread.interrupted() //clear interrupted flag + EOCVSim(params).init() } } @@ -346,6 +355,7 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.close() eocvSimThread.interrupt() + destroying = true if (reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) jvmMainThread.interrupt() } From e9798c8e1e4e0081eaca4ede2d40167fc9c5fcc4 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Mon, 28 Aug 2023 13:02:59 -0700 Subject: [PATCH 46/46] Changelog for v3.5.0 --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 294f538c..97a5e142 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Since OpenCV in Java uses a native library, which is platform specific, the simu * Windows x86_64 (tested) * Windows x86 (untested) * MacOS x86_64 (tested) +* MacOS AARCH64/Apple Silicon (untested) * Linux x86_64 (tested for Ubuntu 20.04) * Linux ARMv7 & ARMv8 (partially tested in Raspbian but not officially endorsed)
@@ -71,6 +72,21 @@ 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.5.0 - New VisionPortal and VisionProcessor API](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.5.0) + - This is the 18th release for EOCV-Sim + - Changelog + - Addresses the changes made in the FTC SDK 8.2 to prepare for the 2023-2024 season: + - EOCV-Sim's Viewport implementation has been changed to one using Skiko (Skia) rendering - to address new features implemented in [EasyOpenCV v1.7.0](https://github.com/OpenFTC/EasyOpenCV/releases/tag/v1.7.0) + - The VisionPortal & VisionProcessor interfaces have been implemented onto EOCV-Sim - VisionProcessors are treated just like OpenCvPipelines and are automatically detected by the sim to be executed from the user interface. + - In order to use the VisionPortal API, OpModes have been added onto the simulator - a new "OpMode" tab on the user interface has been added to address this addition. NOTE: OpModes are only limited to use VisionPortal APIs, other FTC SDK apis such as hardware DcMotor have not been implemented. + - A new public API for android.graphics has been adding onto the simulator, translating android.graphics API called by the user into Skiko calls, adding compatibility to the new features in [EasyOpenCV v1.7.0](https://github.com/OpenFTC/EasyOpenCV/releases/tag/v1.7.0) related to canvas drawing. + - AprilTagProcessor has also been implemented straight from the SDK, allowing its full API to be used and attached to a VisionProcessor - [see this example OpMode](https://github.com/deltacv/EOCV-Sim/blob/dev/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java). + - AprilTagDesktop plugin has been updated to match [EOCV-AprilTag-Plugin v2.0.0](https://github.com/OpenFTC/EOCV-AprilTag-Plugin/releases/tag/v2.0.0) + - Support for Apple Silicon Macs has been added to AprilTagDesktop + - Several quality of life upgrades to the UI + - Bug fixes: + - Fixes issues related to pipeline and input source selection - UI components now exclusively react to user interactions as opposed to past versions where changes triggered by EOCV-Sim were picked up as user-made and caused several issues + ### [v3.4.3 - M1 Mac OpenCV Support](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.4.3) - This is the 17th release for EOCV-Sim - Changelog