From 007709a17d3668a3bccdfccc83013251513910b7 Mon Sep 17 00:00:00 2001 From: Thomas <thomas.musset@pasteur.fr> Date: Fri, 13 Sep 2024 16:27:37 +0200 Subject: [PATCH] updated pom to v5.0.0-a.1, fix classes accordingly to new architecture, added icon, updated .gitignore --- .gitignore | 41 +- pom.xml | 65 +-- .../java/plugins/adufour/opencv/OpenCV.java | 530 ++++++++---------- .../plugins/adufour/opencv/OpenCVCapture.java | 165 +++--- .../resources/plugins/adufour/opencv/icon.png | Bin 0 -> 15296 bytes 5 files changed, 364 insertions(+), 437 deletions(-) create mode 100644 src/main/resources/plugins/adufour/opencv/icon.png diff --git a/.gitignore b/.gitignore index 3d47f98..57f16fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,41 @@ -.idea/ +/build* +/workspace +setting.xml +release/ target/ -.settings/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +icy.log + +### IntelliJ IDEA ### +.idea/ +*.iws *.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath .project -.classpath \ No newline at end of file +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +**/.DS_Store +Icon? \ No newline at end of file diff --git a/pom.xml b/pom.xml index b5f3b6e..1c5d3b6 100644 --- a/pom.xml +++ b/pom.xml @@ -8,18 +8,16 @@ <parent> <artifactId>pom-icy</artifactId> <groupId>org.bioimageanalysis.icy</groupId> - <version>2.0.0</version> + <version>3.0.0-a.2</version> </parent> <!-- Project Information --> <artifactId>opencv</artifactId> - <version>4.5.1-2</version> - - <packaging>jar</packaging> + <version>5.0.0-a.1</version> <name>OpenCV</name> <description>OpenCV (Open Computer Vision) library for Icy. see more at http://opencv.org</description> - <url>http://icy.bioimageanalysis.org/plugin/opencv/</url> + <url>https://icy.bioimageanalysis.org/plugin/opencv/</url> <inceptionYear>2020</inceptionYear> <organization> @@ -52,79 +50,26 @@ </roles> </developer> </developers> - - <!-- Project properties --> - <properties> - <artifact-to-include>opencv</artifact-to-include> - </properties> - - <profiles> - <profile> - <id>icy-plugin-extract-library</id> - <activation> - <activeByDefault>true</activeByDefault> - </activation> - </profile> - </profiles> - - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-dependency-plugin</artifactId> - <executions> - <execution> - <id>${project.artifactId}-fetch</id> - <phase>generate-sources</phase> - <goals> - <goal>unpack-dependencies</goal> - </goals> - <configuration> - <includeArtifactIds>${artifact-to-include}</includeArtifactIds> - <outputDirectory>${project.build.outputDirectory}</outputDirectory> - <stripVersion>true</stripVersion> - <excludeTransitive>true</excludeTransitive> - </configuration> - </execution> - </executions> - </plugin> - </plugins> - </build> <!-- List of project's dependencies --> <dependencies> <dependency> <groupId>org.openpnp</groupId> <artifactId>opencv</artifactId> - <version>4.5.1-2</version> - </dependency> - - <!-- The core of Icy --> - <dependency> - <groupId>org.bioimageanalysis.icy</groupId> - <artifactId>icy-kernel</artifactId> - <version>${icy-kernel.version}</version> + <version>4.9.0-0</version> </dependency> <dependency> <groupId>org.bioimageanalysis.icy</groupId> <artifactId>ezplug</artifactId> - <version>${ezplug.version}</version> </dependency> - - <dependency> - <groupId>org.bioimageanalysis.icy</groupId> - <artifactId>icy-bioformats</artifactId> - <version>${icy-bioformats.version}</version> - </dependency> </dependencies> <!-- Icy Maven repository (to find parent POM) --> <repositories> <repository> <id>icy</id> - <name>Icy's Nexus</name> - <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> + <url>https://nexus-icy.pasteur.cloud/repository/icy/</url> </repository> </repositories> </project> \ No newline at end of file diff --git a/src/main/java/plugins/adufour/opencv/OpenCV.java b/src/main/java/plugins/adufour/opencv/OpenCV.java index 473bb08..9ac4a1e 100644 --- a/src/main/java/plugins/adufour/opencv/OpenCV.java +++ b/src/main/java/plugins/adufour/opencv/OpenCV.java @@ -1,48 +1,68 @@ +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ + package plugins.adufour.opencv; +import org.bioimageanalysis.icy.Icy; +import org.bioimageanalysis.icy.common.Version; +import org.bioimageanalysis.icy.common.math.FPSMeter; +import org.bioimageanalysis.icy.common.type.DataType; +import org.bioimageanalysis.icy.extension.plugin.abstract_.Plugin; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName; +import org.bioimageanalysis.icy.model.image.IcyBufferedImage; +import org.bioimageanalysis.icy.model.sequence.Sequence; +import org.bioimageanalysis.icy.model.sequence.SequenceAdapter; +import org.bioimageanalysis.icy.system.SystemUtil; +import org.bioimageanalysis.icy.system.logging.IcyLogger; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; +import org.opencv.videoio.VideoCapture; +import org.opencv.videoio.Videoio; + +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import javax.imageio.ImageIO; - -import org.opencv.core.Core; -import org.opencv.core.CvType; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; -import org.opencv.videoio.VideoCapture; -import org.opencv.videoio.Videoio; - -import icy.common.Version; -import icy.image.IcyBufferedImage; -import icy.main.Icy; -import icy.math.FPSMeter; -import icy.plugin.abstract_.Plugin; -import icy.plugin.interface_.PluginLibrary; -import icy.sequence.Sequence; -import icy.sequence.SequenceAdapter; -import icy.system.IcyExceptionHandler; -import icy.system.IcyHandledException; -import icy.system.SystemUtil; -import icy.type.DataType; - /** * <a href=http://opencv.org>OpenCV</a> library for Icy. This class contains additional test * functions as well as tools to convert between OpenCV and Icy image formats. - * + * * @author Alexandre Dufour */ -public class OpenCV extends Plugin implements PluginLibrary -{ - public static final Version OpenCV_Version = new Version(Core.VERSION_MAJOR, Core.VERSION_MINOR, - Core.VERSION_REVISION); +@IcyPluginName("OpenCV") +@IcyPluginIcon(path = "/plugins/adufour/opencv/icon.png") +public class OpenCV extends Plugin { + public static final Version OpenCV_Version = new Version(Core.VERSION_MAJOR, Core.VERSION_MINOR, Core.VERSION_REVISION); /** * A local buffer used to speed up repetitive conversions between OpenCV and Icy @@ -51,41 +71,32 @@ public class OpenCV extends Plugin implements PluginLibrary private static final ExecutorService service = Executors.newFixedThreadPool(SystemUtil.getNumberOfCPUs()); - static - { - String loadingMessage = "Loading OpenCV " + OpenCV_Version + "..."; - String successMessage = "OpenCV " + OpenCV_Version + " successfully loaded."; + static { + final String loadingMessage = "Loading OpenCV " + OpenCV_Version + "..."; + final String successMessage = "OpenCV " + OpenCV_Version + " successfully loaded."; - try - { + try { // First check whether OpenCV is already installed - System.out.println(loadingMessage); + IcyLogger.info(OpenCV.class, loadingMessage); nu.pattern.OpenCV.loadShared(); - System.out.println(successMessage); + IcyLogger.success(OpenCV.class, successMessage); } - catch (UnsatisfiedLinkError lib1) - { - IcyExceptionHandler.handleException(lib1, true); - System.out.println("Trying alternate method"); + catch (final UnsatisfiedLinkError lib1) { + IcyLogger.warn(OpenCV.class, lib1, "Unable to load OpenCV, trying alternate method (1/2)"); - try - { + try { // alternate method loadLibrary(OpenCV.class, Core.NATIVE_LIBRARY_NAME); } - catch (UnsatisfiedLinkError lib2) - { - IcyExceptionHandler.handleException(lib2, true); - System.out.println("Last chance..."); + catch (final UnsatisfiedLinkError lib2) { + IcyLogger.warn(OpenCV.class, lib1, "Unable to load OpenCV, trying alternate method (2/2)"); - try - { + try { // last chance... System.loadLibrary(Core.NATIVE_LIBRARY_NAME); } - catch (UnsatisfiedLinkError lib3) - { - IcyExceptionHandler.handleException(lib3, true); + catch (final UnsatisfiedLinkError lib3) { + IcyLogger.fatal(OpenCV.class, lib1, "Unable to load OpenCV"); } } } @@ -95,22 +106,20 @@ public class OpenCV extends Plugin implements PluginLibrary * Initializes OpenCV by loading the native libraries for the current operating system. This * method should be called once before any OpenCV class is used */ - public static void initialize() - { + @Contract(pure = true) + public static void initialize() { // There is no need to actually initialize anything. // The static block above will be executed as soon as this method is called. } /** * Converts the specified OpenCV {@link Mat} into an {@link IcyBufferedImage} - * - * @param mat - * the OpenCV {@link Mat} to convert + * + * @param mat the OpenCV {@link Mat} to convert * @return an {@link IcyBufferedImage} */ - public static IcyBufferedImage convertToIcy(Mat mat) - { - IcyBufferedImage output = new IcyBufferedImage(mat.width(), mat.height(), mat.channels(), getIcyDataType(mat)); + public static @NotNull IcyBufferedImage convertToIcy(final @NotNull Mat mat) { + final IcyBufferedImage output = new IcyBufferedImage(mat.width(), mat.height(), mat.channels(), Objects.requireNonNull(getIcyDataType(mat))); convertToIcy(mat, output); return output; } @@ -120,34 +129,32 @@ public class OpenCV extends Plugin implements PluginLibrary * {@link IcyBufferedImage} object.<br> * This method requires that the output image is initialized and has the correct dimensions and * data type (if unsure, use {@link #convertToIcy(Mat)} instead). - * - * @param mat Mat + * + * @param mat Mat * @param output IcyBufferedImage - * throws {@link NullPointerException} if either parameter is null + * throws {@link NullPointerException} if either parameter is null */ - public static void convertToIcy(Mat mat, final IcyBufferedImage output) - { + @SuppressWarnings({"SuspiciousSystemArraycopy", "JavaReflectionMemberAccess"}) + public static void convertToIcy(final @NotNull Mat mat, final IcyBufferedImage output) { final int width = mat.cols(); final int height = mat.rows(); final int nChannels = mat.channels(); - int bufferSize = width * height * nChannels; + final int bufferSize = width * height * nChannels; // handle the easy case (single-channel) first - if (nChannels == 1) - { - try - { + if (nChannels == 1) { + try { // Retrieve the frame data in a generic way // This is equivalent to calling e.g. Mat.get(0, 0, (byte[]) buffer); - Object dataBuffer = output.getDataXY(0); - Class<?> dataType = dataBuffer.getClass(); + final Object dataBuffer = output.getDataXY(0); + final Class<?> dataType = dataBuffer.getClass(); Mat.class.getMethod("get", int.class, int.class, dataType).invoke(mat, 0, 0, dataBuffer); return; } - catch (Exception e) - { - throw new RuntimeException(e); + catch (final Exception e) { + IcyLogger.error(OpenCV.class, e); + return; } } @@ -155,152 +162,130 @@ public class OpenCV extends Plugin implements PluginLibrary if (Array.getLength(buffer) != bufferSize) buffer = null; final DataType dataType = getIcyDataType(mat); - Class<?> type = dataType.toPrimitiveClass(); + final Class<?> type = Objects.requireNonNull(dataType).toPrimitiveClass(); if (buffer == null || buffer.getClass().getComponentType() != type) buffer = Array.newInstance(type, bufferSize); - try - { + try { // Retrieve the frame data in a generic way // This is equivalent to calling e.g. Mat.get(0, 0, (byte[]) buffer); Mat.class.getMethod("get", int.class, int.class, buffer.getClass()).invoke(mat, 0, 0, buffer); } - catch (Exception e) - { - throw new RuntimeException(e); + catch (final Exception e) { + IcyLogger.error(OpenCV.class, e); + return; } - ArrayList<Future<?>> tasks = new ArrayList<Future<?>>(nChannels); + final ArrayList<Future<?>> tasks = new ArrayList<>(nChannels); - for (int c = 0; c < nChannels; c++) - { + for (int c = 0; c < nChannels; c++) { final int channel = c; - tasks.add(service.submit(new Runnable() - { - @Override - public void run() - { - switch (dataType) - { - case BYTE: - case UBYTE: - { - // OpenCV reads BGR by default => revert this to conventional RGB - final byte[] icyChannel = output.getDataXYAsByte(nChannels - channel - 1); - for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) - icyChannel[out] = ((byte[]) buffer)[in]; - break; - } - case SHORT: - case USHORT: - { - // OpenCV reads BGR by default => revert this to conventional RGB - final short[] icyChannel = output.getDataXYAsShort(nChannels - channel - 1); - for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) - icyChannel[out] = ((short[]) buffer)[in]; - break; - } - case INT: - { - // OpenCV reads BGR by default => revert this to conventional RGB - final int[] icyChannel = output.getDataXYAsInt(nChannels - channel - 1); - for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) - icyChannel[out] = ((int[]) buffer)[in]; - break; - } - case FLOAT: - { - // OpenCV reads BGR by default => revert this to conventional RGB - final float[] icyChannel = output.getDataXYAsFloat(nChannels - channel - 1); - for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) - icyChannel[out] = ((float[]) buffer)[in]; - break; - } - case DOUBLE: - { - // OpenCV reads BGR by default => revert this to conventional RGB - final double[] icyChannel = output.getDataXYAsDouble(nChannels - channel - 1); - for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) - icyChannel[out] = ((double[]) buffer)[in]; - break; - } - default: - { - // OpenCV reads BGR by default => revert this to conventional RGB - final Object icyChannel = output.getDataXY(nChannels - channel - 1); - int size = Array.getLength(icyChannel); - for (int out = 0, in = channel; out < size; out++, in += nChannels) - System.arraycopy(buffer, in, icyChannel, out, 1); - } + tasks.add(service.submit(() -> { + switch (dataType) { + case BYTE: + case UBYTE: { + // OpenCV reads BGR by default => revert this to conventional RGB + final byte[] icyChannel = output.getDataXYAsByte(nChannels - channel - 1); + for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) + icyChannel[out] = ((byte[]) buffer)[in]; + break; + } + case SHORT: + case USHORT: { + // OpenCV reads BGR by default => revert this to conventional RGB + final short[] icyChannel = output.getDataXYAsShort(nChannels - channel - 1); + for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) + icyChannel[out] = ((short[]) buffer)[in]; + break; + } + case INT: { + // OpenCV reads BGR by default => revert this to conventional RGB + final int[] icyChannel = output.getDataXYAsInt(nChannels - channel - 1); + for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) + icyChannel[out] = ((int[]) buffer)[in]; + break; + } + case FLOAT: { + // OpenCV reads BGR by default => revert this to conventional RGB + final float[] icyChannel = output.getDataXYAsFloat(nChannels - channel - 1); + for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) + icyChannel[out] = ((float[]) buffer)[in]; + break; + } + case DOUBLE: { + // OpenCV reads BGR by default => revert this to conventional RGB + final double[] icyChannel = output.getDataXYAsDouble(nChannels - channel - 1); + for (int out = 0, in = channel; out < icyChannel.length; out++, in += nChannels) + icyChannel[out] = ((double[]) buffer)[in]; + break; + } + default: { + // OpenCV reads BGR by default => revert this to conventional RGB + final Object icyChannel = output.getDataXY(nChannels - channel - 1); + final int size = Array.getLength(icyChannel); + for (int out = 0, in = channel; out < size; out++, in += nChannels) + System.arraycopy(buffer, in, icyChannel, out, 1); } } })); } - try - { - for (Future<?> task : tasks) + try { + for (final Future<?> task : tasks) task.get(); } - catch (InterruptedException e) - { + catch (final InterruptedException e) { Thread.currentThread().interrupt(); } - catch (ExecutionException e) - { - e.printStackTrace(); + catch (final ExecutionException e) { + IcyLogger.error(OpenCV.class, e); } } /** * Converts the specified {@link IcyBufferedImage} into an OpenCV {@link Mat} - * - * @param img - * the {@link IcyBufferedImage} to convert + * + * @param img the {@link IcyBufferedImage} to convert * @return an OpenCV {@link Mat} */ - public static Mat convertToOpenCV(IcyBufferedImage img) - { - int width = img.getWidth(); - int height = img.getHeight(); - int sizeC = img.getSizeC(); - int bufferSize = width * height * sizeC; + @SuppressWarnings({"SuspiciousSystemArraycopy", "JavaReflectionMemberAccess"}) + public static @Nullable Mat convertToOpenCV(final @NotNull IcyBufferedImage img) { + final int width = img.getWidth(); + final int height = img.getHeight(); + final int sizeC = img.getSizeC(); + final int bufferSize = width * height * sizeC; - Mat mat = new Mat(new Size(width, height), getCVDataType(img)); + final Mat mat = new Mat(new Size(width, height), getCVDataType(img)); - if (sizeC == 1) - { + if (sizeC == 1) { buffer = img.getDataXY(0); } - else - { + else { // Make sure the buffer has the proper type and size if (Array.getLength(buffer) != bufferSize) buffer = null; - Class<?> type = img.getDataType_().toPrimitiveClass(); + final Class<?> type = img.getDataType().toPrimitiveClass(); if (buffer == null || buffer.getClass().getComponentType() != type) buffer = Array.newInstance(type, bufferSize); - for (int c = 0; c < sizeC; c++) - { + for (int c = 0; c < sizeC; c++) { // OpenCV Mat elements are interleaved... - Object in = img.getDataXY(sizeC - c - 1); + final Object in = img.getDataXY(sizeC - c - 1); for (int j = 0, offIN = 0, offOUT = c; j < height; j++) for (int i = 0; i < width; i++, offIN++, offOUT += sizeC) System.arraycopy(in, offIN, buffer, offOUT, 1); } } - try - { + try { // Retrieve the frame data in a generic way // This is equivalent to calling e.g. Mat.put(0, 0, (byte[]) buffer); Mat.class.getMethod("put", int.class, int.class, buffer.getClass()).invoke(mat, 0, 0, buffer); } - catch (Exception e) - { - throw new RuntimeException(e); + catch (final Exception e) { + IcyLogger.error(OpenCV.class, e); + return null; } return mat; @@ -309,57 +294,37 @@ public class OpenCV extends Plugin implements PluginLibrary /** * @param img Icy Buffered Image * @return the OpenCV data type corresponding to the specified image (see the {@link CvType} - * .CV_* constants) + * .CV_* constants) */ - public static int getCVDataType(IcyBufferedImage img) - { - switch (img.getDataType_()) - { - case BYTE: - return CvType.CV_8SC(img.getSizeC()); - case UBYTE: - return CvType.CV_8UC(img.getSizeC()); - case SHORT: - return CvType.CV_16SC(img.getSizeC()); - case USHORT: - return CvType.CV_16UC(img.getSizeC()); - case UINT: // TODO ? - case INT: - return CvType.CV_32SC(img.getSizeC()); - case FLOAT: - return CvType.CV_32FC(img.getSizeC()); - case DOUBLE: - return CvType.CV_64FC(img.getSizeC()); - default: - throw new UnsupportedOperationException("OpenCV does not support type " + img.getDataType_()); - } + public static int getCVDataType(final @NotNull IcyBufferedImage img) { + return switch (img.getDataType()) { + case BYTE -> CvType.CV_8SC(img.getSizeC()); + case UBYTE -> CvType.CV_8UC(img.getSizeC()); + case SHORT -> CvType.CV_16SC(img.getSizeC()); + case USHORT -> CvType.CV_16UC(img.getSizeC()); + case UINT // TODO ? + , INT -> CvType.CV_32SC(img.getSizeC()); + case FLOAT -> CvType.CV_32FC(img.getSizeC()); + case DOUBLE -> CvType.CV_64FC(img.getSizeC()); + default -> throw new UnsupportedOperationException("OpenCV does not support type " + img.getDataType()); + }; } /** * @param mat Mat * @return the {@link DataType} corresponding to the specified OpenCV {@link Mat}rix */ - public static DataType getIcyDataType(Mat mat) - { - switch (CvType.depth(mat.type())) - { - case CvType.CV_8S: - return DataType.BYTE; - case CvType.CV_8U: - return DataType.UBYTE; - case CvType.CV_16S: - return DataType.SHORT; - case CvType.CV_16U: - return DataType.USHORT; - case CvType.CV_32S: - return DataType.INT; - case CvType.CV_32F: - return DataType.FLOAT; - case CvType.CV_64F: - return DataType.DOUBLE; - default: - return null; - } + public static @Nullable DataType getIcyDataType(final @NotNull Mat mat) { + return switch (CvType.depth(mat.type())) { + case CvType.CV_8S -> DataType.BYTE; + case CvType.CV_8U -> DataType.UBYTE; + case CvType.CV_16S -> DataType.SHORT; + case CvType.CV_16U -> DataType.USHORT; + case CvType.CV_32S -> DataType.INT; + case CvType.CV_32F -> DataType.FLOAT; + case CvType.CV_64F -> DataType.DOUBLE; + default -> null; + }; } /** @@ -367,8 +332,7 @@ public class OpenCV extends Plugin implements PluginLibrary * viewer. This method will run indefinitely unless the viewer is closed or its calling thread * is interrupted (see {@link Thread#interrupt()}) */ - public static void liveWebcam() - { + public static void liveWebcam() { liveWebcam(-1, -1, -1); } @@ -377,14 +341,13 @@ public class OpenCV extends Plugin implements PluginLibrary * viewer. This method will run indefinitely unless the viewer is closed or its calling thread * is interrupted (see {@link Thread#interrupt()}) * - * @param width int + * @param width int * @param height int - * @param fps int + * @param fps int */ - public static void liveWebcam(final int width, final int height, final int fps) - { + public static void liveWebcam(final int width, final int height, final int fps) { // Connect to the camera - VideoCapture vc = new VideoCapture(0); + final VideoCapture vc = new VideoCapture(0); if (width != -1) vc.set(Videoio.CAP_PROP_FRAME_WIDTH, width); if (height != -1) @@ -400,20 +363,17 @@ public class OpenCV extends Plugin implements PluginLibrary * viewer. This method will run indefinitely unless the viewer is closed or its calling thread * is interrupted (see {@link Thread#interrupt()}) */ - public static void liveWebcam(final VideoCapture camera) - { - new Thread(new Runnable() - { + public static void liveWebcam(final VideoCapture camera) { + new Thread(new Runnable() { @Override - public void run() - { + public void run() { // We need to know which thread to interrupt to clean the camera final Thread currentThread = Thread.currentThread(); // Read the first image to initialize the meta-data - Mat mat = new Mat(); + final Mat mat = new Mat(); camera.read(mat); - IcyBufferedImage image = convertToIcy(mat); + final IcyBufferedImage image = convertToIcy(mat); // Create the sequence final Sequence s = new Sequence("Live webcam", image); @@ -424,19 +384,16 @@ public class OpenCV extends Plugin implements PluginLibrary s.setAutoUpdateChannelBounds(false); // Closing the sequence should stop the camera - s.addListener(new SequenceAdapter() - { + s.addListener(new SequenceAdapter() { @Override - public void sequenceClosed(Sequence sequence) - { + public void sequenceClosed(final Sequence sequence) { s.removeListener(this); currentThread.interrupt(); } }); - try - { - FPSMeter fps = new FPSMeter(); + try { + final FPSMeter fps = new FPSMeter(); long grabTime = 0; long readTime = 0; long conversionTime = 0; @@ -444,17 +401,16 @@ public class OpenCV extends Plugin implements PluginLibrary double nbIterations = 0.0; // loop until the thread is interrupted - while (!currentThread.isInterrupted()) - { - long t0 = System.nanoTime(); + while (!currentThread.isInterrupted()) { + final long t0 = System.nanoTime(); camera.grab(); - long t1 = System.nanoTime(); + final long t1 = System.nanoTime(); camera.retrieve(mat); - long t2 = System.nanoTime(); + final long t2 = System.nanoTime(); convertToIcy(mat, image); - long t3 = System.nanoTime(); + final long t3 = System.nanoTime(); image.dataChanged(); - long t4 = System.nanoTime(); + final long t4 = System.nanoTime(); fps.update(); nbIterations++; @@ -463,14 +419,13 @@ public class OpenCV extends Plugin implements PluginLibrary conversionTime += (t3 - t2) / 1000000; dataUpdateTime += (t4 - t3) / 1000000; } - System.out.println("Capture frame-rate: " + fps.getFPS() + " fps"); - System.out.println("Camera grab time: " + grabTime / nbIterations + " ms"); - System.out.println("Camera read time: " + readTime / nbIterations + " ms"); - System.out.println("Conversion time: " + conversionTime / nbIterations + " ms"); - System.out.println("Data update time: " + dataUpdateTime / nbIterations + " ms"); + IcyLogger.info(OpenCV.class, "Capture frame-rate: " + fps.getFPS() + " fps"); + IcyLogger.info(OpenCV.class, "Camera grab time: " + grabTime / nbIterations + " ms"); + IcyLogger.info(OpenCV.class, "Camera read time: " + readTime / nbIterations + " ms"); + IcyLogger.info(OpenCV.class, "Conversion time: " + conversionTime / nbIterations + " ms"); + IcyLogger.info(OpenCV.class, "Data update time: " + dataUpdateTime / nbIterations + " ms"); } - finally - { + finally { // close the camera camera.release(); } @@ -482,32 +437,30 @@ public class OpenCV extends Plugin implements PluginLibrary * OpenCV test that takes the active image and performs a Sobel filter in X and Y and combines * the result into a new image */ - public static void testSobel() - { - new Thread(new Runnable() - { - @Override - public void run() - { - Sequence s = Icy.getMainInterface().getActiveSequence(); + public static void testSobel() { + new Thread(() -> { + Sequence s = Icy.getMainInterface().getActiveSequence(); + + if (s == null) { + IcyLogger.error(OpenCV.class, "Open an image first!"); + return; + } - if (s == null) - throw new IcyHandledException("Open an image first!"); + final Mat mat = convertToOpenCV(Icy.getMainInterface().getActiveImage()); - Mat mat = convertToOpenCV(Icy.getMainInterface().getActiveImage()); + final Mat x = new Mat(); + final Mat y = new Mat(); - Mat x = new Mat(); - Mat y = new Mat(); + Objects.requireNonNull(mat); - Imgproc.Sobel(mat, x, -1, 1, 0); - Imgproc.Sobel(mat, y, -1, 0, 1); + Imgproc.Sobel(mat, x, -1, 1, 0); + Imgproc.Sobel(mat, y, -1, 0, 1); - org.opencv.core.Core.addWeighted(x, 0.5, y, 0.5, 1.0, mat); + Core.addWeighted(x, 0.5, y, 0.5, 1.0, mat); - s = new Sequence("OpenCV: Sobel filter applied to " + s.getName(), convertToIcy(mat)); + s = new Sequence("OpenCV: Sobel filter applied to " + s.getName(), convertToIcy(mat)); - Icy.getMainInterface().addSequence(s); - } + Icy.getMainInterface().addSequence(s); }).start(); } @@ -518,28 +471,24 @@ public class OpenCV extends Plugin implements PluginLibrary * RPiCamera's encoding setting (JPEG by default).<br> * <br> * Usage Example:<br> - * + * * <pre> * BufferedImage buffImg = piCamera.takeBufferedStill(500, 500); * </pre> - * - * @param width - * An int specifying width of image to take. - * @param height - * An int specifying height of image to take. + * + * @param width An int specifying width of image to take. + * @param height An int specifying height of image to take. * @return A BufferedImage containing the image. - * @throws IOException - * this exception is thrown if: - * <ul> - * <li>The system is not a Raspberry Pi (RPi),</li> - * <li>The RPi is not equipped with a camera board,</li> - * <li>The <code>raspistill</code> utility is not installed,</li> - * <li>Something went wrong when reading the image from the camera</li> - * </ul> + * @throws IOException this exception is thrown if: + * <ul> + * <li>The system is not a Raspberry Pi (RPi),</li> + * <li>The RPi is not equipped with a camera board,</li> + * <li>The <code>raspistill</code> utility is not installed,</li> + * <li>Something went wrong when reading the image from the camera</li> + * </ul> */ - public static BufferedImage rpi_raspistillToBufferedImage(int width, int height) throws IOException - { - List<String> command = new ArrayList<String>(); + public static BufferedImage rpi_raspistillToBufferedImage(final int width, final int height) throws IOException { + final List<String> command = new ArrayList<>(); command.add("raspistill"); command.add("-o"); command.add("-v"); @@ -547,7 +496,7 @@ public class OpenCV extends Plugin implements PluginLibrary command.add("" + width); command.add("-h"); command.add("" + height); - ProcessBuilder pb = new ProcessBuilder(command); + final ProcessBuilder pb = new ProcessBuilder(command); // System.out.println("Executed this command:\n\t" + command.toString()); // pb.redirectErrorStream(true); @@ -555,8 +504,7 @@ public class OpenCV extends Plugin implements PluginLibrary // new File(System.getProperty("user.home") + File.separator + // "Desktop" + File.separator + "RPiCamera.out")); - Process p = pb.start(); - BufferedImage bi = ImageIO.read(p.getInputStream()); + final Process p = pb.start(); // -------------------------------------------------------------------------- // This code can be used to specify an ImageReader - perhaps for a specific // type of image - in place of the previous line: @@ -568,7 +516,7 @@ public class OpenCV extends Plugin implements PluginLibrary // ImageReadParam param = reader.getDefaultReadParam(); // BufferedImage bi = reader.read(0, param); // -------------------------------------------------------------------------- - return bi; + return ImageIO.read(p.getInputStream()); } } diff --git a/src/main/java/plugins/adufour/opencv/OpenCVCapture.java b/src/main/java/plugins/adufour/opencv/OpenCVCapture.java index 0e3f234..5fa6d22 100644 --- a/src/main/java/plugins/adufour/opencv/OpenCVCapture.java +++ b/src/main/java/plugins/adufour/opencv/OpenCVCapture.java @@ -1,40 +1,54 @@ -package plugins.adufour.opencv; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; +/* + * Copyright (c) 2010-2024. Institut Pasteur. + * + * This file is part of Icy. + * Icy is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Icy is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Icy. If not, see <https://www.gnu.org/licenses/>. + */ -import javax.swing.JSeparator; +package plugins.adufour.opencv; +import org.bioimageanalysis.icy.Icy; +import org.bioimageanalysis.icy.common.math.FPSMeter; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon; +import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName; +import org.bioimageanalysis.icy.model.image.IcyBufferedImage; +import org.bioimageanalysis.icy.model.sequence.Sequence; +import org.bioimageanalysis.icy.model.sequence.SequenceAdapter; +import org.bioimageanalysis.icy.system.logging.IcyLogger; import org.opencv.core.Mat; import org.opencv.videoio.VideoCapture; import org.opencv.videoio.Videoio; - -import icy.image.IcyBufferedImage; -import icy.main.Icy; -import icy.math.FPSMeter; -import icy.sequence.Sequence; -import icy.sequence.SequenceAdapter; -import plugins.adufour.ezplug.EzPlug; -import plugins.adufour.ezplug.EzStoppable; -import plugins.adufour.ezplug.EzVar; -import plugins.adufour.ezplug.EzVarInteger; -import plugins.adufour.ezplug.EzVarListener; +import plugins.adufour.ezplug.*; import plugins.adufour.vars.util.VarException; -public class OpenCVCapture extends EzPlug implements EzStoppable, EzVarListener<Integer> -{ - List<EzVarInteger> parameters = new ArrayList<EzVarInteger>(); - +import javax.swing.*; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +@IcyPluginName("OpenCV Capture") +@IcyPluginIcon(path = "/plugins/adufour/opencv/icon.png") +public class OpenCVCapture extends EzPlug implements EzStoppable, EzVarListener<Integer> { + List<EzVarInteger> parameters = new ArrayList<>(); + VideoCapture camera = null; - + @Override - protected void initialize() - { + protected void initialize() { OpenCV.initialize(); - - for (Field f : Videoio.class.getDeclaredFields()) - { + + for (final Field f : Videoio.class.getDeclaredFields()) { String name = f.getName(); if (!name.startsWith("CAP_PROP_")) continue; if (name.contains("_DC1394")) continue; @@ -47,114 +61,99 @@ public class OpenCVCapture extends EzPlug implements EzStoppable, EzVarListener< if (name.contains("_POS")) continue; if (name.contains("_PVAPI")) continue; if (name.contains("_XI")) continue; - + name = name.substring(9).toLowerCase(); - EzVarInteger setting = new EzVarInteger(name); - setting.setValue(Integer.valueOf(-1)); + final EzVarInteger setting = new EzVarInteger(name); + setting.setValue(-1); setting.addVarChangeListener(this); parameters.add(setting); } - + int cpt = 0; - for (EzVarInteger setting : parameters) - { + for (final EzVarInteger setting : parameters) { addEzComponent(setting); cpt++; if (cpt % 20 == 0) addComponent(new JSeparator(JSeparator.VERTICAL)); } } - + @Override - protected void execute() - { + protected void execute() { camera = new VideoCapture(0); - - for (EzVarInteger setting : parameters) - { + + for (final EzVarInteger setting : parameters) { if (setting.getValue() == -1) continue; - - String propName = "CAP_PROP_" + setting.name.toUpperCase(); - try - { - int propID = Videoio.class.getDeclaredField(propName).getInt(null); + + final String propName = "CAP_PROP_" + setting.name.toUpperCase(); + try { + final int propID = Videoio.class.getDeclaredField(propName).getInt(null); camera.set(propID, setting.getValue()); } - catch (Exception e) - { + catch (final Exception e) { throw new VarException(setting.getVariable(), e.getMessage()); } } - + // Read the first image to initialize the meta-data - Mat mat = new Mat(); + final Mat mat = new Mat(); camera.read(mat); - IcyBufferedImage image = OpenCV.convertToIcy(mat); - + final IcyBufferedImage image = OpenCV.convertToIcy(mat); + // Create the sequence final Sequence s = new Sequence("Live webcam", image); Icy.getMainInterface().addSequence(s); - + // don't update the channel bounds on update image.setAutoUpdateChannelBounds(false); s.setAutoUpdateChannelBounds(false); - + // Closing the sequence should stop the camera final Thread thread = Thread.currentThread(); - s.addListener(new SequenceAdapter() - { + s.addListener(new SequenceAdapter() { @Override - public void sequenceClosed(Sequence sequence) - { + public void sequenceClosed(final Sequence sequence) { s.removeListener(this); thread.interrupt(); } }); - - FPSMeter fps = new FPSMeter(); - - try - { - while (camera.read(mat) && !Thread.currentThread().isInterrupted()) - { + + final FPSMeter fps = new FPSMeter(); + + try { + while (camera.read(mat) && !Thread.currentThread().isInterrupted()) { OpenCV.convertToIcy(mat, image); image.dataChanged(); fps.update(); getUI().setProgressBarMessage("Acquiring at " + fps.getFPS() + "fps"); } } - catch (Exception e) - { - + catch (final Exception e) { + IcyLogger.warn(this.getClass(), e); } - finally - { + finally { camera.release(); } } - + @Override - public void clean() - { - for (EzVarInteger setting : parameters) + public void clean() { + for (final EzVarInteger setting : parameters) setting.removeVarChangeListener(this); parameters.clear(); } - + @Override - public void variableChanged(EzVar<Integer> source, Integer newValue) - { + public void variableChanged(final EzVar<Integer> source, final Integer newValue) { if (camera == null) return; - - String propName = "CAP_PROP_" + source.name.toUpperCase(); - try - { - int propID = Videoio.class.getDeclaredField(propName).getInt(null); + + final String propName = "CAP_PROP_" + source.name.toUpperCase(); + try { + final int propID = Videoio.class.getDeclaredField(propName).getInt(null); camera.set(propID, newValue); } - catch (Exception e) - { + catch (final Exception e) { throw new VarException(source.getVariable(), e.getMessage()); } } - + } diff --git a/src/main/resources/plugins/adufour/opencv/icon.png b/src/main/resources/plugins/adufour/opencv/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..33c18ccbabdd18ccf257a30267f4a085e64bb6f5 GIT binary patch literal 15296 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 S{+nC?0000<MNUMnLSTZ2t$J+$ literal 0 HcmV?d00001 -- GitLab