diff --git a/.gitignore b/.gitignore
index 30c9a54acde67825b40daec3db2670ef0c6a64ea..57f16fb67c1b1589981416b323d7a9debc728665 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,41 @@
+/build*
+/workspace
+setting.xml
+release/
 target/
-workspace/
-.settings/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+icy.log
+
+### IntelliJ IDEA ###
 .idea/
-setting.xml
-.project
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
 .classpath
-*.prefs
-*.jardesc
-*.jar
-**/.DS_Store
\ No newline at end of file
+.factorypath
+.project
+.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 8f4dc7ab62fe4ffdbf502b1ceb266184abf0a391..09be7c043bb4b3f893d32057dead94d713d1b0fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,12 +8,12 @@
     <parent>
         <groupId>org.bioimageanalysis.icy</groupId>
         <artifactId>pom-icy</artifactId>
-        <version>2.2.0</version>
+        <version>3.0.0-a.1</version>
     </parent>
 
     <!-- Project Information -->
     <artifactId>intensity-projection</artifactId>
-    <version>2.0.0</version>
+    <version>2.0.0-a.1</version>
 
     <name>Intensity Projection plugin</name>
     <description>Intensity projection along depth or time with multiple algorithms: mean, max, median, variance, standard deviation, saturated sum. Projection can be restricted to ROI.</description>
@@ -47,15 +47,11 @@
         </developer>
     </developers>
 
-    <!-- List of project's dependencies -->
     <dependencies>
-        <!-- The EzPlug library, simplifies writing UI for Icy plugins. -->
         <dependency>
             <groupId>org.bioimageanalysis.icy</groupId>
             <artifactId>ezplug</artifactId>
         </dependency>
-
-        <!-- The EzPlug library, simplifies writing UI for Icy plugins. -->
         <dependency>
             <groupId>org.bioimageanalysis.icy</groupId>
             <artifactId>protocols</artifactId>
@@ -66,8 +62,7 @@
     <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>
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
index bc9d7dede3fbc424fb0c66b2bc54fad1c63f8c7d..a3a3799a0965964df0cd98c7bb759d8032266bee 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,6 +18,9 @@
 
 package org.bioimageanalysis.icy.image.projection;
 
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Indicates the direction on which the projection should be performed.
  *
@@ -28,12 +31,14 @@ public enum ProjectionAxis {
 
     private final String name;
 
-    ProjectionAxis(final String name) {
+    @Contract(pure = true)
+    ProjectionAxis(final @NotNull String name) {
         this.name = name;
     }
 
+    @Contract(pure = true)
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return name;
     }
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
index d96316f941189c7b9f06b82dfe50167a3b817387..e744a89f3c49892846169abd492ca3aceef1845c 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,15 +18,17 @@
 
 package org.bioimageanalysis.icy.image.projection;
 
-import icy.image.IcyBufferedImage;
-import icy.image.IcyBufferedImageCursor;
-import icy.math.ArrayMath;
-import icy.roi.ROI;
-import icy.sequence.Sequence;
-import icy.sequence.VolumetricImage;
-import icy.type.DataType;
-import icy.util.OMEUtil;
+import org.bioimageanalysis.icy.common.math.ArrayMath;
+import org.bioimageanalysis.icy.common.type.DataType;
 import org.bioimageanalysis.icy.image.projection.util.MessageProgressListener;
+import org.bioimageanalysis.icy.model.OMEUtil;
+import org.bioimageanalysis.icy.model.image.IcyBufferedImage;
+import org.bioimageanalysis.icy.model.image.IcyBufferedImageCursor;
+import org.bioimageanalysis.icy.model.roi.ROI;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.bioimageanalysis.icy.model.sequence.VolumetricImage;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.*;
 import java.util.concurrent.*;
@@ -38,7 +40,6 @@ import java.util.concurrent.*;
  * @author Daniel Felipe Gonzalez Obando
  */
 public class ProjectionCalculator implements Callable<Sequence> {
-
     /**
      * Builder class for {@link ProjectionCalculator}s. This allows users to specify the sequence, ROIs, axis and operation before creating the calculator.
      *
@@ -57,6 +58,7 @@ public class ProjectionCalculator implements Callable<Sequence> {
          *
          * @param s Target sequence for the projection.
          */
+        @Contract("null -> fail")
         public Builder(final Sequence s) {
             if (s == null || s.isEmpty())
                 throw new IllegalArgumentException("Input sequence is null or empty.");
@@ -138,6 +140,7 @@ public class ProjectionCalculator implements Callable<Sequence> {
     private final List<MessageProgressListener> progressListeners;
     private boolean computed;
 
+    @Contract(pure = true)
     private ProjectionCalculator() {
         progressListeners = new ArrayList<>();
         computed = false;
@@ -184,15 +187,17 @@ public class ProjectionCalculator implements Callable<Sequence> {
     private Sequence result;
 
     private void createResultSequence() throws InterruptedException {
-        result = new Sequence(OMEUtil.createOMEXMLMetadata(seq.getOMEXMLMetadata()),
-                op + " " + axis + "-projection of " + seq.getName());
+        result = new Sequence(
+                OMEUtil.createOMEXMLMetadata(seq.getOMEXMLMetadata()),
+                op + " " + axis + "-projection of " + seq.getName()
+        );
 
         final int width = axis == ProjectionAxis.X ? 1 : seq.getSizeX();
         final int height = axis == ProjectionAxis.Y ? 1 : seq.getSizeY();
         final int depth = axis == ProjectionAxis.Z ? 1 : seq.getSizeZ();
         final int frames = axis == ProjectionAxis.T ? 1 : seq.getSizeT();
         final int channels = axis == ProjectionAxis.C ? 1 : seq.getSizeC();
-        final DataType dataType = seq.getDataType_();
+        final DataType dataType = seq.getDataType();
 
         for (int t = 0; t < frames; t++) {
             final VolumetricImage vol = new VolumetricImage();
@@ -209,18 +214,14 @@ public class ProjectionCalculator implements Callable<Sequence> {
             for (int c = 0; c < seq.getSizeC(); c++)
                 result.getColorModel().setColorMap(c, seq.getColorMap(c), true);
         }
-        notifyProgress(0.01, "Result sequence instatiated");
+        notifyProgress(0.01, "Result sequence instantiated");
     }
 
     private void computeProjection() throws InterruptedException, ExecutionException {
 
         switch (axis) {
             case X:
-                //startProjectionOnPlane();
-                //break;
             case C:
-                //startProjectionOnPlane();
-                //break;
             case Y:
                 startProjectionOnPlane();
                 break;
@@ -252,54 +253,52 @@ public class ProjectionCalculator implements Callable<Sequence> {
             }
         }
 
-        final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
-
-        final List<Future<IcyBufferedImage>> futures = new ArrayList<>(frames * depth);
-        for (t = 0; t < frames; t++) {
-            tOff = t * depth;
-            for (z = 0; z < depth; z++) {
-                final Callable<IcyBufferedImage> task;
-                switch (axis) {
-                    case X:
-                        task = getImageXProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+        try (final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))) {
+            final List<Future<IcyBufferedImage>> futures = new ArrayList<>(frames * depth);
+            for (t = 0; t < frames; t++) {
+                tOff = t * depth;
+                for (z = 0; z < depth; z++) {
+                    final Callable<IcyBufferedImage> task = switch (axis) {
+                        case X -> getImageXProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
                                 height, width);
-                        break;
-                    case Y:
-                        task = getImageYProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+                        case Y -> getImageYProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
                                 height, width);
-                        break;
-                    case C:
-                        task = getImageCProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+                        case C -> getImageCProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
                                 height, width);
-                        break;
-                    default:
-                        throw new IllegalArgumentException("Wrong axis parameter");
+                        default -> throw new IllegalArgumentException("Wrong axis parameter");
+                    };
+                    futures.add(generalTaskPool.submit(task));
                 }
-                futures.add(generalTaskPool.submit(task));
             }
-        }
 
-        generalTaskPool.shutdown();
+            generalTaskPool.shutdown();
 
-        int pos;
-        for (pos = 0, t = 0; t < frames; t++) {
-            for (z = 0; z < depth; z++) {
-                try {
-                    futures.get(pos++).get();
-                }
-                catch (final InterruptedException | ExecutionException e) {
-                    generalTaskPool.shutdownNow();
-                    throw e;
+            int pos;
+            for (pos = 0, t = 0; t < frames; t++) {
+                for (z = 0; z < depth; z++) {
+                    try {
+                        futures.get(pos++).get();
+                    }
+                    catch (final InterruptedException | ExecutionException e) {
+                        generalTaskPool.shutdownNow();
+                        throw e;
+                    }
                 }
+                notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
             }
-            notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
         }
     }
 
-    private Callable<IcyBufferedImage> getImageXProjectionTask(final IcyBufferedImage inputPlane,
-                                                               final IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height,
-                                                               final int width) {
+    @Contract(pure = true)
+    private @NotNull Callable<IcyBufferedImage> getImageXProjectionTask(
+            final IcyBufferedImage inputPlane,
+            final IcyBufferedImage outputPlane,
+            final int t,
+            final int z,
+            final int channels,
+            final int height,
+            final int width
+    ) {
         return () -> {
             int channel, line, column, elementsCount;
             final double[] elements = new double[width];
@@ -336,9 +335,16 @@ public class ProjectionCalculator implements Callable<Sequence> {
         };
     }
 
-    private Callable<IcyBufferedImage> getImageYProjectionTask(final IcyBufferedImage inputPlane,
-                                                               final IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height,
-                                                               final int width) {
+    @Contract(pure = true)
+    private @NotNull Callable<IcyBufferedImage> getImageYProjectionTask(
+            final IcyBufferedImage inputPlane,
+            final IcyBufferedImage outputPlane,
+            final int t,
+            final int z,
+            final int channels,
+            final int height,
+            final int width
+    ) {
         return () -> {
             int channel, line, column, elementsCount;
             final double[] elements = new double[height];
@@ -376,8 +382,16 @@ public class ProjectionCalculator implements Callable<Sequence> {
         };
     }
 
-    private Callable<IcyBufferedImage> getImageCProjectionTask(final IcyBufferedImage inputPlane,
-                                                               final IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height, final int width) {
+    @Contract(pure = true)
+    private @NotNull Callable<IcyBufferedImage> getImageCProjectionTask(
+            final IcyBufferedImage inputPlane,
+            final IcyBufferedImage outputPlane,
+            final int t,
+            final int z,
+            final int channels,
+            final int height,
+            final int width
+    ) {
         return () -> {
             int channel, line, column, elementsCount;
             final double[] elements = new double[height];
@@ -430,46 +444,57 @@ public class ProjectionCalculator implements Callable<Sequence> {
         final int frames = seq.getSizeT();
         final int channels = seq.getSizeC();
 
-        final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
-        final ForkJoinPool volumeTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
-        int t, c;
-        VolumetricImage inputVolume, resultVolume;
-        final List<Future<VolumetricImage>> futures = new ArrayList<>(frames * depth);
-        for (t = 0; t < frames; t++) {
-            inputVolume = seq.getVolumetricImage(t);
-            resultVolume = result.getVolumetricImage(t);
-            for (c = 0; c < channels; c++) {
-                futures.add(generalTaskPool.submit(getImageZProjectionTask(t, c, inputVolume, resultVolume, channels,
-                        height, width, depth, volumeTaskPool)));
+        try (
+                final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
+                final ForkJoinPool volumeTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))
+        ) {
+            int t, c;
+            VolumetricImage inputVolume, resultVolume;
+            final List<Future<VolumetricImage>> futures = new ArrayList<>(frames * depth);
+            for (t = 0; t < frames; t++) {
+                inputVolume = seq.getVolumetricImage(t);
+                resultVolume = result.getVolumetricImage(t);
+                for (c = 0; c < channels; c++) {
+                    futures.add(generalTaskPool.submit(getImageZProjectionTask(t, c, inputVolume, resultVolume, channels,
+                            height, width, depth, volumeTaskPool)));
 
+                }
             }
-        }
 
-        generalTaskPool.shutdown();
+            generalTaskPool.shutdown();
 
-        try {
-            int pos;
-            for (pos = 0, t = 0; t < frames; t++) {
-                for (c = 0; c < channels; c++) {
-                    futures.get(pos++).get();
+            try {
+                int pos;
+                for (pos = 0, t = 0; t < frames; t++) {
+                    for (c = 0; c < channels; c++) {
+                        futures.get(pos++).get();
+                    }
+                    notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
                 }
-                notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
+            }
+            catch (final InterruptedException | ExecutionException e) {
+                generalTaskPool.shutdownNow();
+                throw e;
+            }
+            finally {
+                volumeTaskPool.shutdown();
             }
         }
-        catch (final InterruptedException | ExecutionException e) {
-            generalTaskPool.shutdownNow();
-            throw e;
-        }
-        finally {
-            volumeTaskPool.shutdown();
-        }
-
     }
 
-    private Callable<VolumetricImage> getImageZProjectionTask(final int t, final int c, final VolumetricImage inputVolume,
-                                                              final VolumetricImage resultVolume, final int channels, final int height, final int width, final int depth, final ForkJoinPool volumeTaskPool) {
+    @Contract(pure = true)
+    @SuppressWarnings("StatementWithEmptyBody")
+    private @NotNull Callable<VolumetricImage> getImageZProjectionTask(
+            final int t,
+            final int c,
+            final VolumetricImage inputVolume,
+            final VolumetricImage resultVolume,
+            final int channels,
+            final int height,
+            final int width,
+            final int depth,
+            final ForkJoinPool volumeTaskPool
+    ) {
         return () -> {
             int slice, line, column;
 
@@ -517,7 +542,10 @@ public class ProjectionCalculator implements Callable<Sequence> {
 
             try {
                 for (line = 0; line < height; line++) {
-                    final double[] lineElements = futureLines.get(line).get();
+                    notifyProgress(0.01 + 0.99 * ((double) line / height), axis + "-projection: Processed line=" + line);
+                    final Future<double[]> future = futureLines.get(line);
+                    while (!future.isDone()) ;
+                    final double[] lineElements = future.get();
                     for (column = 0; column < width; column++) {
                         outputCursor.setSafe(column, line, c, lineElements[column]);
                     }
@@ -539,46 +567,58 @@ public class ProjectionCalculator implements Callable<Sequence> {
         final int frames = seq.getSizeT();
         final int channels = seq.getSizeC();
 
-        final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
-        final ForkJoinPool temporalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
-        int z, c;
-        Sequence inputS, resultS;
-        final List<Future<Sequence>> futures = new ArrayList<>(depth);
-        for (z = 0; z < depth; z++) {
-            for (c = 0; c < channels; c++) {
-                inputS = seq;
-                resultS = result;
-                {
-                    futures.add(generalTaskPool.submit(getImageTProjectionTask(z, c, inputS, resultS, channels, height,
-                            width, depth, frames, temporalTaskPool)));
+        try (
+                final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
+                final ForkJoinPool temporalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))
+        ) {
+            int z, c;
+            Sequence inputS, resultS;
+            final List<Future<Sequence>> futures = new ArrayList<>(depth);
+            for (z = 0; z < depth; z++) {
+                for (c = 0; c < channels; c++) {
+                    inputS = seq;
+                    resultS = result;
+                    {
+                        futures.add(generalTaskPool.submit(getImageTProjectionTask(z, c, inputS, resultS, channels, height, width, depth, frames, temporalTaskPool)));
+                    }
                 }
             }
-        }
 
-        generalTaskPool.shutdown();
+            generalTaskPool.shutdown();
 
-        try {
-            int pos;
-            for (pos = 0, z = 0; z < depth; z++) {
-                for (c = 0; c < channels; c++) {
-                    futures.get(pos++).get();
+            try {
+                int pos;
+                for (pos = 0, z = 0; z < depth; z++) {
+                    for (c = 0; c < channels; c++) {
+                        futures.get(pos++).get();
+                    }
+                    notifyProgress(0.01 + 0.99 * ((double) z / depth), axis + "-projection: Processed z=" + z);
                 }
-                notifyProgress(0.01 + 0.99 * ((double) z / depth), axis + "-projection: Processed z=" + z);
             }
-        }
-        catch (final InterruptedException | ExecutionException e) {
-            generalTaskPool.shutdownNow();
-            throw e;
-        }
-        finally {
-            temporalTaskPool.shutdown();
+            catch (final InterruptedException | ExecutionException e) {
+                generalTaskPool.shutdownNow();
+                throw e;
+            }
+            finally {
+                temporalTaskPool.shutdown();
+            }
         }
     }
 
-    private Callable<Sequence> getImageTProjectionTask(final int z, final int c, final Sequence inputS, final Sequence resultS, final int channels,
-                                                       final int height, final int width, final int depth, final int frames, final ForkJoinPool temporalTaskPool) {
+    @Contract(pure = true)
+    @SuppressWarnings("StatementWithEmptyBody")
+    private @NotNull Callable<Sequence> getImageTProjectionTask(
+            final int z,
+            final int c,
+            final Sequence inputS,
+            final Sequence resultS,
+            final int channels,
+            final int height,
+            final int width,
+            final int depth,
+            final int frames,
+            final ForkJoinPool temporalTaskPool
+    ) {
         return () -> {
             int frame, line, column;
             final IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[frames];
@@ -624,7 +664,10 @@ public class ProjectionCalculator implements Callable<Sequence> {
 
             try {
                 for (line = 0; line < height; line++) {
-                    final double[] lineElements = futureLines.get(line).get();
+                    notifyProgress(0.01 + 0.99 * ((double) line / height), axis + "-projection: Processed line=" + line);
+                    final Future<double[]> future = futureLines.get(line);
+                    while (!future.isDone()) ;
+                    final double[] lineElements = future.get();
                     for (column = 0; column < width; column++) {
                         outputCursor.setSafe(column, line, c, lineElements[column]);
                     }
@@ -663,6 +706,7 @@ public class ProjectionCalculator implements Callable<Sequence> {
         progressListeners.forEach(l -> l.onProgress(progress, message));
     }
 
+    @Contract(pure = true)
     private Sequence getResultSequence() {
         return result;
     }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
index 49f4cfdf65951b6ff764e2e431ae36823ac7f2b7..12ba559034dd0580da8c37b3b1640525319b8f84 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,22 +18,32 @@
 
 package org.bioimageanalysis.icy.image.projection;
 
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Represents the operation to be applied when performing an intensity projection on a sequence.
  *
  * @author Daniel Felipe Gonzalez Obando
  */
 public enum ProjectionOperationType {
-    MAX("Maximum"), MEAN("Average"), MED("Median"), MIN("Minimum"), STD("Standard Deviation"), SATSUM("Saturated Sum");
+    MAX("Maximum"),
+    MEAN("Average"),
+    MED("Median"),
+    MIN("Minimum"),
+    STD("Standard Deviation"),
+    SATSUM("Saturated Sum");
 
     private final String description;
 
-    ProjectionOperationType(final String description) {
+    @Contract(pure = true)
+    ProjectionOperationType(final @NotNull String description) {
         this.description = description;
     }
 
+    @Contract(pure = true)
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return description;
     }
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
index a1e70cd553497e12a946cf6de94c39fb767ea36f..ef2088abb3f70826c2703ce42cf5ee7895199990 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,6 +18,8 @@
 
 package org.bioimageanalysis.icy.image.projection.util;
 
+import org.jetbrains.annotations.Nullable;
+
 /**
  * A Progress event listener that handles events associating a value and a message to them.
  *
@@ -30,5 +32,5 @@ public interface MessageProgressListener {
      * @param progress The progress value from 0.0 to 1.0. {@link Double#NaN} value should be taken as no change and -1.0 as indeterminate progress.
      * @param message  The event message. A null value should be taken as no change on the message.
      */
-    void onProgress(double progress, String message);
+    void onProgress(double progress, @Nullable String message);
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
index 166ae994caf8c91eef57b3d0c1b91a93929589b1..a0255efa05d70f7ab50e446f75dc133eb20805e6 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,7 +18,8 @@
 
 package org.bioimageanalysis.icy.image.projection.util;
 
-import icy.sequence.Sequence;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -40,7 +41,7 @@ public class SequenceCursor {
      *
      * @param seq Sequence.
      */
-    public SequenceCursor(final Sequence seq) {
+    public SequenceCursor(final @NotNull Sequence seq) {
         this.seq = seq;
         this.volumeCursors = new VolumetricImageCursor[seq.getSizeT()];
         this.sequenceChanged = new AtomicBoolean();
@@ -75,8 +76,7 @@ public class SequenceCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void set(final int x, final int y, final int z, final int t, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void set(final int x, final int y, final int z, final int t, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getVolumeCursor(t).set(x, y, z, c, val);
         sequenceChanged.set(true);
     }
@@ -94,8 +94,7 @@ public class SequenceCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void setSafe(final int x, final int y, final int z, final int t, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void setSafe(final int x, final int y, final int z, final int t, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getVolumeCursor(t).setSafe(x, y, z, c, val);
         sequenceChanged.set(true);
     }
@@ -103,7 +102,7 @@ public class SequenceCursor {
     private int currentT;
     private VolumetricImageCursor currentCursor;
 
-    private synchronized VolumetricImageCursor getVolumeCursor(final int t) throws IndexOutOfBoundsException {
+    private synchronized @NotNull VolumetricImageCursor getVolumeCursor(final int t) throws IndexOutOfBoundsException {
         if (currentT != t) {
             if (volumeCursors[t] == null) {
                 volumeCursors[t] = new VolumetricImageCursor(seq, t);
@@ -129,7 +128,7 @@ public class SequenceCursor {
     }
 
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return "last T=" + currentT + " " + ((currentCursor != null) ? currentCursor.toString() : "");
     }
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
index 7d3f1083a251bfec37d79023b96be84ddf3f8ed0..c12d681b8b7ed00c5b40ac9f17a0f096ebfc19ab 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,9 +18,10 @@
 
 package org.bioimageanalysis.icy.image.projection.util;
 
-import icy.image.IcyBufferedImageCursor;
-import icy.sequence.Sequence;
-import icy.sequence.VolumetricImage;
+import org.bioimageanalysis.icy.model.image.IcyBufferedImageCursor;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.bioimageanalysis.icy.model.sequence.VolumetricImage;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -44,7 +45,7 @@ public class VolumetricImageCursor {
      *
      * @param vol Target volume.
      */
-    public VolumetricImageCursor(final VolumetricImage vol) {
+    public VolumetricImageCursor(final @NotNull VolumetricImage vol) {
         this.vol = vol;
         planeCursors = new IcyBufferedImageCursor[vol.getSize()];
         volumeChanged = new AtomicBoolean(false);
@@ -57,7 +58,7 @@ public class VolumetricImageCursor {
      * @param seq Target sequence.
      * @param t   Time position where the volume is located in {@code seq}.
      */
-    public VolumetricImageCursor(final Sequence seq, final int t) {
+    public VolumetricImageCursor(final @NotNull Sequence seq, final int t) {
         this(seq.getVolumetricImage(t));
     }
 
@@ -88,8 +89,7 @@ public class VolumetricImageCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void set(final int x, final int y, final int z, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void set(final int x, final int y, final int z, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getPlaneCursor(z).set(x, y, c, val);
         volumeChanged.set(true);
     }
@@ -106,8 +106,7 @@ public class VolumetricImageCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void setSafe(final int x, final int y, final int z, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void setSafe(final int x, final int y, final int z, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getPlaneCursor(z).setSafe(x, y, c, val);
         volumeChanged.set(true);
     }
@@ -115,7 +114,7 @@ public class VolumetricImageCursor {
     private IcyBufferedImageCursor currentCursor;
     private int currentZ;
 
-    private synchronized IcyBufferedImageCursor getPlaneCursor(final int z) throws IndexOutOfBoundsException {
+    private synchronized @NotNull IcyBufferedImageCursor getPlaneCursor(final int z) throws IndexOutOfBoundsException {
         if (currentZ != z) {
             if (planeCursors[z] == null) {
                 planeCursors[z] = new IcyBufferedImageCursor(vol.getImage(z));
diff --git a/src/main/java/plugins/adufour/projection/Projection.java b/src/main/java/plugins/adufour/projection/Projection.java
index 6a5e9a0001048b3668e9bcf51e087ee7b7911b74..b8640ee496820abda39be2e759e2511ea813f769 100644
--- a/src/main/java/plugins/adufour/projection/Projection.java
+++ b/src/main/java/plugins/adufour/projection/Projection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,16 +18,17 @@
 
 package plugins.adufour.projection;
 
-import icy.main.Icy;
-import icy.plugin.PluginLauncher;
-import icy.plugin.PluginLoader;
-import icy.roi.ROI;
-import icy.sequence.Sequence;
-import icy.system.IcyExceptionHandler;
-import icy.system.IcyHandledException;
+import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon;
+import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName;
 import org.bioimageanalysis.icy.image.projection.ProjectionAxis;
 import org.bioimageanalysis.icy.image.projection.ProjectionCalculator;
 import org.bioimageanalysis.icy.image.projection.ProjectionOperationType;
+import org.bioimageanalysis.icy.model.roi.ROI;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.bioimageanalysis.icy.system.logging.IcyLogger;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import plugins.adufour.blocks.lang.Block;
 import plugins.adufour.blocks.util.VarList;
 import plugins.adufour.ezplug.*;
@@ -36,21 +37,29 @@ import plugins.adufour.vars.lang.VarSequence;
 import java.util.Collections;
 import java.util.List;
 
+@IcyPluginName("Intensity Projection")
+@IcyPluginIcon(path = "/plugins/adufour/projection/icon/icon_32.png")
 public class Projection extends EzPlug implements Block, EzStoppable {
     public enum ProjectionDirection {
         Z, T, C, Y, X
     }
 
     public enum ProjectionType {
-        MAX("Maximum"), MEAN("Average"), MED("Median"), MIN("Minimum"), STD("Standard Deviation"),
+        MAX("Maximum"),
+        MEAN("Average"),
+        MED("Median"),
+        MIN("Minimum"),
+        STD("Standard Deviation"),
         SATSUM("Saturated Sum");
 
         private final String description;
 
+        @Contract(pure = true)
         ProjectionType(final String description) {
             this.description = description;
         }
 
+        @Contract(pure = true)
         @Override
         public String toString() {
             return description;
@@ -79,7 +88,7 @@ public class Projection extends EzPlug implements Block, EzStoppable {
     }
 
     @Override
-    public void declareInput(final VarList inputMap) {
+    public void declareInput(final @NotNull VarList inputMap) {
         initCommonVars();
         inputMap.add("input", input.getVariable());
         inputMap.add("projection direction", projectionDir.getVariable());
@@ -96,7 +105,7 @@ public class Projection extends EzPlug implements Block, EzStoppable {
     }
 
     @Override
-    public void declareOutput(final VarList outputMap) {
+    public void declareOutput(final @NotNull VarList outputMap) {
         output = new VarSequence("projected sequence", null);
         outputMap.add("projection output", output);
     }
@@ -113,8 +122,8 @@ public class Projection extends EzPlug implements Block, EzStoppable {
             computeCalculator();
         }
         catch (final Exception e) {
-            IcyExceptionHandler.showErrorMessage(e, true);
-            throw new IcyHandledException("Error while projecting: " + e.getMessage(), e);
+            IcyLogger.error(this.getClass(), e, "Error while projecting.");
+            return;
         }
         setOutput();
         internalClean();
@@ -132,42 +141,27 @@ public class Projection extends EzPlug implements Block, EzStoppable {
         rois = restrictToROI.getValue(true) ? inputSequence.getROIs() : Collections.emptyList();
     }
 
-    private static ProjectionAxis getAxis(final ProjectionDirection direction) {
-        switch (direction) {
-            case X:
-                return ProjectionAxis.X;
-            case Y:
-                return ProjectionAxis.Y;
-            case C:
-                return ProjectionAxis.C;
-            case Z:
-                return ProjectionAxis.Z;
-            case T:
-                return ProjectionAxis.T;
-
-            default:
-                throw new IllegalArgumentException("" + direction);
-        }
+    @Contract(pure = true)
+    private static @NotNull ProjectionAxis getAxis(final @NotNull ProjectionDirection direction) {
+        return switch (direction) {
+            case X -> ProjectionAxis.X;
+            case Y -> ProjectionAxis.Y;
+            case C -> ProjectionAxis.C;
+            case Z -> ProjectionAxis.Z;
+            case T -> ProjectionAxis.T;
+        };
     }
 
-    private static ProjectionOperationType getOperation(final ProjectionType type) {
-        switch (type) {
-            case MAX:
-                return ProjectionOperationType.MAX;
-            case MEAN:
-                return ProjectionOperationType.MEAN;
-            case MED:
-                return ProjectionOperationType.MED;
-            case MIN:
-                return ProjectionOperationType.MIN;
-            case SATSUM:
-                return ProjectionOperationType.SATSUM;
-            case STD:
-                return ProjectionOperationType.STD;
-
-            default:
-                throw new IllegalArgumentException("" + type);
-        }
+    @Contract(pure = true)
+    private static @NotNull ProjectionOperationType getOperation(final @NotNull ProjectionType type) {
+        return switch (type) {
+            case MAX -> ProjectionOperationType.MAX;
+            case MEAN -> ProjectionOperationType.MEAN;
+            case MED -> ProjectionOperationType.MED;
+            case MIN -> ProjectionOperationType.MIN;
+            case SATSUM -> ProjectionOperationType.SATSUM;
+            case STD -> ProjectionOperationType.STD;
+        };
     }
 
     private Sequence resultSequence;
@@ -188,7 +182,7 @@ public class Projection extends EzPlug implements Block, EzStoppable {
         }
     }
 
-    private void onProgress(final double progress, final String message) {
+    private void onProgress(final double progress, final @Nullable String message) {
         if (!isHeadLess()) {
             getUI().setProgressBarValue(progress);
             getUI().setProgressBarMessage(message);
@@ -242,7 +236,11 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence zProjection(final Sequence in, final ProjectionType projection, final boolean multiThread) throws Exception {
+    public static Sequence zProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread
+    ) throws Exception {
         return zProjection(in, projection, multiThread, false);
     }
 
@@ -259,10 +257,19 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence zProjection(final Sequence in, final ProjectionType projection, final boolean multiThread, final boolean restrictToROI) throws Exception {
+    public static Sequence zProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread,
+            final boolean restrictToROI
+    ) throws Exception {
         final List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList();
-        final ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.Z)
-                .operation(getOperation(projection)).addRois(rois).build();
+        final ProjectionCalculator calculator = new ProjectionCalculator
+                .Builder(in)
+                .axis(ProjectionAxis.Z)
+                .operation(getOperation(projection))
+                .addRois(rois)
+                .build();
         return calculator.call();
     }
 
@@ -277,7 +284,11 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence tProjection(final Sequence in, final ProjectionType projection, final boolean multiThread) throws Exception {
+    public static Sequence tProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread
+    ) throws Exception {
         return tProjection(in, projection, multiThread, false);
     }
 
@@ -294,23 +305,19 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence tProjection(final Sequence in, final ProjectionType projection, final boolean multiThread, final boolean restrictToROI) throws Exception {
+    public static Sequence tProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread,
+            final boolean restrictToROI
+    ) throws Exception {
         final List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList();
-        final ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.T)
-                .operation(getOperation(projection)).addRois(rois).build();
+        final ProjectionCalculator calculator = new ProjectionCalculator
+                .Builder(in)
+                .axis(ProjectionAxis.T)
+                .operation(getOperation(projection))
+                .addRois(rois)
+                .build();
         return calculator.call();
     }
-
-    /**
-     * Main method used for testing.
-     *
-     * @param args Input arguments.
-     */
-    public static void main(final String[] args) {
-        // start icy
-        Icy.main(args);
-
-        // then start plugin
-        PluginLauncher.start(PluginLoader.getPlugin(Projection.class.getName()));
-    }
 }
diff --git a/src/main/resources/plugins/adufour/projection/icon/icon_32.png b/src/main/resources/plugins/adufour/projection/icon/icon_32.png
new file mode 100644
index 0000000000000000000000000000000000000000..5dc83272bac2de94fcc0cd3feba5bdb227752022
Binary files /dev/null and b/src/main/resources/plugins/adufour/projection/icon/icon_32.png differ