/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.roi;

import icy.image.IcyBufferedImage;
import icy.image.IntensityInfo;
import icy.math.DataIteratorMath;
import icy.math.MathUtil;
import icy.plugin.interface_.PluginROIDescriptor;
import icy.sequence.Sequence;
import icy.sequence.SequenceDataIterator;
import icy.type.DataIteratorUtil;
import icy.type.DataType;
import icy.type.collection.CollectionUtil;
import icy.type.dimension.Dimension5D;
import icy.type.geom.Line2DUtil;
import icy.type.geom.Polygon2D;
import icy.type.point.Point3D;
import icy.type.point.Point4D;
import icy.type.point.Point5D;
import icy.type.rectangle.Rectangle2DUtil;
import icy.type.rectangle.Rectangle3D;
import icy.type.rectangle.Rectangle4D;
import icy.type.rectangle.Rectangle5D;
import icy.util.ShapeUtil.BooleanOperator;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import plugins.kernel.roi.descriptor.intensity.ROIIntensityDescriptorsPlugin;
import plugins.kernel.roi.descriptor.intensity.ROIMaxIntensityDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROIMeanIntensityDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROIMinIntensityDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROIStandardDeviationDescriptor;
import plugins.kernel.roi.descriptor.intensity.ROISumIntensityDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIAreaDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIBasicMeasureDescriptorsPlugin;
import plugins.kernel.roi.descriptor.measure.ROIContourDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIInteriorDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIMassCenterDescriptorsPlugin;
import plugins.kernel.roi.descriptor.measure.ROIPerimeterDescriptor;
import plugins.kernel.roi.descriptor.measure.ROISurfaceAreaDescriptor;
import plugins.kernel.roi.descriptor.measure.ROIVolumeDescriptor;
import plugins.kernel.roi.roi2d.ROI2DArea;
import plugins.kernel.roi.roi2d.ROI2DEllipse;
import plugins.kernel.roi.roi2d.ROI2DPolygon;
import plugins.kernel.roi.roi2d.ROI2DRectangle;
import plugins.kernel.roi.roi2d.ROI2DShape;
import plugins.kernel.roi.roi3d.ROI3DArea;
import plugins.kernel.roi.roi3d.ROI3DStackEllipse;
import plugins.kernel.roi.roi3d.ROI3DStackPolygon;
import plugins.kernel.roi.roi3d.ROI3DStackRectangle;
import plugins.kernel.roi.roi4d.ROI4DArea;
import plugins.kernel.roi.roi5d.ROI5DArea;

/**
 * ROI utilities class.
 * 
 * @author Stephane
 */
public class ROIUtil
{
    final public static String STACK_SUFFIX = " stack";
    final public static String MASK_SUFFIX = " mask";
    final public static String SHAPE_SUFFIX = " shape";
    final public static String OBJECT_SUFFIX = " object";
    final public static String PART_SUFFIX = " part";

    /**
     * Returns all available ROI descriptors (see {@link ROIDescriptor}) and their attached plugin
     * (see {@link PluginROIDescriptor}).<br/>
     * This list can be extended by installing new plugin(s) implementing the {@link PluginROIDescriptor} interface.<br/>
     * This method is an alias of {@link ROIDescriptor#getDescriptors()}
     * 
     * @see ROIDescriptor#compute(ROI, Sequence)
     * @see PluginROIDescriptor#compute(ROI, Sequence)
     */
    public static Map<ROIDescriptor, PluginROIDescriptor> getROIDescriptors()
    {
        return ROIDescriptor.getDescriptors();
    }

    /**
     * Computes the specified descriptor from the input {@link ROIDescriptor} set on given ROI
     * and returns the result (or <code>null</code> if the descriptor is not found).<br/>
     * This method is an alias of {@link ROIDescriptor#computeDescriptor(Set, String, ROI, Sequence)}
     * 
     * @param roiDescriptors
     *        the input {@link ROIDescriptor} set (see {@link #getROIDescriptors()} method)
     * @param descriptorId
     *        the id of the descriptor we want to compute ({@link ROIBasicMeasureDescriptorsPlugin#ID_VOLUME} for
     *        instance)
     * @param roi
     *        the ROI on which the descriptor(s) should be computed
     * @param sequence
     *        an optional sequence where the pixel size can be retrieved
     * @return the computed descriptor or <code>null</code> if the descriptor if not found in the
     *         specified set
     * @throws UnsupportedOperationException
     *         if the type of the given ROI is not supported by this descriptor, or if <code>sequence</code> is
     *         <code>null</code> while the calculation requires it, or if
     *         the specified Z, T or C position are not supported by the descriptor
     */
    public static Object computeDescriptor(Set<ROIDescriptor> roiDescriptors, String descriptorId, ROI roi,
            Sequence sequence)
    {
        return ROIDescriptor.computeDescriptor(roiDescriptors, descriptorId, roi, sequence);
    }

    /**
     * Computes the specified descriptor on given ROI and returns the result (or <code>null</code> if the descriptor is
     * not found).<br/>
     * This method is an alias of {@link ROIDescriptor#computeDescriptor(String, ROI, Sequence)}
     * 
     * @param descriptorId
     *        the id of the descriptor we want to compute ({@link ROIBasicMeasureDescriptorsPlugin#ID_VOLUME} for
     *        instance)
     * @param roi
     *        the ROI on which the descriptor(s) should be computed
     * @param sequence
     *        an optional sequence where the pixel size can be retrieved
     * @return the computed descriptor or <code>null</code> if the descriptor if not found in the
     *         specified set
     * @throws UnsupportedOperationException
     *         if the type of the given ROI is not supported by this descriptor, or if <code>sequence</code> is
     *         <code>null</code> while the calculation requires it, or if
     *         the specified Z, T or C position are not supported by the descriptor
     */
    public static Object computeDescriptor(String descriptorId, ROI roi, Sequence sequence)
    {
        return ROIDescriptor.computeDescriptor(descriptorId, roi, sequence);
    }

    /**
     * @deprecated Use {@link ROIStandardDeviationDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static double getStandardDeviation(Sequence sequence, ROI roi, int z, int t, int c)
    {
        try
        {
            final SequenceDataIterator it = new SequenceDataIterator(sequence, roi, false, z, t, c);

            long numPixels = 0;
            double sum = 0;
            double sum2 = 0;

            // faster to do all calculation in a single iteration run
            while (!it.done())
            {
                final double value = it.get();

                sum += value;
                sum2 += value * value;
                numPixels++;

                it.next();
            }

            if (numPixels > 0)
            {
                double x1 = (sum2 / numPixels);
                double x2 = sum / numPixels;
                x2 *= x2;

                return Math.sqrt(x1 - x2);
            }
        }
        catch (Exception e)
        {
            // we can have exception as the process can be really long
            // and size modified during this period
        }

        return 0d;
    }

    /**
     * @deprecated Use {@link ROIIntensityDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static IntensityInfo getIntensityInfo(Sequence sequence, ROI roi, int z, int t, int c)
    {
        try
        {
            final IntensityInfo result = new IntensityInfo();
            final SequenceDataIterator it = new SequenceDataIterator(sequence, roi, false, z, t, c);

            long numPixels = 0;
            double min = Double.MAX_VALUE;
            double max = -Double.MAX_VALUE;
            double sum = 0;

            // faster to do all calculation in a single iteration run
            while (!it.done())
            {
                final double value = it.get();

                if (value < min)
                    min = value;
                if (value > max)
                    max = value;
                sum += value;
                numPixels++;

                it.next();
            }

            if (numPixels > 0)
            {
                result.minIntensity = min;
                result.maxIntensity = max;
                result.meanIntensity = sum / numPixels;
            }
            else
            {
                result.minIntensity = 0d;
                result.maxIntensity = 0d;
                result.meanIntensity = 0d;
            }

            return result;
        }
        catch (Exception e)
        {
            // we can have exception as the process can be really long
            // and size modified during this period
            return null;
        }
    }

    /**
     * Returns the number of sequence pixels contained in the specified ROI.
     * 
     * @param sequence
     *        The sequence we want to get the number of pixel.
     * @param roi
     *        The ROI define the region where we want to compute the number of pixel.
     * @param z
     *        The specific Z position (slice) where we want to compute the number of pixel or <code>-1</code> to use the
     *        ROI Z dimension information.
     * @param t
     *        The specific T position (frame) where we want to compute the number of pixel or <code>-1</code> to use the
     *        ROI T dimension information.
     * @param c
     *        The specific C position (channel) where we want to compute the number of pixel or <code>-1</code> to use
     *        the ROI C dimension information.
     */
    public static long getNumPixel(Sequence sequence, ROI roi, int z, int t, int c)
    {
        return DataIteratorUtil.count(new SequenceDataIterator(sequence, roi, false, z, t, c));
    }

    /**
     * @deprecated Use {@link ROIMinIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getMinIntensity(Sequence sequence, ROI roi, int z, int t, int c)
    {
        return DataIteratorMath.min(new SequenceDataIterator(sequence, roi, false, z, t, c));
    }

    /**
     * @deprecated Use {@link ROIMaxIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getMaxIntensity(Sequence sequence, ROI roi, int z, int t, int c)
    {
        return DataIteratorMath.max(new SequenceDataIterator(sequence, roi, false, z, t, c));
    }

    /**
     * @deprecated Use {@link ROIMeanIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getMeanIntensity(Sequence sequence, ROI roi, int z, int t, int c)
    {
        return DataIteratorMath.mean(new SequenceDataIterator(sequence, roi, false, z, t, c));
    }

    /**
     * @deprecated Use {@link ROISumIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getSumIntensity(Sequence sequence, ROI roi, int z, int t, int c)
    {
        return DataIteratorMath.sum(new SequenceDataIterator(sequence, roi, false, z, t, c));
    }

    /**
     * @deprecated Use {@link ROIStandardDeviationDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static double getStandardDeviation(Sequence sequence, ROI roi)
    {
        return getStandardDeviation(sequence, roi, -1, -1, -1);
    }

    /**
     * @deprecated Use {@link ROIIntensityDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static IntensityInfo getIntensityInfo(Sequence sequence, ROI roi)
    {
        return getIntensityInfo(sequence, roi, -1, -1, -1);
    }

    /**
     * Returns the number of sequence pixels contained in the specified ROI.
     */
    public static long getNumPixel(Sequence sequence, ROI roi)
    {
        return getNumPixel(sequence, roi, -1, -1, -1);
    }

    /**
     * @deprecated Use {@link ROIMinIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getMinIntensity(Sequence sequence, ROI roi)
    {
        return getMinIntensity(sequence, roi, -1, -1, -1);
    }

    /**
     * @deprecated Use {@link ROIMaxIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getMaxIntensity(Sequence sequence, ROI roi)
    {
        return getMaxIntensity(sequence, roi, -1, -1, -1);
    }

    /**
     * @deprecated Use {@link ROIMeanIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getMeanIntensity(Sequence sequence, ROI roi)
    {
        return getMeanIntensity(sequence, roi, -1, -1, -1);
    }

    /**
     * @deprecated Use {@link ROISumIntensityDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static double getSumIntensity(Sequence sequence, ROI roi)
    {
        return getSumIntensity(sequence, roi, -1, -1, -1);
    }

    /**
     * @deprecated Use {@link ROIMassCenterDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static Point5D getMassCenter(ROI roi)
    {
        switch (roi.getDimension())
        {
            case 2:
                final ROI2D roi2d = (ROI2D) roi;
                final Point2D pt2d = getMassCenter(roi2d);
                return new Point5D.Double(pt2d.getX(), pt2d.getY(), roi2d.getZ(), roi2d.getT(), roi2d.getC());

            case 3:
                final ROI3D roi3d = (ROI3D) roi;
                final Point3D pt3d = getMassCenter(roi3d);
                return new Point5D.Double(pt3d.getX(), pt3d.getY(), pt3d.getZ(), roi3d.getT(), roi3d.getC());

            case 4:
                final ROI4D roi4d = (ROI4D) roi;
                final Point4D pt4d = getMassCenter(roi4d);
                return new Point5D.Double(pt4d.getX(), pt4d.getY(), pt4d.getZ(), pt4d.getT(), roi4d.getC());

            case 5:
                return getMassCenter((ROI5D) roi);

            default:
                return null;
        }
    }

    /**
     * @deprecated Use {@link ROIMassCenterDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static Point2D getMassCenter(ROI2D roi)
    {
        double x = 0, y = 0;
        long len = 0;

        final BooleanMask2D mask = roi.getBooleanMask(true);
        final boolean m[] = mask.mask;
        final int h = mask.bounds.height;
        final int w = mask.bounds.width;

        int off = 0;
        for (int j = 0; j < h; j++)
        {
            for (int i = 0; i < w; i++)
            {
                if (m[off++])
                {
                    x += i;
                    y += j;
                    len++;
                }
            }
        }

        // get bounds
        final Rectangle2D bounds2D = roi.getBounds2D();

        // empty roi --> use bounds center
        if (len == 0)
            return new Point2D.Double(bounds2D.getCenterX(), bounds2D.getCenterY());

        return new Point2D.Double(bounds2D.getX() + (x / len), bounds2D.getY() + (y / len));
    }

    /**
     * @deprecated Use {@link ROIMassCenterDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static Point3D getMassCenter(ROI3D roi)
    {
        double x = 0, y = 0, z = 0;
        long len = 0;
        final BooleanMask3D mask3d = roi.getBooleanMask(true);

        for (Integer zSlice : mask3d.mask.keySet())
        {
            final int zi = zSlice.intValue();
            final double zd = zi;
            final BooleanMask2D mask = mask3d.getMask2D(zi);
            final boolean m[] = mask.mask;
            final double bx = mask.bounds.x;
            final double by = mask.bounds.y;
            final int h = mask.bounds.height;
            final int w = mask.bounds.width;

            int off = 0;
            for (int j = 0; j < h; j++)
            {
                for (int i = 0; i < w; i++)
                {
                    if (m[off++])
                    {
                        x += bx + i;
                        y += by + j;
                        z += zd;
                        len++;
                    }
                }
            }
        }

        // get bounds
        final Rectangle3D bounds3D = roi.getBounds3D();

        // empty roi --> use bounds center
        if (len == 0)
            return new Point3D.Double(bounds3D.getCenterX(), bounds3D.getCenterY(), bounds3D.getCenterZ());

        return new Point3D.Double((x / len), (y / len), (z / len));
    }

    /**
     * @deprecated Use {@link ROIMassCenterDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static Point4D getMassCenter(ROI4D roi)
    {
        final BooleanMask4D mask4d = roi.getBooleanMask(true);
        double x = 0, y = 0, z = 0, t = 0;
        long len = 0;

        for (Integer tFrame : mask4d.mask.keySet())
        {
            final int ti = tFrame.intValue();
            final double td = ti;
            final BooleanMask3D mask3d = mask4d.getMask3D(ti);

            for (Integer zSlice : mask3d.mask.keySet())
            {
                final int zi = zSlice.intValue();
                final double zd = zi;
                final BooleanMask2D mask = mask3d.getMask2D(zi);
                final boolean m[] = mask.mask;
                final double bx = mask.bounds.x;
                final double by = mask.bounds.y;
                final int h = mask.bounds.height;
                final int w = mask.bounds.width;

                int off = 0;
                for (int j = 0; j < h; j++)
                {
                    for (int i = 0; i < w; i++)
                    {
                        if (m[off++])
                        {
                            x += bx + i;
                            y += by + j;
                            z += zd;
                            t += td;
                            len++;
                        }
                    }
                }
            }
        }

        // get bounds
        final Rectangle4D bounds4D = roi.getBounds4D();

        // empty roi --> use bounds center
        if (len == 0)
            return new Point4D.Double(bounds4D.getCenterX(), bounds4D.getCenterY(), bounds4D.getCenterZ(),
                    bounds4D.getCenterT());

        return new Point4D.Double((x / len), (y / len), (z / len), (t / len));

    }

    /**
     * @deprecated Use {@link ROIMassCenterDescriptorsPlugin} or {@link #computeDescriptor(String, ROI, Sequence)}
     *             method instead.
     */
    @Deprecated
    public static Point5D getMassCenter(ROI5D roi)
    {
        final BooleanMask5D mask5d = roi.getBooleanMask(true);
        double x = 0, y = 0, z = 0, t = 0, c = 0;
        long len = 0;

        for (Integer cChannel : mask5d.mask.keySet())
        {
            final int ci = cChannel.intValue();
            final double cd = ci;
            final BooleanMask4D mask4d = mask5d.getMask4D(ci);

            for (Integer tFrame : mask4d.mask.keySet())
            {
                final int ti = tFrame.intValue();
                final double td = ti;
                final BooleanMask3D mask3d = mask4d.getMask3D(ti);

                for (Integer zSlice : mask3d.mask.keySet())
                {
                    final int zi = zSlice.intValue();
                    final double zd = zi;
                    final BooleanMask2D mask = mask3d.getMask2D(zi);
                    final boolean m[] = mask.mask;
                    final double bx = mask.bounds.x;
                    final double by = mask.bounds.y;
                    final int h = mask.bounds.height;
                    final int w = mask.bounds.width;

                    int off = 0;
                    for (int j = 0; j < h; j++)
                    {
                        for (int i = 0; i < w; i++)
                        {
                            if (m[off++])
                            {
                                x += bx + i;
                                y += by + j;
                                z += zd;
                                t += td;
                                c += cd;
                                len++;
                            }
                        }
                    }
                }
            }
        }

        // get bounds
        final Rectangle5D bounds5D = roi.getBounds5D();

        // empty roi --> use bounds center
        if (len == 0)
            return new Point5D.Double(bounds5D.getCenterX(), bounds5D.getCenterY(), bounds5D.getCenterZ(),
                    bounds5D.getCenterT(), bounds5D.getCenterC());

        return new Point5D.Double((x / len), (y / len), (z / len), (t / len), (c / len));
    }

    /**
     * @deprecated
     */
    @Deprecated
    private static double getMultiplier(Sequence sequence, ROI roi, int dim)
    {
        final int dimRoi = roi.getDimension();

        // cannot give this information for this roi
        if (dimRoi > dim)
            return 0d;

        final Rectangle5D boundsRoi = roi.getBounds5D();
        double mul = 1d;

        switch (dim)
        {
            case 5:
                if (dimRoi == 4)
                {
                    final int sizeC = sequence.getSizeC();

                    if ((boundsRoi.getSizeC() == Double.POSITIVE_INFINITY) && (sizeC > 1))
                        mul *= sizeC;
                    // cannot give this information for this roi
                    else
                        mul = 0d;
                }
            case 4:
                if (dimRoi == 3)
                {
                    final int sizeT = sequence.getSizeT();

                    if ((boundsRoi.getSizeT() == Double.POSITIVE_INFINITY) && (sizeT > 1))
                        mul *= sizeT;
                    // cannot give this information for this roi
                    else
                        mul = 0d;
                }
            case 3:
                if (dimRoi == 2)
                {
                    final int sizeZ = sequence.getSizeZ();

                    if ((boundsRoi.getSizeZ() == Double.POSITIVE_INFINITY) && (sizeZ > 1))
                        mul *= sizeZ;
                    // cannot give this information for this roi
                    else
                        mul = 0d;
                }
            case 2:
                if (dimRoi == 1)
                {
                    final int sizeY = sequence.getSizeY();

                    if ((boundsRoi.getSizeY() == Double.POSITIVE_INFINITY) && (sizeY > 1))
                        mul *= sizeY;
                    // cannot give this information for this roi
                    else
                        mul = 0d;
                }
        }

        return mul;
    }

    /**
     * @deprecated Use {@link ROIContourDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getContourSize(Sequence sequence, double contourPoints, ROI roi, int dim, int roundSignificant)
    {
        final double mul = getMultiplier(sequence, roi, dim);

        // 0 means the operation is not supported for this ROI
        if (mul != 0d)
            return sequence.calculateSize(MathUtil.roundSignificant(contourPoints, roundSignificant) * mul, dim,
                    dim - 1, 5);

        return "";
    }

    /**
     * @deprecated Use {@link ROIContourDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getContourSize(Sequence sequence, ROI roi, int dim, int roundSignificant)
    {
        return getContourSize(sequence, roi.getNumberOfContourPoints(), roi, dim, roundSignificant);
    }

    /**
     * @deprecated Use {@link ROIContourDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getContourSize(Sequence sequence, ROI roi, int dim)
    {
        return getContourSize(sequence, roi, dim, 0);
    }

    /**
     * @deprecated Use {@link ROIInteriorDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getInteriorSize(Sequence sequence, double interiorPoints, ROI roi, int dim,
            int roundSignificant)
    {
        final double mul = getMultiplier(sequence, roi, dim);

        // 0 means the operation is not supported for this ROI
        if (mul != 0d)
            return sequence.calculateSize(MathUtil.roundSignificant(interiorPoints, roundSignificant) * mul, dim, dim,
                    5);

        return "";
    }

    /**
     * @deprecated Use {@link ROIInteriorDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getInteriorSize(Sequence sequence, ROI roi, int dim, int roundSignificant)
    {
        return getInteriorSize(sequence, roi.getNumberOfPoints(), roi, dim, roundSignificant);
    }

    /**
     * @deprecated Use {@link ROIInteriorDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getInteriorSize(Sequence sequence, ROI roi, int dim)
    {
        return getInteriorSize(sequence, roi, dim, 0);
    }

    /**
     * @deprecated Use {@link ROIPerimeterDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getPerimeter(Sequence sequence, ROI roi, int roundSignificant)
    {
        return getContourSize(sequence, roi, 2, roundSignificant);
    }

    /**
     * @deprecated Use {@link ROIPerimeterDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getPerimeter(Sequence sequence, ROI roi)
    {
        return getPerimeter(sequence, roi, 0);
    }

    /**
     * @deprecated Use {@link ROIAreaDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getArea(Sequence sequence, ROI roi, int roundSignificant)
    {
        return getInteriorSize(sequence, roi, 2, roundSignificant);
    }

    /**
     * @deprecated Use {@link ROIAreaDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getArea(Sequence sequence, ROI roi)
    {
        return getArea(sequence, roi, 0);
    }

    /**
     * @deprecated Use {@link ROISurfaceAreaDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getSurfaceArea(Sequence sequence, ROI roi, int roundSignificant)
    {
        return getContourSize(sequence, roi, 3, roundSignificant);
    }

    /**
     * @deprecated Use {@link ROISurfaceAreaDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method
     *             instead.
     */
    @Deprecated
    public static String getSurfaceArea(Sequence sequence, ROI roi)
    {
        return getSurfaceArea(sequence, roi, 0);
    }

    /**
     * @deprecated Use {@link ROIVolumeDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getVolume(Sequence sequence, ROI roi, int roundSignificant)
    {
        return getInteriorSize(sequence, roi, 3, roundSignificant);
    }

    /**
     * @deprecated Use {@link ROIVolumeDescriptor} or {@link #computeDescriptor(String, ROI, Sequence)} method instead.
     */
    @Deprecated
    public static String getVolume(Sequence sequence, ROI roi)
    {
        return getVolume(sequence, roi, 0);
    }

    /**
     * Returns the effective ROI number of dimension needed for the specified bounds.
     */
    public static int getEffectiveDimension(Rectangle5D bounds)
    {
        int result = 5;

        if (bounds.isInfiniteC() || (bounds.getSizeC() <= 1d))
        {
            result--;
            if (bounds.isInfiniteT() || (bounds.getSizeT() <= 1d))
            {
                result--;
                if (bounds.isInfiniteZ() || (bounds.getSizeZ() <= 1d))
                    result--;
            }
        }

        return result;
    }

    /**
     * Return 5D dimension for specified operation dimension
     */
    private static Dimension5D.Integer getOpDim(int dim, Rectangle5D.Integer bounds)
    {
        final Dimension5D.Integer result = new Dimension5D.Integer();

        switch (dim)
        {
            case 2: // XY ROI with fixed ZTC
                result.sizeZ = 1;
                result.sizeT = 1;
                result.sizeC = 1;
                break;

            case 3: // XYZ ROI with fixed TC
                result.sizeZ = bounds.sizeZ;
                result.sizeT = 1;
                result.sizeC = 1;
                break;

            case 4: // XYZT ROI with fixed C
                result.sizeZ = bounds.sizeZ;
                result.sizeT = bounds.sizeT;
                result.sizeC = 1;
                break;

            default: // XYZTC ROI
                result.sizeZ = bounds.sizeZ;
                result.sizeT = bounds.sizeT;
                result.sizeC = bounds.sizeC;
                break;
        }

        return result;
    }

    /**
     * Get ROI result for specified 5D mask and operation dimension.
     */
    private static ROI getOpResult(int dim, BooleanMask5D mask, Rectangle5D.Integer bounds)
    {
        final ROI result;

        switch (dim)
        {
            case 2: // XY ROI with fixed ZTC
                result = new ROI2DArea(mask.getMask2D(bounds.z, bounds.t, bounds.c));

                // set ZTC position
                result.beginUpdate();
                try
                {
                    ((ROI2D) result).setZ(bounds.z);
                    ((ROI2D) result).setT(bounds.t);
                    ((ROI2D) result).setC(bounds.c);
                }
                finally
                {
                    result.endUpdate();
                }
                break;

            case 3: // XYZ ROI with fixed TC
                result = new ROI3DArea(mask.getMask3D(bounds.t, bounds.c));

                // set TC position
                result.beginUpdate();
                try
                {
                    ((ROI3D) result).setT(bounds.t);
                    ((ROI3D) result).setC(bounds.c);
                }
                finally
                {
                    result.endUpdate();
                }
                break;

            case 4: // XYZT ROI with fixed C
                result = new ROI4DArea(mask.getMask4D(bounds.c));
                // set C position
                ((ROI4D) result).setC(bounds.c);
                break;

            case 5: // XYZTC ROI
                result = new ROI5DArea(mask);
                break;

            default:
                throw new UnsupportedOperationException(
                        "Can't process boolean operation on a ROI with unknown dimension.");
        }

        return result;
    }

    /**
     * Compute the resulting bounds for <i>union</i> operation between specified ROIs.<br>
     * It throws an exception if the <i>union</i> operation cannot be done (incompatible dimension).
     */
    public static Rectangle5D getUnionBounds(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // null checking
        if (roi1 == null)
        {
            if (roi2 == null)
                return new Rectangle5D.Double();
            return roi2.getBounds5D();
        }
        else if (roi2 == null)
            return roi1.getBounds5D();

        final Rectangle5D bounds1 = roi1.getBounds5D();
        final Rectangle5D bounds2 = roi2.getBounds5D();

        // init infinite dim infos
        final boolean ic1 = bounds1.isInfiniteC();
        final boolean ic2 = bounds2.isInfiniteC();
        final boolean it1 = bounds1.isInfiniteT();
        final boolean it2 = bounds2.isInfiniteT();
        final boolean iz1 = bounds1.isInfiniteZ();
        final boolean iz2 = bounds2.isInfiniteZ();

        // cannot process union when we have an infinite dimension with a finite one
        if ((ic1 ^ ic2) || (it1 ^ it2) || (iz1 ^ iz2))
            throw new UnsupportedOperationException("Can't process union on ROI with different infinite dimension");

        // do union
        Rectangle5D.union(bounds1, bounds2, bounds1);

        // init infinite dim infos on result
        final boolean ic = bounds1.isInfiniteC() || (bounds1.getSizeC() <= 1d);
        final boolean it = bounds1.isInfiniteT() || (bounds1.getSizeT() <= 1d);
        final boolean iz = bounds1.isInfiniteZ() || (bounds1.getSizeZ() <= 1d);

        // cannot process union if C dimension is finite but T or Z is infinite
        if (!ic && (it || iz))
            throw new UnsupportedOperationException(
                    "Can't process union on ROI with a finite C dimension and infinite T or Z dimension");
        // cannot process union if T dimension is finite but Z is infinite
        if (!it && iz)
            throw new UnsupportedOperationException(
                    "Can't process union on ROI with a finite T dimension and infinite Z dimension");

        return bounds1;
    }

    /**
     * Compute the resulting bounds for <i>intersection</i> operation between specified ROIs.<br>
     * It throws an exception if the <i>intersection</i> operation cannot be done (incompatible dimension).
     */
    protected static Rectangle5D getIntersectionBounds(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // null checking
        if ((roi1 == null) || (roi2 == null))
            return new Rectangle5D.Double();

        final Rectangle5D bounds1 = roi1.getBounds5D();
        final Rectangle5D bounds2 = roi2.getBounds5D();

        // do intersection
        Rectangle5D.intersect(bounds1, bounds2, bounds1);

        // init infinite dim infos
        final boolean ic = bounds1.isInfiniteC() || (bounds1.getSizeC() <= 1d);
        final boolean it = bounds1.isInfiniteT() || (bounds1.getSizeT() <= 1d);
        final boolean iz = bounds1.isInfiniteZ() || (bounds1.getSizeZ() <= 1d);

        // cannot process intersection if C dimension is finite but T or Z is infinite
        if (!ic && (it || iz))
            throw new UnsupportedOperationException(
                    "Can't process intersection on ROI with a finite C dimension and infinite T or Z dimension");
        // cannot process intersection if T dimension is finite but Z is infinite
        if (!it && iz)
            throw new UnsupportedOperationException(
                    "Can't process intersection on ROI with a finite T dimension and infinite Z dimension");

        return bounds1;
    }

    /**
     * Compute the resulting bounds for <i>subtraction</i> of (roi1 - roi2).<br>
     * It throws an exception if the <i>subtraction</i> operation cannot be done (incompatible dimension).
     */
    protected static Rectangle5D getSubtractionBounds(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // null checking
        if (roi1 == null)
            return new Rectangle5D.Double();
        if (roi2 == null)
            return roi1.getBounds5D();

        final Rectangle5D bounds1 = roi1.getBounds5D();
        final Rectangle5D bounds2 = roi2.getBounds5D();

        // init infinite dim infos
        final boolean ic1 = bounds1.isInfiniteC();
        final boolean ic2 = bounds2.isInfiniteC();
        final boolean it1 = bounds1.isInfiniteT();
        final boolean it2 = bounds2.isInfiniteT();
        final boolean iz1 = bounds1.isInfiniteZ();
        final boolean iz2 = bounds2.isInfiniteZ();

        // cannot process subtraction when we have an finite dimension on second ROI
        // while having a infinite one on the first ROI
        if (ic1 && !ic2)
            throw new UnsupportedOperationException(
                    "Can't process subtraction: ROI 1 has infinite C dimension while ROI 2 has a finite one");
        if (it1 && !it2)
            throw new UnsupportedOperationException(
                    "Can't process subtraction: ROI 1 has infinite T dimension while ROI 2 has a finite one");
        if (iz1 && !iz2)
            throw new UnsupportedOperationException(
                    "Can't process subtraction: ROI 1 has infinite Z dimension while ROI 2 has a finite one");

        return bounds1;
    }

    /**
     * Computes union of specified <code>ROI</code> and return result in a new <code>ROI</code>.
     */
    public static ROI getUnion(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // null checking
        if (roi1 == null)
        {
            // return empty ROI
            if (roi2 == null)
                return new ROI2DArea();
            // return simple copy
            return roi2.getCopy();
        }
        else if (roi2 == null)
            return roi1.getCopy();

        final Rectangle5D bounds5D = getUnionBounds(roi1, roi2);
        final int dim = getEffectiveDimension(bounds5D);

        // we want integer bounds now
        final Rectangle5D.Integer bounds = bounds5D.toInteger();
        final Dimension5D.Integer roiSize = getOpDim(dim, bounds);
        // get 3D and 4D bounds
        final Rectangle3D.Integer bounds3D = (Rectangle3D.Integer) bounds.toRectangle3D();
        final Rectangle4D.Integer bounds4D = (Rectangle4D.Integer) bounds.toRectangle4D();

        final BooleanMask4D mask5D[] = new BooleanMask4D[roiSize.sizeC];

        for (int c = 0; c < roiSize.sizeC; c++)
        {
            final BooleanMask3D mask4D[] = new BooleanMask3D[roiSize.sizeT];

            for (int t = 0; t < roiSize.sizeT; t++)
            {
                final BooleanMask2D mask3D[] = new BooleanMask2D[roiSize.sizeZ];

                for (int z = 0; z < roiSize.sizeZ; z++)
                {
                    mask3D[z] = BooleanMask2D.getUnion(
                            roi1.getBooleanMask2D(bounds.z + z, bounds.t + t, bounds.c + c, true),
                            roi2.getBooleanMask2D(bounds.z + z, bounds.t + t, bounds.c + c, true));
                }

                mask4D[t] = new BooleanMask3D(bounds3D, mask3D);
            }

            mask5D[c] = new BooleanMask4D(bounds4D, mask4D);
        }

        // build the 5D result ROI
        final BooleanMask5D mask = new BooleanMask5D(bounds, mask5D);
        // optimize bounds of the new created mask
        mask.optimizeBounds();

        // get result
        final ROI result = getOpResult(dim, mask, bounds);
        // set name
        result.setName("Union");

        return result;
    }

    /**
     * Computes intersection of specified <code>ROI</code> and return result in a new <code>ROI</code>.
     */
    public static ROI getIntersection(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // null checking
        if ((roi1 == null) || (roi2 == null))
            // return empty ROI
            return new ROI2DArea();

        final Rectangle5D bounds5D = getIntersectionBounds(roi1, roi2);
        final int dim = getEffectiveDimension(bounds5D);

        // we want integer bounds now
        final Rectangle5D.Integer bounds = bounds5D.toInteger();
        final Dimension5D.Integer roiSize = getOpDim(dim, bounds);
        // get 2D, 3D and 4D bounds
        final Rectangle bounds2D = (Rectangle) bounds.toRectangle2D();
        final Rectangle3D.Integer bounds3D = (Rectangle3D.Integer) bounds.toRectangle3D();
        final Rectangle4D.Integer bounds4D = (Rectangle4D.Integer) bounds.toRectangle4D();

        final BooleanMask4D mask5D[] = new BooleanMask4D[roiSize.sizeC];

        for (int c = 0; c < roiSize.sizeC; c++)
        {
            final BooleanMask3D mask4D[] = new BooleanMask3D[roiSize.sizeT];

            for (int t = 0; t < roiSize.sizeT; t++)
            {
                final BooleanMask2D mask3D[] = new BooleanMask2D[roiSize.sizeZ];

                for (int z = 0; z < roiSize.sizeZ; z++)
                {
                    final BooleanMask2D roi1Mask2D = new BooleanMask2D(bounds2D, roi1.getBooleanMask2D(bounds2D,
                            bounds.z + z, bounds.t + t, bounds.c + c, true));
                    final BooleanMask2D roi2Mask2D = new BooleanMask2D(bounds2D, roi2.getBooleanMask2D(bounds2D,
                            bounds.z + z, bounds.t + t, bounds.c + c, true));

                    mask3D[z] = BooleanMask2D.getIntersection(roi1Mask2D, roi2Mask2D);
                }

                mask4D[t] = new BooleanMask3D(bounds3D, mask3D);
            }

            mask5D[c] = new BooleanMask4D(bounds4D, mask4D);
        }

        // build the 5D result ROI
        final BooleanMask5D mask = new BooleanMask5D(bounds, mask5D);
        // optimize bounds of the new created mask
        mask.optimizeBounds();

        // get result
        final ROI result = getOpResult(dim, mask, bounds);
        // set name
        result.setName("Intersection");

        return result;
    }

    /**
     * Compute exclusive union of specified <code>ROI</code> and return result in a new <code>ROI</code>.
     */
    public static ROI getExclusiveUnion(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // null checking
        if (roi1 == null)
        {
            // return empty ROI
            if (roi2 == null)
                return new ROI2DArea();
            // return simple copy
            return roi2.getCopy();
        }
        else if (roi2 == null)
            return roi1.getCopy();

        final Rectangle5D bounds5D = getUnionBounds(roi1, roi2);
        final int dim = getEffectiveDimension(bounds5D);

        // we want integer bounds now
        final Rectangle5D.Integer bounds = bounds5D.toInteger();
        final Dimension5D.Integer roiSize = getOpDim(dim, bounds);
        // get 3D and 4D bounds
        final Rectangle3D.Integer bounds3D = (Rectangle3D.Integer) bounds.toRectangle3D();
        final Rectangle4D.Integer bounds4D = (Rectangle4D.Integer) bounds.toRectangle4D();

        final BooleanMask4D mask5D[] = new BooleanMask4D[roiSize.sizeC];

        for (int c = 0; c < roiSize.sizeC; c++)
        {
            final BooleanMask3D mask4D[] = new BooleanMask3D[roiSize.sizeT];

            for (int t = 0; t < roiSize.sizeT; t++)
            {
                final BooleanMask2D mask3D[] = new BooleanMask2D[roiSize.sizeZ];

                for (int z = 0; z < roiSize.sizeZ; z++)
                {
                    mask3D[z] = BooleanMask2D.getExclusiveUnion(
                            roi1.getBooleanMask2D(bounds.z + z, bounds.t + t, bounds.c + c, true),
                            roi2.getBooleanMask2D(bounds.z + z, bounds.t + t, bounds.c + c, true));
                }

                mask4D[t] = new BooleanMask3D(bounds3D, mask3D);
            }

            mask5D[c] = new BooleanMask4D(bounds4D, mask4D);
        }

        // build the 5D result ROI
        final BooleanMask5D mask = new BooleanMask5D(bounds, mask5D);
        // optimize bounds of the new created mask
        mask.optimizeBounds();

        // get result
        final ROI result = getOpResult(dim, mask, bounds);
        // set name
        result.setName("Exclusive union");

        return result;
    }

    /**
     * Computes the subtraction of roi1 - roi2 and returns result in a new <code>ROI</code>.
     */
    public static ROI getSubtraction(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        // return empty ROI
        if (roi1 == null)
            return new ROI2DArea();
        // return copy of ROI1
        if (roi2 == null)
            return roi1.getCopy();

        final Rectangle5D bounds5D = getSubtractionBounds(roi1, roi2);
        final int dim = getEffectiveDimension(bounds5D);

        // we want integer bounds now
        final Rectangle5D.Integer bounds = bounds5D.toInteger();
        final Dimension5D.Integer roiSize = getOpDim(dim, bounds);
        // get 3D and 4D bounds
        final Rectangle3D.Integer bounds3D = (Rectangle3D.Integer) bounds.toRectangle3D();
        final Rectangle4D.Integer bounds4D = (Rectangle4D.Integer) bounds.toRectangle4D();

        final BooleanMask4D mask5D[] = new BooleanMask4D[roiSize.sizeC];

        for (int c = 0; c < roiSize.sizeC; c++)
        {
            final BooleanMask3D mask4D[] = new BooleanMask3D[roiSize.sizeT];

            for (int t = 0; t < roiSize.sizeT; t++)
            {
                final BooleanMask2D mask3D[] = new BooleanMask2D[roiSize.sizeZ];

                for (int z = 0; z < roiSize.sizeZ; z++)
                {
                    mask3D[z] = BooleanMask2D.getSubtraction(
                            roi1.getBooleanMask2D(bounds.z + z, bounds.t + t, bounds.c + c, true),
                            roi2.getBooleanMask2D(bounds.z + z, bounds.t + t, bounds.c + c, true));
                }

                mask4D[t] = new BooleanMask3D(bounds3D, mask3D);
            }

            mask5D[c] = new BooleanMask4D(bounds4D, mask4D);
        }

        // build the 5D result ROI
        final BooleanMask5D mask = new BooleanMask5D(bounds, mask5D);
        // optimize bounds of the new created mask
        mask.optimizeBounds();

        // get result
        final ROI result = getOpResult(dim, mask, bounds);
        // set name
        result.setName("Substraction");

        return result;
    }

    /**
     * Merge the specified array of {@link ROI} with the given {@link BooleanOperator}.<br>
     * 
     * @param rois
     *        ROIs we want to merge.
     * @param operator
     *        {@link BooleanOperator} to apply.
     * @return {@link ROI} representing the result of the merge operation.
     */
    public static ROI merge(List<? extends ROI> rois, BooleanOperator operator) throws UnsupportedOperationException
    {
        if (rois.size() == 0)
            return null;

        ROI result = rois.get(0).getCopy();

        // copy can fail...
        if (result != null)
        {
            switch (operator)
            {
                case AND:
                    for (int i = 1; i < rois.size(); i++)
                        result = result.intersect(rois.get(i), true);
                    break;
                case OR:
                    for (int i = 1; i < rois.size(); i++)
                        result = result.add(rois.get(i), true);
                    break;
                case XOR:
                    for (int i = 1; i < rois.size(); i++)
                        result = result.exclusiveAdd(rois.get(i), true);
                    break;
            }
        }

        // for (int i = 1; i < rois.size(); i++)
        // {
        // final ROI roi = rois.get(i);
        //
        // switch (operator)
        // {
        // case AND:
        // result = result.getIntersection(roi);
        // break;
        // case OR:
        // result = result.getUnion(roi);
        // break;
        // case XOR:
        // result = result.getExclusiveUnion(roi);
        // break;
        // }
        // }

        return result;
    }

    /**
     * Builds and returns a ROI corresponding to the union of the specified ROI list.
     */
    public static ROI getUnion(List<? extends ROI> rois) throws UnsupportedOperationException
    {
        return merge(rois, BooleanOperator.OR);
    }

    /**
     * Builds and returns a ROI corresponding to the exclusive union of the specified ROI list.
     */
    public static ROI getExclusiveUnion(List<? extends ROI> rois) throws UnsupportedOperationException
    {
        return merge(rois, BooleanOperator.XOR);
    }

    /**
     * Builds and returns a ROI corresponding to the intersection of the specified ROI list.
     */
    public static ROI getIntersection(List<? extends ROI> rois) throws UnsupportedOperationException
    {
        return merge(rois, BooleanOperator.AND);
    }

    /**
     * Subtract the content of the roi2 from the roi1 and return the result as a new {@link ROI}.<br>
     * This is equivalent to: <code>roi1.getSubtraction(roi2)</code>
     * 
     * @return {@link ROI} representing the result of subtraction.
     */
    public static ROI subtract(ROI roi1, ROI roi2) throws UnsupportedOperationException
    {
        return roi1.getSubtraction(roi2);
    }

    /**
     * Converts the specified 2D ROI to 3D Stack ROI (ROI3DStack) by stacking it along the Z axis given zMin and zMax
     * (inclusive) parameters.
     * 
     * @return the converted 3D stack ROI or <code>null</code> if the ROI
     */
    public static ROI convertToStack(ROI2D roi, int zMin, int zMax)
    {
        ROI result = null;

        if (roi instanceof ROI2DRectangle)
            result = new ROI3DStackRectangle(((ROI2DRectangle) roi).getRectangle(), zMin, zMax);
        else if (roi instanceof ROI2DEllipse)
            result = new ROI3DStackEllipse(((ROI2DEllipse) roi).getEllipse(), zMin, zMax);
        else if (roi instanceof ROI2DPolygon)
            result = new ROI3DStackPolygon(((ROI2DPolygon) roi).getPolygon2D(), zMin, zMax);
        else if (roi instanceof ROI2DArea)
            result = new ROI3DArea(((ROI2DArea) roi).getBooleanMask(true), zMin, zMax);
        else if (roi != null)
            result = new ROI3DArea(roi.getBooleanMask2D(roi.getZ(), roi.getT(), roi.getC(), true), zMin, zMax);

        if ((roi != null) && (result != null))
        {
            // unselect all control points
            result.unselectAllPoints();
            // keep original ROI informations
            result.setName(roi.getName() + STACK_SUFFIX);
            copyROIProperties(roi, result, false);
        }

        return result;
    }

    /**
     * Converts the specified ROI to a boolean mask type ROI (ROI Area).
     * 
     * @return the ROI Area corresponding to the input ROI.<br>
     *         If the ROI is already of boolean mask type then it's directly returned without any conversion.
     */
    public static ROI convertToMask(ROI roi)
    {
        // no conversion needed
        if ((roi instanceof ROI2DArea) || (roi instanceof ROI3DArea) || (roi instanceof ROI4DArea)
                || (roi instanceof ROI5DArea))
            return roi;

        final Rectangle5D bounds5D = roi.getBounds5D();
        final int dim = getEffectiveDimension(bounds5D);

        // we want integer bounds now
        final Rectangle5D.Integer bounds = bounds5D.toInteger();
        final Dimension5D.Integer roiSize = getOpDim(dim, bounds);
        // get 2D, 3D and 4D bounds
        final Rectangle bounds2D = (Rectangle) bounds.toRectangle2D();
        final Rectangle3D.Integer bounds3D = (Rectangle3D.Integer) bounds.toRectangle3D();
        final Rectangle4D.Integer bounds4D = (Rectangle4D.Integer) bounds.toRectangle4D();

        // build 5D mask result
        final BooleanMask4D mask5D[] = new BooleanMask4D[roiSize.sizeC];

        for (int c = 0; c < roiSize.sizeC; c++)
        {
            final BooleanMask3D mask4D[] = new BooleanMask3D[roiSize.sizeT];

            for (int t = 0; t < roiSize.sizeT; t++)
            {
                final BooleanMask2D mask3D[] = new BooleanMask2D[roiSize.sizeZ];

                for (int z = 0; z < roiSize.sizeZ; z++)
                    mask3D[z] = new BooleanMask2D(bounds2D, roi.getBooleanMask2D(bounds2D, bounds.z + z, bounds.t + t,
                            bounds.c + c, true));

                mask4D[t] = new BooleanMask3D(bounds3D, mask3D);
            }

            mask5D[c] = new BooleanMask4D(bounds4D, mask4D);
        }

        // build the 5D result ROI
        final BooleanMask5D mask = new BooleanMask5D(bounds, mask5D);
        // optimize bounds of the new created mask
        mask.optimizeBounds();

        // get result
        final ROI result = getOpResult(dim, mask, bounds);

        // keep original ROI informations
        String newName = roi.getName() + MASK_SUFFIX;
        // check if we can shorter name
        final String cancelableSuffix = SHAPE_SUFFIX + MASK_SUFFIX;
        if (newName.endsWith(cancelableSuffix))
            newName = newName.substring(0, newName.length() - cancelableSuffix.length());
        // set name
        result.setName(newName);
        // copy properties
        copyROIProperties(roi, result, false);

        return result;
    }

    /**
     * Converts the specified ROI to a shape type ROI (ROI Polygon or ROI Mesh).
     * 
     * @param roi
     *        the roi to convert to shape type ROI
     * @param maxDeviation
     *        maximum allowed deviation/distance of resulting ROI polygon from the input ROI contour (in pixel).
     *        Use <code>-1</code> for automatic maximum deviation calculation.
     * @return the ROI Polygon or ROI Mesh corresponding to the input ROI.<br>
     *         If the ROI is already of shape type then it's directly returned without any conversion.
     */
    public static ROI convertToShape(ROI roi, double maxDeviation) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2DShape)
            return roi;

        if (roi instanceof ROI2D)
        {
            final ROI2D roi2d = (ROI2D) roi;

            // get contour points in connected order
            final List<Point> points = roi2d.getBooleanMask(true).getConnectedContourPoints();

            // convert to point2D and center points in observed pixel.
            final List<Point2D> points2D = new ArrayList<Point2D>(points.size());
            for (Point pt : points)
                points2D.add(new Point2D.Double(pt.x + 0.5d, pt.y + 0.5d));

            final double dev;

            // auto deviation
            if (maxDeviation < 0)
            {
                // compute it from ROI size
                final Rectangle2D bnd = roi2d.getBounds2D();
                dev = Math.log10(Math.sqrt(bnd.getWidth() * bnd.getHeight())) / Math.log10(3);
            }
            else
                dev = maxDeviation;

            // convert to ROI polygon
            final ROI2DPolygon result = new ROI2DPolygon(Polygon2D.getPolygon2D(points2D, dev));

            // keep original ROI informations
            String newName = roi.getName() + SHAPE_SUFFIX;
            // check if we can shorter name
            final String cancelableSuffix = MASK_SUFFIX + SHAPE_SUFFIX;
            if (newName.endsWith(cancelableSuffix))
                newName = newName.substring(0, newName.length() - cancelableSuffix.length());
            // set name
            result.setName(newName);
            // copy properties
            copyROIProperties(roi, result, false);

            return result;
        }

        if (roi instanceof ROI3D)
        {
            // not yet supported
            throw new UnsupportedOperationException("ROIUtil.convertToShape(ROI): Operation not supported for 3D ROI.");

        }

        throw new UnsupportedOperationException("ROIUtil.convertToShape(ROI): Operation not supported for this ROI: "
                + roi.getName());
    }

    /**
     * Returns connected component from specified ROI as a list of ROI (Area type).
     */
    public static List<ROI> getConnectedComponents(ROI roi) throws UnsupportedOperationException
    {
        final List<ROI> result = new ArrayList<ROI>();

        if (roi instanceof ROI2D)
        {
            final ROI2D roi2d = (ROI2D) roi;
            int ind = 0;

            for (BooleanMask2D component : roi2d.getBooleanMask(true).getComponents())
            {
                final ROI2DArea componentRoi = new ROI2DArea(component);

                if (!componentRoi.isEmpty())
                {
                    // keep original ROI informations
                    componentRoi.setName(roi.getName() + OBJECT_SUFFIX + " #" + ind++);
                    copyROIProperties(roi, componentRoi, false);

                    result.add(componentRoi);
                }
            }

            return result;
        }

        if (roi instanceof ROI3D)
        {
            // TODO: add label extractor implementation here

            // final ROI3D roi3d = (ROI3D) roi;
            // int ind = 0;
            //
            // for (BooleanMask3D component : roi3d.getBooleanMask(true).getComponents())
            // {
            // final ROI2DArea componentRoi = new ROI2DArea(component);
            //
            // if (!componentRoi.isEmpty())
            // {
            // // keep original ROI informations
            // componentRoi.setName(roi.getName() + " object #" + ind++);
            // copyROIProperties(roi, componentRoi, false);
            //
            // result.add(componentRoi);
            // }
            // }

            // not yet supported
            throw new UnsupportedOperationException(
                    "ROIUtil.getConnectedComponents(ROI): Operation not supported yet for 3D ROI.");
        }

        throw new UnsupportedOperationException(
                "ROIUtil.getConnectedComponents(ROI): Operation not supported for this ROI: " + roi.getName());
    }

    static boolean computePolysFromLine(Line2D line, Point2D edgePt1, Point2D edgePt2, Polygon2D poly1,
            Polygon2D poly2, boolean inner)
    {
        final Line2D edgeLine = new Line2D.Double(edgePt1, edgePt2);

        // they intersect ?
        if (edgeLine.intersectsLine(line))
        {
            final Point2D intersection = Line2DUtil.getIntersection(edgeLine, line);

            // are we inside poly2 ?
            if (inner)
            {
                // add intersection to poly2
                poly2.addPoint(intersection);
                // add intersection and pt2 to poly1
                poly1.addPoint(intersection);
                poly1.addPoint(edgePt2);
            }
            else
            {
                // add intersection to poly1
                poly1.addPoint(intersection);
                // add intersection and pt2 to poly2
                poly2.addPoint(intersection);
                poly2.addPoint(edgePt2);
            }

            // we changed region
            return !inner;
        }

        // inside poly2 --> add point to poly2
        if (inner)
            poly2.addPoint(edgePt2);
        else
            poly1.addPoint(edgePt2);

        // same region
        return inner;
    }

    /**
     * Cut the specified ROI with the given Line2D (extended to ROI bounds) and return the 2 resulting ROI in a list.<br>
     * If the specified ROI cannot be cut by the given Line2D then <code>null</code> is returned.
     */
    public static List<ROI> split(ROI roi, Line2D line)
    {
        final Rectangle2D bounds2d = roi.getBounds5D().toRectangle2D();
        // need to enlarge bounds a bit to avoid roundness issues on line intersection
        final Rectangle2D extendedBounds2d = Rectangle2DUtil.getScaledRectangle(bounds2d, 1.1d, true);
        // enlarge line to ROI bounds
        final Line2D extendedLine = Rectangle2DUtil.getIntersectionLine(extendedBounds2d, line);

        // if the extended line intersects the ROI bounds
        if ((extendedLine != null) && bounds2d.intersectsLine(extendedLine))
        {
            final List<ROI> result = new ArrayList<ROI>();
            final Point2D topLeft = new Point2D.Double(bounds2d.getMinX(), bounds2d.getMinY());
            final Point2D topRight = new Point2D.Double(bounds2d.getMaxX(), bounds2d.getMinY());
            final Point2D bottomRight = new Point2D.Double(bounds2d.getMaxX(), bounds2d.getMaxY());
            final Point2D bottomLeft = new Point2D.Double(bounds2d.getMinX(), bounds2d.getMaxY());
            final Polygon2D poly1 = new Polygon2D();
            final Polygon2D poly2 = new Polygon2D();
            boolean inner;

            // add first point to poly1
            poly1.addPoint(topLeft);
            // we are inside poly1 for now
            inner = false;

            // compute the 2 rectangle part (polygon) from top, right, bottom and left lines
            inner = computePolysFromLine(extendedLine, topLeft, topRight, poly1, poly2, inner);
            inner = computePolysFromLine(extendedLine, topRight, bottomRight, poly1, poly2, inner);
            inner = computePolysFromLine(extendedLine, bottomRight, bottomLeft, poly1, poly2, inner);
            inner = computePolysFromLine(extendedLine, bottomLeft, topLeft, poly1, poly2, inner);

            // get intersection result from both polygon
            final ROI roiPart1 = new ROI2DPolygon(poly1).getIntersection(roi);
            final ROI roiPart2 = new ROI2DPolygon(poly2).getIntersection(roi);

            // keep original ROI informations
            roiPart1.setName(roi.getName() + PART_SUFFIX + " #1");
            copyROIProperties(roi, roiPart1, false);
            roiPart2.setName(roi.getName() + PART_SUFFIX + " #2");
            copyROIProperties(roi, roiPart2, false);

            // add to result list
            result.add(roiPart1);
            result.add(roiPart2);

            return result;
        }

        return null;
    }

    /**
     * Convert a list of ROI into a binary / labeled Sequence.
     * 
     * @param inputRois
     *        list of ROI to convert
     * @param sizeX
     *        the wanted size X of output Sequence, if set to <code>0</code> then Sequence size X is computed
     *        automatically from
     *        the global ROI bounds.
     * @param sizeY
     *        the wanted size Y of output Sequence, if set to <code>0</code> then Sequence size Y is computed
     *        automatically from
     *        the global ROI bounds.
     * @param sizeC
     *        the wanted size C of output Sequence, if set to <code>0</code> then Sequence size C is computed
     *        automatically from
     *        the global ROI bounds.
     * @param sizeZ
     *        the wanted size Z of output Sequence, if set to <code>0</code> then Sequence size Z is computed
     *        automatically from
     *        the global ROI bounds.
     * @param sizeT
     *        the wanted size T of output Sequence, if set to <code>0</code> then Sequence size T is computed
     *        automatically from
     *        the global ROI bounds.
     * @param dataType
     *        the wanted dataType of output Sequence
     * @param label
     *        if set to <code>true</code> then each ROI will be draw as a separate label (value) in the sequence
     *        starting from 1.
     */
    public static Sequence convertToSequence(List<ROI> inputRois, int sizeX, int sizeY, int sizeC, int sizeZ,
            int sizeT, DataType dataType, boolean label)
    {
        final List<ROI> rois = new ArrayList<ROI>();
        final Rectangle5D bounds = new Rectangle5D.Double();

        try
        {
            // compute the union of all ROI
            final ROI roi = ROIUtil.merge(inputRois, BooleanOperator.OR);
            // get bounds of result
            bounds.add(roi.getBounds5D());
            // add this single ROI to list
            rois.add(roi);
        }
        catch (Exception e)
        {
            for (ROI roi : inputRois)
            {
                // compute global bounds
                if (roi != null)
                {
                    bounds.add(roi.getBounds5D());
                    rois.add(roi);
                }
            }
        }

        int sX = sizeX;
        int sY = sizeY;
        int sC = sizeC;
        int sZ = sizeZ;
        int sT = sizeT;

        if (sX == 0)
            sX = (int) bounds.getSizeX();
        if (sY == 0)
            sY = (int) bounds.getSizeY();
        if (sC == 0)
            sC = (bounds.isInfiniteC() ? 1 : (int) bounds.getSizeC());
        if (sZ == 0)
            sZ = (bounds.isInfiniteZ() ? 1 : (int) bounds.getSizeZ());
        if (sT == 0)
            sT = (bounds.isInfiniteT() ? 1 : (int) bounds.getSizeT());

        // empty base dimension and empty result --> generate a empty 320x240 image
        if (sX == 0)
            sX = 320;
        if (sY == 0)
            sY = 240;
        if (sC == 0)
            sC = 1;
        if (sZ == 0)
            sZ = 1;
        if (sT == 0)
            sT = 1;

        final Sequence out = new Sequence("ROI conversion");

        out.beginUpdate();
        try
        {
            for (int t = 0; t < sT; t++)
                for (int z = 0; z < sZ; z++)
                    out.setImage(t, z, new IcyBufferedImage(sX, sY, sC, dataType));

            double fillValue = 1d;

            // set value from ROI(s)
            for (ROI roi : rois)
            {
                if (!roi.getBounds5D().isEmpty())
                    DataIteratorUtil.set(new SequenceDataIterator(out, roi), fillValue);

                if (label)
                    fillValue += 1d;
            }

            // notify data changed
            out.dataChanged();
        }
        finally
        {
            out.endUpdate();
        }

        return out;
    }

    /**
     * Convert a list of ROI into a binary / labeled Sequence.
     * 
     * @param inputRois
     *        list of ROI to convert
     * @param sequence
     *        the sequence used to define the wanted sequence dimension in return.<br>
     *        If this field is <code>null</code> then the global ROI bounds will be used to define the Sequence
     *        dimension
     * @param label
     *        if set to <code>true</code> then each ROI will be draw as a separate label (value) in the sequence
     *        starting from 1.
     */
    public static Sequence convertToSequence(List<ROI> inputRois, Sequence sequence, boolean label)
    {
        if (sequence == null)
            return convertToSequence(inputRois, 0, 0, 0, 0, 0, label ? ((inputRois.size() > 255) ? DataType.USHORT
                    : DataType.UBYTE) : DataType.UBYTE, label);

        return convertToSequence(inputRois, sequence.getSizeX(), sequence.getSizeY(), sequence.getSizeC(),
                sequence.getSizeZ(), sequence.getSizeT(), sequence.getDataType_(), label);
    }

    /**
     * Convert a single ROI into a binary / labeled Sequence.
     * 
     * @param inputRoi
     *        ROI to convert
     * @param sequence
     *        the sequence used to define the wanted sequence dimension in return.<br>
     *        If this field is <code>null</code> then the global ROI bounds will be used to define the Sequence
     *        dimension
     */
    public static Sequence convertToSequence(ROI inputRoi, Sequence sequence)
    {
        return convertToSequence(CollectionUtil.createArrayList(inputRoi), sequence, false);
    }

    /**
     * Copy properties (name, color...) from <code>source</code> ROI and apply it to <code>destination</code> ROI.
     */
    public static void copyROIProperties(ROI source, ROI destination, boolean copyName)
    {
        if ((source == null) || (destination == null))
            return;

        if (copyName)
            destination.setName(source.getName());
        destination.setColor(source.getColor());
        destination.setOpacity(source.getOpacity());
        destination.setStroke(source.getStroke());
        destination.setReadOnly(source.isReadOnly());
        destination.setSelected(source.isSelected());
    }
}