diff --git a/.gitignore b/.gitignore
index 025e963ead2aa54de670d6e933032e42ea68e61d..21f1a7f532cfb4cb24ebce26322eeb1b9b3adcb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
-/target
+target/
+workspace/
+.settings/
+setting.xml
 .project
 .classpath
 *.prefs
 *.jardesc
-*.jar
-.settings/
\ No newline at end of file
+*.jar
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 7529cf8de4fec4071bac702e2f73b6096875f4fd..b4a935b0c1b590678282c2183e68a7210caf0240 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,12 +8,12 @@
     <parent>
         <groupId>org.bioimageanalysis.icy</groupId>
         <artifactId>parent-pom-plugin</artifactId>
-        <version>1.0.1</version>
+        <version>1.0.4</version>
     </parent>
 
     <!-- Project Information -->
     <artifactId>intensity-projection</artifactId>
-    <version>1.7.0-SNAPSHOT</version>
+    <version>1.8.0-SNAPSHOT</version>
     
     <packaging>jar</packaging>
 
@@ -77,7 +77,6 @@
         <dependency>
             <groupId>org.bioimageanalysis.icy</groupId>
             <artifactId>protocols</artifactId>
-            <version>${protocols.version}</version>
         </dependency>
     </dependencies>
 
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
new file mode 100644
index 0000000000000000000000000000000000000000..9531fe89028bdb900801e3bb2e913dc8655a0039
--- /dev/null
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
@@ -0,0 +1,24 @@
+package org.bioimageanalysis.icy.image.projection;
+
+/**
+ * Indicates the direction on which the projection should be performed.
+ * 
+ * @author Daniel Felipe Gonzalez Obando
+ */
+public enum ProjectionAxis
+{
+    X("x"), Y("y"), Z("z"), T("t"), C("c");
+
+    private String name;
+
+    ProjectionAxis(String name)
+    {
+        this.name = name;
+    }
+
+    @Override
+    public 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
new file mode 100644
index 0000000000000000000000000000000000000000..3f277106e31393a16fa48d180c4baed9a9e28b30
--- /dev/null
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
@@ -0,0 +1,788 @@
+package org.bioimageanalysis.icy.image.projection;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.Future;
+
+import org.bioimageanalysis.icy.image.projection.util.MessageProgressListener;
+
+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;
+
+/**
+ * Calculates the projection of a sequence along the specified angle and using the specified operation. The projection can be applied to a given set of ROIs,
+ * only pixels contained in the ROIs will be taken into account to compute the projection.
+ * 
+ * @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.
+     * 
+     * @author Daniel Felipe Gonzalez Obando
+     */
+    public static class Builder
+    {
+        private Sequence s;
+        private List<ROI> rois;
+        private ProjectionAxis axis;
+        private ProjectionOperationType op;
+
+        /**
+         * Creates a builder instance with the target sequence.
+         * By default, the ROI list is set to empty, the projection axis is set to {@link ProjectionAxis#Z} and the operation is set to
+         * {@link ProjectionOperationType#MAX}.
+         * 
+         * @param s
+         *        Target sequence for the projection.
+         */
+        public Builder(Sequence s)
+        {
+            if (s == null || s.isEmpty())
+                throw new IllegalArgumentException("Input sequence is null or empty.");
+
+            this.s = s;
+            this.rois = new ArrayList<ROI>();
+            this.axis = ProjectionAxis.Z;
+            this.op = ProjectionOperationType.MAX;
+        }
+
+        /**
+         * Adds a ROI to the projection constraints.
+         * 
+         * @param roi
+         *        The ROI.
+         * @return This builder instance once the ROI is added.
+         */
+        public Builder addRoi(ROI roi)
+        {
+            rois.add(roi);
+            return this;
+        }
+
+        /**
+         * Adds a collection of ROIs to the projection constraints.
+         * 
+         * @param rois
+         *        The collection of ROIs to add.
+         * @return This builder instance once the ROIs have been added.
+         */
+        public Builder addRois(Collection<? extends ROI> rois)
+        {
+            this.rois.addAll(rois);
+            return this;
+        }
+
+        /**
+         * Sets the projection axis.
+         * If the argument is null the axis is set to {@link ProjectionAxis#Z}.
+         * 
+         * @param axis
+         *        Axis.
+         * @return This builder instance after setting the axis.
+         */
+        public Builder axis(ProjectionAxis axis)
+        {
+            if (axis == null)
+                this.axis = ProjectionAxis.Z;
+            else
+                this.axis = axis;
+
+            return this;
+        }
+
+        /**
+         * Sets the operation used during the projection.
+         * If the argument is null, then the operation is set to {@link ProjectionOperationType#MAX}
+         * 
+         * @param op
+         *        Operation to apply.
+         * @return This builder instance after setting the operation.
+         */
+        public Builder operation(ProjectionOperationType op)
+        {
+            if (op == null)
+                this.op = ProjectionOperationType.MAX;
+            else
+                this.op = op;
+
+            return this;
+        }
+
+        /**
+         * Builds the calculator using the parameters from this instance.
+         * 
+         * @return The projection calculator ready to be called.
+         */
+        public ProjectionCalculator build()
+        {
+            ProjectionCalculator calculator = new ProjectionCalculator();
+            calculator.seq = s;
+            calculator.rois = rois;
+            calculator.axis = axis;
+            calculator.op = op;
+            return calculator;
+
+        }
+    }
+
+    private Sequence seq;
+    private List<ROI> rois;
+    private ProjectionAxis axis;
+    private ProjectionOperationType op;
+    private List<MessageProgressListener> progressListeners;
+    private boolean computed;
+
+    private ProjectionCalculator()
+    {
+        progressListeners = new ArrayList<>();
+        computed = false;
+    }
+
+    /**
+     * Adds a listener for progress events.
+     * 
+     * @param listener
+     *        Progress event listener
+     */
+    public void addProgressListener(MessageProgressListener listener)
+    {
+        progressListeners.add(listener);
+    }
+
+    /**
+     * Removes a listener for progress events.
+     * 
+     * @param listener
+     *        Progress event listener
+     */
+    public void removeProgressListener(MessageProgressListener listener)
+    {
+        progressListeners.remove(listener);
+    }
+
+    /**
+     * Starts the projection if the calculator has not already computed the result.
+     */
+    @Override
+    public Sequence call() throws Exception
+    {
+        try
+        {
+            if (!computed)
+            {
+                createResultSequence();
+                result.beginUpdate();
+                computeProjection();
+                result.endUpdate();
+                computed = true;
+            }
+            return getResultSequence();
+        }
+        finally
+        {
+            notifyProgress(1, "Projection finished");
+        }
+    }
+
+    private Sequence result;
+
+    private void createResultSequence() throws InterruptedException
+    {
+        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_();
+
+        for (int t = 0; t < frames; t++)
+        {
+            VolumetricImage vol = new VolumetricImage();
+            for (int z = 0; z < depth; z++)
+            {
+                if (Thread.interrupted())
+                    throw new InterruptedException();
+                vol.setImage(z, new IcyBufferedImage(width, height, channels, dataType));
+            }
+            result.addVolumetricImage(t, vol);
+            notifyProgress(Math.max(0.01, t * (0.1 / frames)), "Creating result sequence t=" + t);
+        }
+
+        if (axis != ProjectionAxis.C)
+        {
+            for (int c = 0; c < seq.getSizeC(); c++)
+                result.getColorModel().setColorMap(c, seq.getColorMap(c), true);
+        }
+        notifyProgress(0.01, "Result sequence instatiated");
+    }
+
+    private void computeProjection() throws InterruptedException, ExecutionException
+    {
+
+        switch (axis)
+        {
+            case X:
+                startProjectionOnPlane();
+                break;
+            case Y:
+                startProjectionOnPlane();
+                break;
+            case Z:
+                startZProjection();
+                break;
+            case T:
+                startTProjection();
+                break;
+            case C:
+                startProjectionOnPlane();
+                break;
+        }
+        notifyProgress(1, axis + "-projection done");
+    }
+
+    private void startProjectionOnPlane() throws InterruptedException, ExecutionException
+    {
+        final int frames = seq.getSizeT();
+        final int depth = seq.getSizeZ();
+        final int channels = seq.getSizeC();
+        final int height = seq.getSizeY();
+        final int width = seq.getSizeX();
+
+        final IcyBufferedImage[] inputPlanes = new IcyBufferedImage[frames * depth];
+        final IcyBufferedImage[] outputPlanes = new IcyBufferedImage[frames * depth];
+        int t, z, tOff;
+        for (t = 0; t < frames; t++)
+        {
+            tOff = t * depth;
+            for (z = 0; z < depth; z++)
+            {
+                inputPlanes[tOff + z] = seq.getImage(t, z);
+                outputPlanes[tOff + z] = result.getImage(t, z);
+            }
+        }
+
+        ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
+                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
+
+        List<Future<IcyBufferedImage>> futures = new ArrayList<>(frames * depth);
+        for (t = 0; t < frames; t++)
+        {
+            tOff = t * depth;
+            for (z = 0; z < depth; z++)
+            {
+                Callable<IcyBufferedImage> task;
+                switch (axis)
+                {
+                    case X:
+                        task = 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,
+                                height, width);
+                        break;
+                    case C:
+                        task = getImageCProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+                                height, width);
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Wrong axis parameter");
+                }
+                futures.add(generalTaskPool.submit(task));
+            }
+        }
+
+        generalTaskPool.shutdown();
+
+        int pos;
+        for (pos = 0, t = 0; t < frames; t++)
+        {
+            for (z = 0; z < depth; z++)
+            {
+                try
+                {
+                    futures.get(pos++).get();
+                }
+                catch (InterruptedException | ExecutionException e)
+                {
+                    generalTaskPool.shutdownNow();
+                    throw e;
+                }
+            }
+            notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
+        }
+    }
+
+    private Callable<IcyBufferedImage> getImageXProjectionTask(IcyBufferedImage inputPlane,
+            IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height,
+            final int width)
+    {
+        return () -> {
+            int channel, line, column, elementsCount;
+            double[] elements = new double[width];
+
+            IcyBufferedImageCursor inputCursor = new IcyBufferedImageCursor(inputPlane);
+            IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(outputPlane);
+            for (channel = 0; channel < channels; channel++)
+            {
+                for (line = 0; line < height; line++)
+                {
+                    elementsCount = 0;
+                    for (column = 0; column < width; column++)
+                    {
+                        if (rois.isEmpty())
+                        {
+                            elements[elementsCount++] = inputCursor.get(column, line, channel);
+                        }
+                        else
+                        {
+                            for (ROI roi : rois)
+                            {
+                                if (roi.contains(column, line, z, t, channel))
+                                {
+                                    elements[elementsCount++] = inputCursor.get(column, line, channel);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (elementsCount == 0)
+                    {
+                        outputCursor.setSafe(0, line, channel, 0d);
+                    }
+                    else
+                    {
+                        outputCursor.setSafe(0, line, channel, computeResVal(Arrays.copyOf(elements, elementsCount)));
+                    }
+
+                }
+            }
+            outputCursor.commitChanges();
+            return outputPlane;
+        };
+    }
+
+    private Callable<IcyBufferedImage> getImageYProjectionTask(IcyBufferedImage inputPlane,
+            IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height,
+            final int width)
+    {
+        return () -> {
+            int channel, line, column, elementsCount;
+            double[] elements = new double[height];
+
+            IcyBufferedImageCursor inputCursor = new IcyBufferedImageCursor(inputPlane);
+            IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(outputPlane);
+            for (channel = 0; channel < channels; channel++)
+            {
+                for (column = 0; column < width; column++)
+                {
+                    elementsCount = 0;
+
+                    for (line = 0; line < height; line++)
+                    {
+                        if (rois.isEmpty())
+                        {
+                            elements[elementsCount++] = inputCursor.get(column, line, channel);
+                        }
+                        else
+                        {
+                            for (ROI roi : rois)
+                            {
+                                if (roi.contains(column, line, z, t, channel))
+                                {
+                                    elements[elementsCount++] = inputCursor.get(column, line, channel);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (elementsCount == 0)
+                    {
+                        outputCursor.setSafe(column, 0, channel, 0d);
+                    }
+                    else
+                    {
+                        outputCursor.setSafe(column, 0, channel, computeResVal(Arrays.copyOf(elements, elementsCount)));
+                    }
+
+                }
+            }
+            outputCursor.commitChanges();
+            return outputPlane;
+        };
+    }
+
+    private Callable<IcyBufferedImage> getImageCProjectionTask(IcyBufferedImage inputPlane,
+            IcyBufferedImage outputPlane, int t, int z, int channels, int height, int width)
+    {
+        return () -> {
+            int channel, line, column, elementsCount;
+            double[] elements = new double[height];
+
+            IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[channels];
+            IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(outputPlane);
+            for (channel = 0; channel < channels; channel++)
+            {
+                inputCursors[channel] = new IcyBufferedImageCursor(inputPlane);
+            }
+
+            for (column = 0; column < width; column++)
+            {
+                for (line = 0; line < height; line++)
+                {
+                    elementsCount = 0;
+
+                    for (channel = 0; channel < channels; channel++)
+                    {
+                        if (rois.isEmpty())
+                        {
+                            elements[elementsCount++] = inputCursors[channel].get(column, line, channel);
+                        }
+                        else
+                        {
+                            for (ROI roi : rois)
+                            {
+                                if (roi.contains(column, line, z, t, channel))
+                                {
+                                    elements[elementsCount++] = inputCursors[channel].get(column, line, channel);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+
+                    if (elementsCount == 0)
+                    {
+                        outputCursor.setSafe(column, line, 0, 0d);
+                    }
+                    else
+                    {
+                        outputCursor.setSafe(column, line, 0, computeResVal(Arrays.copyOf(elements, elementsCount)));
+                    }
+                }
+
+            }
+
+            outputCursor.commitChanges();
+
+            return outputPlane;
+        };
+
+    }
+
+    private void startZProjection() throws InterruptedException, ExecutionException
+    {
+        final int width = seq.getSizeX();
+        final int height = seq.getSizeY();
+        final int depth = seq.getSizeZ();
+        final int frames = seq.getSizeT();
+        final int channels = seq.getSizeC();
+
+        ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
+                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
+        ForkJoinPool volumeTaskPool = (ForkJoinPool) Executors
+                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
+        int t, c;
+        VolumetricImage inputVolume, resultVolume;
+        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();
+
+        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);
+            }
+        }
+        catch (InterruptedException | ExecutionException e)
+        {
+            generalTaskPool.shutdownNow();
+            throw e;
+        }
+        finally
+        {
+            volumeTaskPool.shutdown();
+        }
+
+    }
+
+    private Callable<VolumetricImage> getImageZProjectionTask(int t, int c, VolumetricImage inputVolume,
+            VolumetricImage resultVolume, int channels, int height, int width, int depth, ForkJoinPool volumeTaskPool)
+    {
+        return () -> {
+            int slice, line, column;
+
+            IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[depth];
+            IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(resultVolume.getImage(0));
+            for (slice = 0; slice < depth; slice++)
+            {
+                inputCursors[slice] = new IcyBufferedImageCursor(inputVolume.getImage(slice));
+            }
+
+            ArrayList<Future<double[]>> futureLines = new ArrayList<>(width);
+            for (line = 0; line < height; line++)
+            {
+                final int y = line;
+                futureLines.add(volumeTaskPool.submit(() -> {
+                    int x, elemCount, z;
+                    double[] lineElements = new double[width], elements = new double[depth];
+
+                    for (x = 0; x < width; x++)
+                    {
+                        elemCount = 0;
+                        for (z = 0; z < depth; z++)
+                        {
+                            if (rois.isEmpty())
+                            {
+                                elements[elemCount++] = inputCursors[z].get(x, y, c);
+                            }
+                            else
+                            {
+                                for (ROI roi : rois)
+                                {
+                                    if (roi.contains(x, y, z, t, c))
+                                    {
+                                        elements[elemCount++] = inputCursors[z].get(x, y, c);
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+
+                        if (elemCount == 0)
+                        {
+                            lineElements[x] = 0d;
+                        }
+                        else
+                        {
+                            lineElements[x] = computeResVal(Arrays.copyOf(elements, elemCount));
+                        }
+                    }
+
+                    return lineElements;
+                }));
+            }
+
+            try
+            {
+                for (line = 0; line < height; line++)
+                {
+                    double[] lineElements = futureLines.get(line).get();
+                    for (column = 0; column < width; column++)
+                    {
+                        outputCursor.setSafe(column, line, c, lineElements[column]);
+                    }
+                }
+            }
+            finally
+            {
+                outputCursor.commitChanges();
+            }
+
+            return resultVolume;
+        };
+
+    }
+
+    private void startTProjection() throws InterruptedException, ExecutionException
+    {
+        final int width = seq.getSizeX();
+        final int height = seq.getSizeY();
+        final int depth = seq.getSizeZ();
+        final int frames = seq.getSizeT();
+        final int channels = seq.getSizeC();
+
+        ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
+                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
+        ForkJoinPool temporalTaskPool = (ForkJoinPool) Executors
+                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
+        int z, c;
+        Sequence inputS, resultS;
+        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();
+
+        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);
+            }
+        }
+        catch (InterruptedException | ExecutionException e)
+        {
+            generalTaskPool.shutdownNow();
+            throw e;
+        }
+        finally
+        {
+            temporalTaskPool.shutdown();
+        }
+    }
+
+    private Callable<Sequence> getImageTProjectionTask(int z, int c, Sequence inputS, Sequence resultS, int channels,
+            int height, int width, int depth, int frames, ForkJoinPool temporalTaskPool)
+    {
+        return () -> {
+            int frame, line, column;
+            IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[frames];
+            IcyBufferedImageCursor outputCursor = new IcyBufferedImageCursor(resultS.getImage(0, z));
+            for (frame = 0; frame < frames; frame++)
+            {
+                inputCursors[frame] = new IcyBufferedImageCursor(inputS.getImage(frame, z));
+            }
+
+            List<Future<double[]>> futureLines = new ArrayList<>(height);
+            for (line = 0; line < height; line++)
+            {
+                final int y = line;
+                futureLines.add(temporalTaskPool.submit(() -> {
+                    int x, elemCount, t;
+                    double[] lineElements = new double[width], elements = new double[frames];
+
+                    for (x = 0; x < width; x++)
+                    {
+                        elemCount = 0;
+                        for (t = 0; t < frames; t++)
+                        {
+                            if (rois.isEmpty())
+                            {
+                                elements[elemCount++] = inputCursors[t].get(x, y, c);
+                            }
+                            else
+                            {
+                                for (ROI roi : rois)
+                                {
+                                    if (roi.contains(x, y, z, t, c))
+                                    {
+                                        elements[elemCount++] = inputCursors[t].get(x, y, c);
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+
+                        if (elemCount == 0)
+                        {
+                            lineElements[x] = 0d;
+                        }
+                        else
+                        {
+                            lineElements[x] = computeResVal(Arrays.copyOf(elements, elemCount));
+                        }
+                    }
+                    return lineElements;
+                }));
+            }
+
+            try
+            {
+                for (line = 0; line < height; line++)
+                {
+                    double[] lineElements = futureLines.get(line).get();
+                    for (column = 0; column < width; column++)
+                    {
+                        outputCursor.setSafe(column, line, c, lineElements[column]);
+                    }
+                }
+            }
+            finally
+            {
+                outputCursor.commitChanges();
+            }
+
+            return resultS;
+        };
+    }
+
+    private double computeResVal(double[] elements)
+    {
+        switch (op)
+        {
+            case MAX:
+                return ArrayMath.max(elements);
+            case MEAN:
+                return ArrayMath.mean(elements);
+            case MED:
+                if (elements.length == 1)
+                    return elements[0];
+                else
+                    return ArrayMath.median(elements, false);
+            case MIN:
+                return ArrayMath.min(elements);
+            case SATSUM:
+                return ArrayMath.sum(elements);
+            case STD:
+                return ArrayMath.std(elements, true);
+        }
+        return 0;
+    }
+
+    private void notifyProgress(double progress, String message)
+    {
+        progressListeners.forEach(l -> l.onProgress(progress, message));
+    }
+
+    private Sequence getResultSequence()
+    {
+        return result;
+    }
+
+    public void reset()
+    {
+        result = null;
+        computed = false;
+    }
+}
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c693918e3ba0d22acffa6ab0e9e6b1a4b550793
--- /dev/null
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
@@ -0,0 +1,27 @@
+/**
+ * 
+ */
+package org.bioimageanalysis.icy.image.projection;
+
+/**
+ * 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");
+
+    private final String description;
+
+    ProjectionOperationType(String description)
+    {
+        this.description = description;
+    }
+
+    @Override
+    public 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
new file mode 100644
index 0000000000000000000000000000000000000000..c3de81e3f42542a963d549157ec78f6b75163640
--- /dev/null
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
@@ -0,0 +1,22 @@
+/**
+ * 
+ */
+package org.bioimageanalysis.icy.image.projection.util;
+
+/**
+ * A Progress event listener that handles events associating a value and a message to them.
+ * 
+ * @author Daniel Felipe Gonzalez Obando
+ */
+public interface MessageProgressListener
+{
+    /**
+     * A method handling progress events with a value and a message.
+     * 
+     * @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);
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..5c10cd6f2b5592a2f998ce089ca9dcf089a453c8
--- /dev/null
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
@@ -0,0 +1,150 @@
+package org.bioimageanalysis.icy.image.projection.util;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import icy.sequence.Sequence;
+
+/**
+ * This class allows to optimally access randomly around a {@link Sequence}. Instances of this class can perform reading and writing operations on
+ * non-contiguous positions of the sequence without incurring in important performance issues. When a set of modifications to pixel data is performed a call to
+ * {@link #commitChanges()} must be made in order to make this changes permanent of the image and let other resources using the image be aware of to these
+ * changes.
+ * 
+ * @author Daniel Felipe Gonzalez Obando
+ */
+public class SequenceCursor
+{
+    private Sequence seq;
+    private VolumetricImageCursor[] volumeCursors;
+    private AtomicBoolean sequenceChanged;
+
+    /**
+     * Creates a cursor for the given sequence {@code seq}.
+     */
+    public SequenceCursor(Sequence seq)
+    {
+        this.seq = seq;
+        this.volumeCursors = new VolumetricImageCursor[seq.getSizeT()];
+        this.sequenceChanged = new AtomicBoolean();
+        this.currentT = -1;
+    }
+
+    /**
+     * Retrieves the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}) at time {@code t}.
+     * 
+     * @param x
+     *        Position in the X-axis.
+     * @param y
+     *        Position in the Y-axis.
+     * @param z
+     *        Position in the Z-axis.
+     * @param t
+     *        Time point index.
+     * @param c
+     *        Channel index.
+     * @return Intensity value at specified position.
+     * @throws IndexOutOfBoundsException
+     *         If the position is not in the image.
+     * @throws RuntimeException
+     *         If the data type is not a valid format.
+     */
+    public double get(int x, int y, int z, int t, int c) throws IndexOutOfBoundsException, RuntimeException
+    {
+        return getVolumeCursor(t).get(x, y, z, c);
+    }
+
+    /**
+     * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}) at time {@code t}.
+     * 
+     * @param x
+     *        Position in the X-axis.
+     * @param y
+     *        Position in the Y-axis.
+     * @param z
+     *        Position in the Z-axis.
+     * @param t
+     *        Time point index.
+     * @param c
+     *        Channel index.
+     * @param val
+     *        Intensity value to set.
+     * @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(int x, int y, int z, int t, int c, double val)
+            throws IndexOutOfBoundsException, RuntimeException
+    {
+        getVolumeCursor(t).set(x, y, z, c, val);
+        sequenceChanged.set(true);
+    }
+
+    /**
+     * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}) at time {@code t}. This method limits the
+     * value of the intensity according to the image data type value range.
+     * 
+     * @param x
+     *        Position in the X-axis.
+     * @param y
+     *        Position in the Y-axis.
+     * @param z
+     *        Position in the Z-axis.
+     * @param t
+     *        Time point index.
+     * @param c
+     *        Channel index.
+     * @param val
+     *        Intensity value to set.
+     * @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(int x, int y, int z, int t, int c, double val)
+            throws IndexOutOfBoundsException, RuntimeException
+    {
+        getVolumeCursor(t).setSafe(x, y, z, c, val);
+        sequenceChanged.set(true);
+    }
+
+    private int currentT;
+    private VolumetricImageCursor currentCursor;
+
+    private synchronized VolumetricImageCursor getVolumeCursor(int t) throws IndexOutOfBoundsException
+    {
+        if (currentT != t)
+        {
+            if (volumeCursors[t] == null)
+            {
+                volumeCursors[t] = new VolumetricImageCursor(seq, t);
+            }
+            currentCursor = volumeCursors[t];
+            currentT = t;
+        }
+        return currentCursor;
+    }
+
+    /**
+     * This method should be called after a set of intensity changes have been made to the target sequence. This methods allows other resources using the target
+     * sequence to be informed about the changes made to it.
+     */
+    public synchronized void commitChanges()
+    {
+        if (sequenceChanged.get())
+        {
+            for (int i = 0; i < volumeCursors.length; i++)
+            {
+                if (volumeCursors[i] != null)
+                    volumeCursors[i].commitChanges();
+            }
+            sequenceChanged.set(false);
+        }
+    }
+
+    @Override
+    public 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
new file mode 100644
index 0000000000000000000000000000000000000000..54d36d39bdb3567224bb1666ae9fe934f12583f6
--- /dev/null
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
@@ -0,0 +1,165 @@
+package org.bioimageanalysis.icy.image.projection.util;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import icy.image.IcyBufferedImageCursor;
+import icy.sequence.Sequence;
+import icy.sequence.VolumetricImage;
+
+/**
+ * This class allows to optimally access randomly around an {@link VolumetricImage}. Instances of this class can perform reading and writing operations on
+ * non-contiguous positions of the volume without incurring in important performance issues. When a set of modifications to pixel data is performed a call to
+ * {@link #commitChanges()} must be made in order to make this changes permanent of the image and let other resources using the image be aware of to these
+ * changes.
+ * 
+ * @author Daniel Felipe Gonzalez Obando
+ */
+public class VolumetricImageCursor
+{
+    private VolumetricImage vol;
+
+    private AtomicBoolean volumeChanged;
+
+    private IcyBufferedImageCursor[] planeCursors;
+
+    /**
+     * Creates a cursor on the given volume {@code vol}.
+     * 
+     * @param vol
+     *        Target volume.
+     */
+    public VolumetricImageCursor(VolumetricImage vol)
+    {
+        this.vol = vol;
+        planeCursors = new IcyBufferedImageCursor[vol.getSize()];
+        volumeChanged = new AtomicBoolean(false);
+        currentZ = -1;
+    }
+
+    /**
+     * Creates a cursor on the volume at time position {@code t} in the given sequence {@code seq}.
+     * 
+     * @param seq
+     *        Target sequence.
+     * @param t
+     *        Time position where the volume is located in {@code seq}.
+     */
+    public VolumetricImageCursor(Sequence seq, int t)
+    {
+        this(seq.getVolumetricImage(t));
+    }
+
+    /**
+     * Retrieves the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}).
+     * 
+     * @param x
+     *        Position in the X-axis.
+     * @param y
+     *        Position in the Y-axis.
+     * @param z
+     *        Position in the Z-axis.
+     * @param c
+     *        Channel index.
+     * @return Intensity value at specified position.
+     * @throws IndexOutOfBoundsException
+     *         If the position is not in the image.
+     * @throws RuntimeException
+     *         If the data type is not a valid format.
+     */
+    public double get(int x, int y, int z, int c) throws IndexOutOfBoundsException, RuntimeException
+    {
+        return getPlaneCursor(z).get(x, y, c);
+
+    }
+
+    /**
+     * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}).
+     * 
+     * @param x
+     *        Position in the X-axis.
+     * @param y
+     *        Position in the Y-axis.
+     * @param z
+     *        Position in the Z-axis.
+     * @param c
+     *        Channel index.
+     * @param val
+     *        Intensity value to set.
+     * @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(int x, int y, int z, int c, double val)
+            throws IndexOutOfBoundsException, RuntimeException
+    {
+        getPlaneCursor(z).set(x, y, c, val);
+        volumeChanged.set(true);
+    }
+
+    /**
+     * Sets the intensity of the channel {@code c} of the pixel located at position ({@code x}, {@code y}, {@code z}). This method limits the
+     * value of the intensity according to the image data type value range.
+     * 
+     * @param x
+     *        Position in the X-axis.
+     * @param y
+     *        Position in the Y-axis.
+     * @param z
+     *        Position in the Z-axis.
+     * @param c
+     *        Channel index.
+     * @param val
+     *        Intensity value to set.
+     * @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(int x, int y, int z, int c, double val)
+            throws IndexOutOfBoundsException, RuntimeException
+    {
+        getPlaneCursor(z).setSafe(x, y, c, val);
+        volumeChanged.set(true);
+    }
+
+    private IcyBufferedImageCursor currentCursor;
+    private int currentZ;
+
+    private synchronized IcyBufferedImageCursor getPlaneCursor(int z) throws IndexOutOfBoundsException
+    {
+        if (currentZ != z)
+        {
+            if (planeCursors[z] == null)
+            {
+                planeCursors[z] = new IcyBufferedImageCursor(vol.getImage(z));
+            }
+            currentCursor = planeCursors[z];
+            currentZ = z;
+        }
+        return currentCursor;
+    }
+
+    /**
+     * This method should be called after a set of intensity changes have been made to the target volume. This methods allows other resources using the target
+     * volume to be informed about the changes made to it.
+     */
+    public synchronized void commitChanges()
+    {
+        if (volumeChanged.get())
+        {
+            for (int i = 0; i < planeCursors.length; i++)
+            {
+                if (planeCursors[i] != null)
+                    planeCursors[i].commitChanges();
+            }
+            volumeChanged.set(false);
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return "last Z=" + currentZ + " " + currentCursor != null ? currentCursor.toString() : "";
+    }
+}
diff --git a/src/main/java/plugins/adufour/projection/Projection.java b/src/main/java/plugins/adufour/projection/Projection.java
index 78e98bad685966c5bdd81c731cf6458120eade59..bea801a1f090324a5f73aabf7d7a44252dbc7516 100644
--- a/src/main/java/plugins/adufour/projection/Projection.java
+++ b/src/main/java/plugins/adufour/projection/Projection.java
@@ -1,24 +1,18 @@
 package plugins.adufour.projection;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
 
-import icy.image.IcyBufferedImage;
+import org.bioimageanalysis.icy.image.projection.ProjectionAxis;
+import org.bioimageanalysis.icy.image.projection.ProjectionCalculator;
+import org.bioimageanalysis.icy.image.projection.ProjectionOperationType;
+
 import icy.main.Icy;
-import icy.math.ArrayMath;
 import icy.plugin.PluginLauncher;
 import icy.plugin.PluginLoader;
 import icy.roi.ROI;
 import icy.sequence.Sequence;
-import icy.system.SystemUtil;
-import icy.system.thread.Processor;
-import icy.type.DataType;
-import icy.type.collection.array.Array1DUtil;
-import icy.util.OMEUtil;
+import icy.system.IcyHandledException;
 import plugins.adufour.blocks.lang.Block;
 import plugins.adufour.blocks.util.VarList;
 import plugins.adufour.ezplug.EzPlug;
@@ -32,7 +26,7 @@ public class Projection extends EzPlug implements Block, EzStoppable
 {
     public enum ProjectionDirection
     {
-        Z, T
+        Z, T, C, Y, X
     }
 
     public enum ProjectionType
@@ -54,58 +48,175 @@ public class Projection extends EzPlug implements Block, EzStoppable
         }
     }
 
-    private final EzVarSequence input = new EzVarSequence("Input");
+    private EzVarSequence input;
 
-    private final EzVarEnum<ProjectionDirection> projectionDir = new EzVarEnum<Projection.ProjectionDirection>(
-            "Project along", ProjectionDirection.values(), ProjectionDirection.Z);
+    private EzVarEnum<ProjectionDirection> projectionDir;
 
-    private final EzVarEnum<ProjectionType> projectionType = new EzVarEnum<Projection.ProjectionType>("Projection type",
-            ProjectionType.values(), ProjectionType.MAX);
+    private EzVarEnum<ProjectionType> projectionType;
 
-    private final EzVarBoolean restrictToROI = new EzVarBoolean("Restrict to ROI", false);
+    private EzVarBoolean restrictToROI;
 
-    private final VarSequence output = new VarSequence("projected sequence", null);
+    private VarSequence output;
 
     @Override
     protected void initialize()
     {
+        initCommonVars();
+        restrictToROI.setToolTipText(
+                "Check this option to project only the intensity data contained within the sequence ROI");
+
         addEzComponent(input);
         addEzComponent(projectionDir);
         addEzComponent(projectionType);
-
-        restrictToROI.setToolTipText(
-                "Check this option to project only the intensity data contained within the sequence ROI");
         addEzComponent(restrictToROI);
+    }
+
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        initCommonVars();
+        inputMap.add("input", input.getVariable());
+        inputMap.add("projection direction", projectionDir.getVariable());
+        inputMap.add("projection type", projectionType.getVariable());
+        inputMap.add("restrict to ROI", restrictToROI.getVariable());
+    }
 
+    private void initCommonVars()
+    {
+        input = new EzVarSequence("Input");
+        projectionDir = new EzVarEnum<Projection.ProjectionDirection>("Project along", ProjectionDirection.values(),
+                ProjectionDirection.Z);
+        projectionType = new EzVarEnum<Projection.ProjectionType>("Projection type", ProjectionType.values(),
+                ProjectionType.MAX);
+        restrictToROI = new EzVarBoolean("Restrict to ROI", false);
         setTimeDisplay(true);
     }
 
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        output = new VarSequence("projected sequence", null);
+        outputMap.add("projection output", output);
+    }
+
     @Override
     protected void execute()
     {
-        switch (projectionDir.getValue())
+        if (!isHeadLess())
         {
-            case T:
-                output.setValue(tProjection(input.getValue(true), projectionType.getValue(), true,
-                        restrictToROI.getValue().booleanValue()));
-                break;
+            getUI().setProgressBarVisible(true);
+            getUI().setProgressBarValue(Double.NaN);
+        }
+
+        readInput();
+        try
+        {
+            computeCalculator();
+        }
+        catch (Exception e)
+        {
+            e.printStackTrace();
+            throw new IcyHandledException("Error while projecting: " + e.getMessage(), e);
+        }
+        setOutput();
+        internalClean();
+    }
+
+    private Sequence inputSequence;
+    private ProjectionAxis axis;
+    private ProjectionOperationType op;
+    private List<ROI> rois;
+
+    private void readInput()
+    {
+        inputSequence = input.getValue(true);
+        axis = getAxis(projectionDir.getValue(true));
+        op = getOperation(projectionType.getValue(true));
+        rois = restrictToROI.getValue(true) ? inputSequence.getROIs() : Collections.emptyList();
+    }
+
+    private static ProjectionAxis getAxis(ProjectionDirection direction)
+    {
+        switch (direction)
+        {
+            case X:
+                return ProjectionAxis.X;
+            case Y:
+                return ProjectionAxis.Y;
+            case C:
+                return ProjectionAxis.C;
             case Z:
-                output.setValue(zProjection(input.getValue(true), projectionType.getValue(), true,
-                        restrictToROI.getValue().booleanValue()));
-                break;
+                return ProjectionAxis.Z;
+            case T:
+                return ProjectionAxis.T;
+
+            default:
+                throw new IllegalArgumentException("" + direction);
+        }
+    }
+
+    private static ProjectionOperationType getOperation(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 UnsupportedOperationException(
-                        "Projection along " + projectionDir.getValue() + " not supported");
+                throw new IllegalArgumentException("" + type);
         }
+    }
+
+    private Sequence resultSequence;
+
+    private void computeCalculator() throws Exception
+    {
+        ProjectionCalculator calculator = new ProjectionCalculator.Builder(inputSequence).axis(axis).operation(op)
+                .addRois(rois).build();
+        if (!isHeadLess())
+        {
+            calculator.addProgressListener(this::onProgress);
+        }
+        resultSequence = calculator.call();
+    }
+
+    private void onProgress(double progress, String message)
+    {
+        getUI().setProgressBarValue(progress);
+        getUI().setProgressBarMessage(message);
+    }
+
+    private void setOutput()
+    {
+        if (isHeadLess())
+            output.setValue(resultSequence);
+        else
+            addSequence(resultSequence);
+    }
 
-        if (getUI() != null)
-            addSequence(output.getValue());
+    private void internalClean()
+    {
+        getUI().setProgressBarVisible(false);
+        inputSequence = null;
+        axis = null;
+        op = null;
+        rois = null;
+        resultSequence = null;
     }
 
     @Override
     public void clean()
     {
-        //
     }
 
     /**
@@ -117,8 +228,11 @@ public class Projection extends EzPlug implements Block, EzStoppable
      * @param projection
      *        the type of projection to perform (see {@link ProjectionType} enumeration)
      * @return the projected sequence
+     * @throws Exception
+     *         If the projection cannot be correctly done.
+     * @see ProjectionCalculator
      */
-    public static Sequence zProjection(final Sequence in, final ProjectionType projection)
+    public static Sequence zProjection(final Sequence in, final ProjectionType projection) throws Exception
     {
         return zProjection(in, projection, true, false);
     }
@@ -134,8 +248,12 @@ public class Projection extends EzPlug implements Block, EzStoppable
      * @param multiThread
      *        deprecated (there is not reason to not use it)
      * @return the projected sequence
+     * @throws Exception
+     *         If the projection cannot be correctly done.
+     * @see ProjectionCalculator
      */
     public static Sequence zProjection(final Sequence in, final ProjectionType projection, boolean multiThread)
+            throws Exception
     {
         return zProjection(in, projection, multiThread, false);
     }
@@ -154,332 +272,19 @@ public class Projection extends EzPlug implements Block, EzStoppable
      *        <code>true</code> projects only data located within the sequence ROI,
      *        <code>false</code> projects the entire data set
      * @return the projected sequence
+     * @throws Exception
+     *         If the projection cannot be correctly done.
+     * @see ProjectionCalculator
      */
     public static Sequence zProjection(final Sequence in, final ProjectionType projection, boolean multiThread,
-            boolean restrictToROI)
+            boolean restrictToROI) throws Exception
     {
-        final Sequence out = new Sequence(OMEUtil.createOMEXMLMetadata(in.getOMEXMLMetadata()),
-                projection.name() + " projection of " + in.getName());
-
-        final int width = in.getSizeX();
-        final int height = in.getSizeY();
-        final int depth = in.getSizeZ();
-        final int frames = in.getSizeT();
-        final int channels = in.getSizeC();
-        final DataType dataType = in.getDataType_();
-
-        final Collection<ROI> rois = in.getROISet();
-        final boolean processROI = restrictToROI && (rois.size() > 0);
-
-        final Processor processor = new Processor(Math.max(1024, height), SystemUtil.getNumberOfCPUs());
-        final List<Future<?>> futures = new ArrayList<Future<?>>();
-
-        for (int frame = 0; frame < frames; frame++)
-        {
-            final int t = frame;
-
-            // set new image in result sequence
-            final IcyBufferedImage resultImg = new IcyBufferedImage(width, height, channels, dataType);
-            // to optimize image access in main loop !
-            final IcyBufferedImage[] images = in.getImages(t).toArray(new IcyBufferedImage[0]);
-
-            for (int channel = 0; channel < channels; channel++)
-            {
-                final int c = channel;
-                // fast access to result pixel data for this channel
-                final Object resultData = resultImg.getDataXY(c);
-
-                try
-                {
-                    for (int line = 0; line < height; line++)
-                    {
-                        final int y = line;
-
-                        futures.add(processor.submit(new Runnable()
-                        {
-                            @Override
-                            public void run()
-                            {
-                                double[] pixelStack = new double[depth];
-                                int offset = y * width;
-
-                                for (int x = 0; x < width; x++, offset++)
-                                {
-                                    int nbPixel = 0;
-
-                                    for (int z = 0; z < depth; z++)
-                                    {
-                                        boolean processPixel;
-
-                                        if (processROI)
-                                        {
-                                            processPixel = false;
-
-                                            for (ROI roi : rois)
-                                            {
-                                                if (roi.contains(x, y, z, t, c))
-                                                {
-                                                    processPixel = true;
-                                                    break;
-                                                }
-                                            }
-                                        }
-                                        else
-                                            processPixel = true;
-
-                                        if (processPixel)
-                                            pixelStack[nbPixel++] = images[z].getData(x, y, c);
-                                    }
-
-                                    // no pixel processed here ? --> next
-                                    if (nbPixel == 0)
-                                        continue;
-
-                                    final double[] pixels;
-
-                                    // adjust pixel array size if needed
-                                    if (pixelStack.length > nbPixel)
-                                        pixels = Arrays.copyOf(pixelStack, nbPixel);
-                                    else
-                                        pixels = pixelStack;
-
-                                    double result = 0d;
-
-                                    switch (projection)
-                                    {
-                                        case MAX:
-                                            result = ArrayMath.max(pixels);
-                                            break;
-                                        case MEAN:
-                                            result = ArrayMath.mean(pixels);
-                                            break;
-                                        case MED:
-                                            result = ArrayMath.median(pixels, false);
-                                            break;
-                                        case MIN:
-                                            result = ArrayMath.min(pixels);
-                                            break;
-                                        case STD:
-                                            result = ArrayMath.std(pixels, true);
-                                            break;
-                                        case SATSUM:
-                                            result = ArrayMath.sum(pixels);
-                                            break;
-                                        default:
-                                            throw new UnsupportedOperationException(
-                                                    projection + " intensity projection not implemented");
-                                    }
-
-                                    // set result in data array
-                                    Array1DUtil.setValue(resultData, offset, dataType, result);
-                                }
-                            }
-                        }));
-                    }
-
-                    // wait for completion for tasks
-                    for (Future<?> future : futures)
-                        future.get();
-                }
-                catch (RejectedExecutionException e)
-                {
-                    // mean that we were interrupted
-                    processor.shutdownNow();
-                }
-                catch (InterruptedException e)
-                {
-                    // ignore
-                    processor.shutdownNow();
-                }
-                catch (Exception e)
-                {
-                    throw new RuntimeException(e);
-                }
-
-                // data changed and cache update
-                resultImg.setDataXY(c, resultData);
-            }
-
-            // set image in sequence
-            out.setImage(t, 0, resultImg);
-        }
-
-        processor.shutdown();
-
-        // Copy color map information
-        for (int c = 0; c < in.getSizeC(); c++)
-            out.getColorModel().setColorMap(c, in.getColorMap(c), true);
-
-        return out;
+        List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList();
+        ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.Z)
+                .operation(getOperation(projection)).addRois(rois).build();
+        return calculator.call();
     }
 
-    // /**
-    // * Performs a Z projection of the input sequence using the specified algorithm. If the sequence
-    // * is already 2D, then a copy of the sequence is returned
-    // *
-    // * @param in
-    // * the sequence to project
-    // * @param projection
-    // * the type of projection to perform (see {@link ProjectionType} enumeration)
-    // * @param multiThread
-    // * true if the process should be multi-threaded
-    // * @param restrictToROI
-    // * <code>true</code> projects only data located within the sequence ROI,
-    // * <code>false</code> projects the entire data set
-    // * @return the projected sequence
-    // */
-    // public static Sequence zProjection_(final Sequence in, final ProjectionType projection, boolean multiThread,
-    // boolean restrictToROI)
-    // {
-    // final int depth = in.getSizeZ();
-    // if (depth == 1 && !restrictToROI)
-    // return SequenceUtil.getCopy(in);
-    //
-    // final Sequence out = new Sequence(projection.name() + " projection of " + in.getName());
-    // out.copyMetaDataFrom(in, false);
-    //
-    // final int width = in.getSizeX();
-    // final int height = in.getSizeY();
-    // final int frames = in.getSizeT();
-    // final int channels = in.getSizeC();
-    // final DataType dataType = in.getDataType_();
-    //
-    // final Collection<ROI> rois = in.getROISet();
-    // final boolean processROI = restrictToROI && rois.size() > 0;
-    //
-    // int cpus = SystemUtil.getNumberOfCPUs();
-    // int chunkSize = width * height / cpus;
-    // final int[] minOffsets = new int[cpus];
-    // final int[] maxOffsets = new int[cpus];
-    // for (int cpu = 0; cpu < cpus; cpu++)
-    // {
-    // minOffsets[cpu] = chunkSize * cpu;
-    // maxOffsets[cpu] = chunkSize * (cpu + 1);
-    // }
-    // // NB: the last chunk must include the remaining pixels (in case rounding off occurs)
-    // maxOffsets[cpus - 1] = width * height;
-    //
-    // ExecutorService service = multiThread ? Executors.newFixedThreadPool(cpus)
-    // : Executors.newSingleThreadExecutor();
-    // ArrayList<Future<?>> futures = new ArrayList<Future<?>>(channels * frames * cpus);
-    //
-    // for (int frame = 0; frame < frames; frame++)
-    // {
-    // final int t = frame;
-    //
-    // if (Thread.currentThread().isInterrupted())
-    // break;
-    //
-    // out.setImage(t, 0, new IcyBufferedImage(width, height, channels, dataType));
-    //
-    // for (int channel = 0; channel < channels; channel++)
-    // {
-    // final int c = channel;
-    // final Object[] in_Z_XY = (Object[]) in.getDataXYZ(t, c);
-    // final Object out_Z_XY = out.getDataXY(t, 0, c);
-    //
-    // for (int cpu = 0; cpu < cpus; cpu++)
-    // {
-    // final int minOffset = minOffsets[cpu];
-    // final int maxOffset = maxOffsets[cpu];
-    //
-    // futures.add(service.submit(new Runnable()
-    // {
-    // @Override
-    // public void run()
-    // {
-    // double[] buffer = new double[depth];
-    // double[] dataToProject = null;
-    //
-    // for (int offset = minOffset; offset < maxOffset; offset++)
-    // {
-    // if (processROI)
-    // {
-    // int x = offset % width;
-    // int y = offset / width;
-    //
-    // int nbValues = 0;
-    //
-    // for (int z = 0; z < depth; z++)
-    // for (ROI roi : rois)
-    // if (roi.contains(x, y, z, t, c))
-    // {
-    // buffer[nbValues++] = Array1DUtil.getValue(in_Z_XY[z], offset, dataType);
-    // break;
-    // }
-    //
-    // if (nbValues == 0)
-    // continue;
-    //
-    // dataToProject = (nbValues == buffer.length) ? buffer
-    // : Arrays.copyOf(buffer, nbValues);
-    // }
-    // else
-    // {
-    // for (int z = 0; z < depth; z++)
-    // buffer[z] = Array1DUtil.getValue(in_Z_XY[z], offset, dataType);
-    // dataToProject = buffer;
-    // }
-    //
-    // switch (projection)
-    // {
-    // case MAX:
-    // Array1DUtil.setValue(out_Z_XY, offset, dataType, ArrayMath.max(dataToProject));
-    // break;
-    // case MEAN:
-    // Array1DUtil.setValue(out_Z_XY, offset, dataType, ArrayMath.mean(dataToProject));
-    // break;
-    // case MED:
-    // Array1DUtil.setValue(out_Z_XY, offset, dataType,
-    // ArrayMath.median(dataToProject, false));
-    // break;
-    // case MIN:
-    // Array1DUtil.setValue(out_Z_XY, offset, dataType, ArrayMath.min(dataToProject));
-    // break;
-    // case STD:
-    // Array1DUtil.setValue(out_Z_XY, offset, dataType,
-    // ArrayMath.std(dataToProject, true));
-    // break;
-    // case SATSUM:
-    // Array1DUtil.setValue(out_Z_XY, offset, dataType,
-    // Math.min(ArrayMath.sum(dataToProject), dataType.getMaxValue()));
-    // break;
-    // default:
-    // throw new UnsupportedOperationException(
-    // projection + " intensity projection not implemented");
-    // }
-    // } // offset
-    // }
-    // }));
-    // }
-    // }
-    // }
-    //
-    // try
-    // {
-    // for (Future<?> future : futures)
-    // future.get();
-    // }
-    // catch (InterruptedException iE)
-    // {
-    // Thread.currentThread().interrupt();
-    // }
-    // catch (ExecutionException eE)
-    // {
-    // throw new RuntimeException(eE);
-    // }
-    //
-    // service.shutdown();
-    //
-    // // Copy color map information
-    // for (int c = 0; c < in.getSizeC(); c++)
-    // out.getColorModel().setColorMap(c, in.getColorMap(c), true);
-    //
-    // out.dataChanged();
-    //
-    // return out;
-    // }
-
     /**
      * Performs a T projection of the input sequence using the specified algorithm. If the sequence
      * has only one time point, then a copy of the sequence is returned
@@ -491,8 +296,12 @@ public class Projection extends EzPlug implements Block, EzStoppable
      * @param multiThread
      *        true if the process should be multi-threaded
      * @return the projected sequence
+     * @throws Exception
+     *         If the projection cannot be correctly done.
+     * @see ProjectionCalculator
      */
     public static Sequence tProjection(final Sequence in, final ProjectionType projection, boolean multiThread)
+            throws Exception
     {
         return tProjection(in, projection, multiThread, false);
     }
@@ -511,344 +320,24 @@ public class Projection extends EzPlug implements Block, EzStoppable
      *        <code>true</code> projects only data located within the sequence ROI,
      *        <code>false</code> projects the entire data set
      * @return the projected sequence
+     * @throws Exception
+     *         If the projection cannot be correctly done.
+     * @see ProjectionCalculator
      */
     public static Sequence tProjection(final Sequence in, final ProjectionType projection, boolean multiThread,
-            boolean restrictToROI)
+            boolean restrictToROI) throws Exception
     {
-        final Sequence out = new Sequence(OMEUtil.createOMEXMLMetadata(in.getOMEXMLMetadata()),
-                projection.name() + " projection of " + in.getName());
-
-        final int width = in.getSizeX();
-        final int height = in.getSizeY();
-        final int depth = in.getSizeZ();
-        final int frames = in.getSizeT();
-        final int channels = in.getSizeC();
-        final DataType dataType = in.getDataType_();
-
-        final Collection<ROI> rois = in.getROISet();
-        final boolean processROI = restrictToROI && (rois.size() > 0);
-
-        final Processor processor = new Processor(Math.max(1024, height), SystemUtil.getNumberOfCPUs());
-        final List<Future<?>> futures = new ArrayList<Future<?>>();
-
-        // to optimize image access in main loop !
-        final IcyBufferedImage[] images = new IcyBufferedImage[depth * frames];
-        for (int z = 0; z < depth; z++)
-            for (int t = 0; t < frames; t++)
-                images[(z * frames) + t] = in.getImage(t, z);
-
-        for (int slice = 0; slice < depth; slice++)
-        {
-            final int z = slice;
-            final int imgOff = z * frames;
-
-            // set new image in result sequence
-            final IcyBufferedImage resultImg = new IcyBufferedImage(width, height, channels, dataType);
-
-            for (int channel = 0; channel < channels; channel++)
-            {
-                final int c = channel;
-                // fast access to result pixel data for this channel
-                final Object resultData = resultImg.getDataXY(c);
-
-                try
-                {
-                    for (int line = 0; line < height; line++)
-                    {
-                        final int y = line;
-
-                        futures.add(processor.submit(new Runnable()
-                        {
-                            @Override
-                            public void run()
-                            {
-                                double[] pixelFrames = new double[frames];
-                                int offset = y * width;
-
-                                for (int x = 0; x < width; x++, offset++)
-                                {
-                                    int nbPixel = 0;
-
-                                    for (int t = 0; t < frames; t++)
-                                    {
-                                        boolean processPixel;
-
-                                        if (processROI)
-                                        {
-                                            processPixel = false;
-
-                                            for (ROI roi : rois)
-                                            {
-                                                if (roi.contains(x, y, z, t, c))
-                                                {
-                                                    processPixel = true;
-                                                    break;
-                                                }
-                                            }
-                                        }
-                                        else
-                                            processPixel = true;
-
-                                        if (processPixel)
-                                            pixelFrames[nbPixel++] = images[imgOff + t].getData(x, y, c);
-                                    }
-
-                                    // no pixel processed here ? --> next
-                                    if (nbPixel == 0)
-                                        continue;
-
-                                    final double[] pixels;
-
-                                    // adjust pixel array size if needed
-                                    if (pixelFrames.length > nbPixel)
-                                        pixels = Arrays.copyOf(pixelFrames, nbPixel);
-                                    else
-                                        pixels = pixelFrames;
-
-                                    double result = 0d;
-
-                                    switch (projection)
-                                    {
-                                        case MAX:
-                                            result = ArrayMath.max(pixels);
-                                            break;
-                                        case MEAN:
-                                            result = ArrayMath.mean(pixels);
-                                            break;
-                                        case MED:
-                                            result = ArrayMath.median(pixels, false);
-                                            break;
-                                        case MIN:
-                                            result = ArrayMath.min(pixels);
-                                            break;
-                                        case STD:
-                                            result = ArrayMath.std(pixels, true);
-                                            break;
-                                        case SATSUM:
-                                            result = ArrayMath.sum(pixels);
-                                            break;
-                                        default:
-                                            throw new UnsupportedOperationException(
-                                                    projection + " intensity projection not implemented");
-                                    }
-
-                                    // set result in data array
-                                    Array1DUtil.setValue(resultData, offset, dataType, result);
-                                }
-                            }
-                        }));
-                    }
-
-                    // wait for completion for tasks
-                    for (Future<?> future : futures)
-                        future.get();
-                }
-                catch (RejectedExecutionException e)
-                {
-                    // mean that we were interrupted
-                    processor.shutdownNow();
-                }
-                catch (InterruptedException e)
-                {
-                    // ignore
-                    processor.shutdownNow();
-                }
-                catch (Exception e)
-                {
-                    throw new RuntimeException(e);
-                }
-
-                // data changed and cache update
-                resultImg.setDataXY(c, resultData);
-            }
-
-            // set image in sequence
-            out.setImage(0, z, resultImg);
-        }
-
-        processor.shutdown();
-
-        // Copy color map information
-        for (int c = 0; c < in.getSizeC(); c++)
-            out.getColorModel().setColorMap(c, in.getColorMap(c), true);
-
-        return out;
-
-        /*
-         * 
-         * 
-         * final Sequence out = new Sequence(OMEUtil.createOMEXMLMetadata(in.getOMEXMLMetadata()),
-         * projection.name() + " projection of " + in.getName());
-         * 
-         * final int width = in.getSizeX();
-         * final int height = in.getSizeY();
-         * final int depth = in.getSizeZ();
-         * final int frames = in.getSizeT();
-         * final int channels = in.getSizeC();
-         * final DataType dataType = in.getDataType_();
-         * 
-         * final Collection<ROI> rois = in.getROISet();
-         * final boolean processROI = restrictToROI && (rois.size() > 0);
-         * final int cpus = SystemUtil.getNumberOfCPUs();
-         * 
-         * ExecutorService service = multiThread ? Executors.newFixedThreadPool(cpus)
-         * : Executors.newSingleThreadExecutor();
-         * ArrayList<Future<?>> futures = new ArrayList<Future<?>>();
-         * 
-         * for (int slice = 0; slice < depth; slice++)
-         * {
-         * if (Thread.currentThread().isInterrupted())
-         * {
-         * // stop all task now
-         * service.shutdownNow();
-         * break;
-         * }
-         * 
-         * final int z = slice;
-         * 
-         * // set new image in result sequence
-         * out.setImage(0, z, new IcyBufferedImage(width, height, channels, dataType));
-         * 
-         * for (int channel = 0; channel < channels; channel++)
-         * {
-         * final int c = channel;
-         * 
-         * futures.add(service.submit(new Runnable()
-         * {
-         * 
-         * @Override
-         * public void run()
-         * {
-         * final IcyBufferedImage resultImg = out.getImage(0, z);
-         * final Object resultData = resultImg.getDataXY(c);
-         * int offset = 0;
-         * 
-         * for (int y = 0; y < height; y++)
-         * {
-         * for (int x = 0; x < width; x++, offset++)
-         * {
-         * double[] framePixels = new double[frames];
-         * int nbPixel = 0;
-         * 
-         * for (int t = 0; t < frames; t++)
-         * {
-         * boolean processPixel;
-         * 
-         * if (processROI)
-         * {
-         * processPixel = false;
-         * 
-         * for (ROI roi : rois)
-         * {
-         * if (roi.contains(x, y, z, t, c))
-         * {
-         * processPixel = true;
-         * break;
-         * }
-         * }
-         * }
-         * else
-         * processPixel = true;
-         * 
-         * if (processPixel)
-         * framePixels[nbPixel++] = in.getData(t, z, c, y, x);
-         * }
-         * 
-         * if (nbPixel == 0)
-         * continue;
-         * 
-         * // adjust pixel array size if needed
-         * framePixels = Arrays.copyOf(framePixels, nbPixel);
-         * 
-         * double result = 0d;
-         * 
-         * switch (projection)
-         * {
-         * case MAX:
-         * result = ArrayMath.max(framePixels);
-         * break;
-         * case MEAN:
-         * result = ArrayMath.mean(framePixels);
-         * break;
-         * case MED:
-         * result = ArrayMath.median(framePixels, false);
-         * break;
-         * case MIN:
-         * result = ArrayMath.min(framePixels);
-         * break;
-         * case STD:
-         * result = ArrayMath.std(framePixels, true);
-         * break;
-         * case SATSUM:
-         * result = ArrayMath.sum(framePixels);
-         * break;
-         * default:
-         * throw new UnsupportedOperationException(
-         * projection + " intensity projection not implemented");
-         * }
-         * 
-         * // set result in data array
-         * Array1DUtil.setValue(resultData, offset, dataType, result);
-         * }
-         * 
-         * // task interrupted ?
-         * if (Thread.currentThread().isInterrupted())
-         * {
-         * // propagate partial changes and stop here
-         * resultImg.setDataXY(c, resultData);
-         * return;
-         * }
-         * }
-         * 
-         * // data changed and cache update
-         * resultImg.setDataXY(c, resultData);
-         * }
-         * }));
-         * }
-         * }
-         * 
-         * try
-         * {
-         * for (Future<?> future : futures)
-         * future.get();
-         * }
-         * catch (InterruptedException e)
-         * {
-         * service.shutdownNow();
-         * }
-         * catch (Exception e)
-         * {
-         * throw new RuntimeException(e);
-         * }
-         * 
-         * service.shutdown();
-         * 
-         * // Copy color map information
-         * for (int c = 0; c < in.getSizeC(); c++)
-         * out.getColorModel().setColorMap(c, in.getColorMap(c), true);
-         * 
-         * return out;
-         */
-    }
-
-    @Override
-    public void declareInput(VarList inputMap)
-    {
-        inputMap.add("input", input.getVariable());
-        inputMap.add("projection direction", projectionDir.getVariable());
-        inputMap.add("projection type", projectionType.getVariable());
-        inputMap.add("restrict to ROI", restrictToROI.getVariable());
-    }
-
-    @Override
-    public void declareOutput(VarList outputMap)
-    {
-        outputMap.add("projection output", output);
+        List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList();
+        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 args
+     *        Input arguments.
      */
     public static void main(String[] args)
     {