package icy.roi;
import icy.type.collection.array.DynamicArray;
import icy.type.point.Point3D;
import icy.type.rectangle.Rectangle3D;
import java.awt.Rectangle;
import java.util.Map.Entry;
import java.util.TreeMap;
/**
* Class to define a 3D boolean mask region and make basic boolean operation between masks.<br>
* The bounds property of this object represents the region defined by the boolean mask.
*
* @author Stephane
*/
public class BooleanMask3D implements Cloneable
{
// Internal use only
private static BooleanMask2D doUnion2D(BooleanMask2D m1, BooleanMask2D m2)
{
if (m1 == null)
{
// only use the 2D mask from second mask
if (m2 != null)
return (BooleanMask2D) m2.clone();
return null;
}
else if (m2 == null)
// only use the 2D mask from first mask
return (BooleanMask2D) m1.clone();
// process union of 2D mask
return BooleanMask2D.getUnion(m1, m2);
}
// Internal use only
private static BooleanMask2D doIntersection2D(BooleanMask2D m1, BooleanMask2D m2)
{
if ((m1 == null) || (m2 == null))
return null;
// process intersection of 2D mask
return BooleanMask2D.getIntersection(m1, m2);
}
// Internal use only
private static BooleanMask2D doExclusiveUnion2D(BooleanMask2D m1, BooleanMask2D m2)
{
if (m1 == null)
{
// only use the 2D mask from second mask
if (m2 != null)
return (BooleanMask2D) m2.clone();
return null;
}
else if (m2 == null)
// only use the 2D mask from first mask
return (BooleanMask2D) m1.clone();
// process exclusive union of 2D mask
return BooleanMask2D.getExclusiveUnion(m1, m2);
}
// Internal use only
private static BooleanMask2D doSubtraction2D(BooleanMask2D m1, BooleanMask2D m2)
{
if (m1 == null)
return null;
// only use the 2D mask from first mask
if (m2 == null)
return (BooleanMask2D) m1.clone();
// process subtraction of 2D mask
return BooleanMask2D.getSubtraction(m1, m2);
}
/**
* Build resulting mask from union of the mask1 and mask2:
*
* <pre>
* mask1 + mask2 = result
*
* ################ ################ ################
* ############## ############## ################
* ############ ############ ################
* ########## ########## ################
* ######## ######## ################
* ###### ###### ###### ######
* #### #### #### ####
* ## ## ## ##
* </pre>
*/
public static BooleanMask3D getUnion(BooleanMask3D mask1, BooleanMask3D mask2)
{
if ((mask1 == null) && (mask2 == null))
return new BooleanMask3D();
if ((mask1 == null) || mask1.isEmpty())
return (BooleanMask3D) mask2.clone();
if ((mask2 == null) || mask2.isEmpty())
return (BooleanMask3D) mask1.clone();
final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createUnion(mask2.bounds);
if (!bounds.isEmpty())
{
final BooleanMask2D[] mask;
// special case of infinite Z dimension
if (bounds.sizeZ == Integer.MAX_VALUE)
{
// we can allow merge ROI only if they both has infinite Z dimension
if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE))
throw new UnsupportedOperationException(
"Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI");
mask = new BooleanMask2D[1];
final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue();
final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue();
mask[0] = doUnion2D(m2d1, m2d2);
}
else
{
mask = new BooleanMask2D[bounds.sizeZ];
for (int z = 0; z < bounds.sizeZ; z++)
{
final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z);
final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z);
mask[z] = doUnion2D(m2d1, m2d2);
}
}
return new BooleanMask3D(bounds, mask);
}
return new BooleanMask3D();
}
/**
* Build resulting mask from intersection of the mask1 and mask2:
*
* <pre>
* mask1 intersect mask2 = result
*
* ################ ################ ################
* ############## ############## ############
* ############ ############ ########
* ########## ########## ####
* ######## ########
* ###### ######
* #### ####
* ## ##
* </pre>
*/
public static BooleanMask3D getIntersection(BooleanMask3D mask1, BooleanMask3D mask2)
{
if ((mask1 == null) || (mask2 == null))
return new BooleanMask3D();
final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createIntersection(mask2.bounds);
if (!bounds.isEmpty())
{
final BooleanMask2D[] mask;
// special case of infinite Z dimension
if (bounds.sizeZ == Integer.MAX_VALUE)
{
// we can allow merge ROI only if they both has infinite Z dimension
if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE))
throw new UnsupportedOperationException(
"Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI");
mask = new BooleanMask2D[1];
final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue();
final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue();
mask[0] = doIntersection2D(m2d1, m2d2);
}
else
{
mask = new BooleanMask2D[bounds.sizeZ];
for (int z = 0; z < bounds.sizeZ; z++)
{
final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z);
final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z);
mask[z] = doIntersection2D(m2d1, m2d2);
}
}
return new BooleanMask3D(bounds, mask);
}
return new BooleanMask3D();
}
/**
* Build resulting mask from exclusive union of the mask1 and mask2:
*
* <pre>
* mask1 xor mask2 = result
*
* ################ ################
* ############## ############## ## ##
* ############ ############ #### ####
* ########## ########## ###### ######
* ######## ######## ################
* ###### ###### ###### ######
* #### #### #### ####
* ## ## ## ##
* </pre>
*/
public static BooleanMask3D getExclusiveUnion(BooleanMask3D mask1, BooleanMask3D mask2)
{
if ((mask1 == null) && (mask2 == null))
return new BooleanMask3D();
if ((mask1 == null) || mask1.isEmpty())
return (BooleanMask3D) mask2.clone();
if ((mask2 == null) || mask2.isEmpty())
return (BooleanMask3D) mask1.clone();
final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createUnion(mask2.bounds);
if (!bounds.isEmpty())
{
final BooleanMask2D[] mask;
// special case of infinite Z dimension
if (bounds.sizeZ == Integer.MAX_VALUE)
{
// we can allow merge ROI only if they both has infinite Z dimension
if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE))
throw new UnsupportedOperationException(
"Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI");
mask = new BooleanMask2D[1];
final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue();
final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue();
mask[0] = doExclusiveUnion2D(m2d1, m2d2);
}
else
{
mask = new BooleanMask2D[bounds.sizeZ];
for (int z = 0; z < bounds.sizeZ; z++)
{
final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z);
final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z);
mask[z] = doExclusiveUnion2D(m2d1, m2d2);
}
}
return new BooleanMask3D(bounds, mask);
}
return new BooleanMask3D();
}
/**
* Build resulting mask from the subtraction of mask2 from mask1:
*
* <pre>
* mask1 - mask2 = result
*
* ################ ################
* ############## ############## ##
* ############ ############ ####
* ########## ########## ######
* ######## ######## ########
* ###### ###### ######
* #### #### ####
* ## ## ##
* </pre>
*/
public static BooleanMask3D getSubtraction(BooleanMask3D mask1, BooleanMask3D mask2)
{
if (mask1 == null)
return new BooleanMask3D();
if (mask2 == null)
return (BooleanMask3D) mask1.clone();
final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createIntersection(mask2.bounds);
// need to subtract something ?
if (!bounds.isEmpty())
{
final BooleanMask2D[] mask;
// special case of infinite Z dimension
if (bounds.sizeZ == Integer.MAX_VALUE)
{
// we can allow merge ROI only if they both has infinite Z dimension
if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE))
throw new UnsupportedOperationException(
"Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI");
mask = new BooleanMask2D[1];
final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue();
final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue();
mask[0] = doSubtraction2D(m2d1, m2d2);
}
else
{
mask = new BooleanMask2D[bounds.sizeZ];
for (int z = 0; z < bounds.sizeZ; z++)
{
final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z);
final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z);
mask[z] = doSubtraction2D(m2d1, m2d2);
}
}
return new BooleanMask3D(bounds, mask);
}
return (BooleanMask3D) mask1.clone();
}
/**
* Region represented by the mask.
*/
public Rectangle3D.Integer bounds;
/**
* Boolean mask 2D array.
*/
public final TreeMap<Integer, BooleanMask2D> mask;
/**
* Build a new 3D boolean mask with specified bounds and 2D mask array.<br>
* The 2D mask array length should be >= to <code>bounds.getSizeZ()</code>.
*/
public BooleanMask3D(Rectangle3D.Integer bounds, BooleanMask2D[] mask)
{
super();
this.bounds = bounds;
this.mask = new TreeMap<Integer, BooleanMask2D>();
// special case of infinite Z dim
if (bounds.sizeZ == Integer.MAX_VALUE)
this.mask.put(Integer.valueOf(Integer.MIN_VALUE), mask[0]);
else
{
for (int z = 0; z < bounds.sizeZ; z++)
if (mask[z] != null)
this.mask.put(Integer.valueOf(bounds.z + z), mask[z]);
}
}
/**
* Build a new 3D boolean mask from the specified array of {@link Point3D}.<br>
*/
public BooleanMask3D(Point3D.Integer[] points)
{
super();
mask = new TreeMap<Integer, BooleanMask2D>();
if ((points == null) || (points.length == 0))
bounds = new Rectangle3D.Integer();
else
{
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (Point3D.Integer pt : points)
{
final int x = pt.x;
final int y = pt.y;
final int z = pt.z;
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
if (y < minY)
minY = y;
if (y > maxY)
maxY = y;
if (z < minZ)
minZ = z;
if (z > maxZ)
maxZ = z;
}
// define bounds
bounds = new Rectangle3D.Integer(minX, minY, minZ, (maxX - minX) + 1, (maxY - minY) + 1, (maxZ - minZ) + 1);
// set mask
for (Point3D.Integer pt : points)
{
BooleanMask2D m = mask.get(Integer.valueOf(pt.z));
// allocate boolean mask if needed
if (m == null)
{
m = new BooleanMask2D(new Rectangle(minX, minY, bounds.sizeX, bounds.sizeY),
new boolean[bounds.sizeX * bounds.sizeY]);
// set 2D mask for position Z
mask.put(Integer.valueOf(pt.z), m);
}
// set mask point
m.mask[((pt.y - minY) * bounds.sizeX) + (pt.x - minX)] = true;
}
// optimize mask 2D bounds
for (BooleanMask2D m : mask.values())
m.optimizeBounds();
}
}
/**
* Build a new boolean mask from the specified array of {@link Point3D}.<br>
*/
public BooleanMask3D(Point3D[] points)
{
super();
mask = new TreeMap<Integer, BooleanMask2D>();
if ((points == null) || (points.length == 0))
bounds = new Rectangle3D.Integer();
else
{
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (Point3D pt : points)
{
final int x = (int) pt.getX();
final int y = (int) pt.getY();
final int z = (int) pt.getZ();
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
if (y < minY)
minY = y;
if (y > maxY)
maxY = y;
if (z < minZ)
minZ = z;
if (z > maxZ)
maxZ = z;
}
// define bounds
bounds = new Rectangle3D.Integer(minX, minY, minZ, (maxX - minX) + 1, (maxY - minY) + 1, (maxZ - minZ) + 1);
// set mask
for (Point3D pt : points)
{
BooleanMask2D m = mask.get(Integer.valueOf((int) pt.getZ()));
// allocate boolean mask if needed
if (m == null)
{
m = new BooleanMask2D(new Rectangle(minX, minY, bounds.sizeX, bounds.sizeY),
new boolean[bounds.sizeX * bounds.sizeY]);
// set 2D mask for position Z
mask.put(Integer.valueOf((int) pt.getZ()), m);
}
// set mask point
m.mask[(((int) pt.getY() - minY) * bounds.sizeX) + ((int) pt.getX() - minX)] = true;
}
// optimize mask 2D bounds
for (BooleanMask2D m : mask.values())
m.optimizeBounds();
}
}
public BooleanMask3D()
{
this(new Rectangle3D.Integer(), new BooleanMask2D[0]);
}
/**
* Returns the 2D boolean mask for the specified Z position
*/
public BooleanMask2D getMask2D(int z)
{
// special case of infinite Z dimension
if (bounds.sizeZ == Integer.MAX_VALUE)
return mask.firstEntry().getValue();
return mask.get(Integer.valueOf(z));
}
/**
* Return <code>true</code> if boolean mask is empty
*/
public boolean isEmpty()
{
return bounds.isEmpty();
}
/**
* Return true if mask contains the specified point
*/
public boolean contains(int x, int y, int z)
{
if (bounds.contains(x, y, z))
{
final BooleanMask2D m2d = getMask2D(z);
if (m2d != null)
return m2d.contains(x, y);
}
return false;
}
/**
* Return true if mask contains the specified 2D mask at position Z.
*/
public boolean contains(BooleanMask2D booleanMask, int z)
{
if (isEmpty())
return false;
final BooleanMask2D mask2d = getMask2D(z);
if (mask2d != null)
return mask2d.contains(booleanMask);
return false;
}
/**
* Return true if mask contains the specified 3D mask.
*/
public boolean contains(BooleanMask3D booleanMask)
{
if (isEmpty())
return false;
final int sizeZ = booleanMask.bounds.sizeZ;
// check for special MAX_INTEGER case (infinite Z dim)
if (sizeZ == Integer.MAX_VALUE)
{
// we cannot contains it if we are not on infinite Z dim too
if (bounds.sizeZ != Integer.MAX_VALUE)
return false;
return booleanMask.mask.firstEntry().getValue().contains(mask.firstEntry().getValue());
}
final int offZ = booleanMask.bounds.z;
for (int z = offZ; z < offZ + sizeZ; z++)
if (!contains(booleanMask.getMask2D(z), z))
return false;
return true;
}
/**
* Return true if mask intersects (contains at least one point) the specified 2D mask at
* position Z.
*/
public boolean intersects(BooleanMask2D booleanMask, int z)
{
if (isEmpty())
return false;
final BooleanMask2D mask2d = getMask2D(z);
if (mask2d != null)
return mask2d.intersects(booleanMask);
return false;
}
/**
* Return true if mask intersects (contains at least one point) the specified 3D mask region.
*/
public boolean intersects(BooleanMask3D booleanMask)
{
if (isEmpty())
return false;
final int sizeZ = booleanMask.bounds.sizeZ;
// check for special MAX_INTEGER case (infinite Z dim)
if (sizeZ == Integer.MAX_VALUE)
{
// get the single Z slice
final BooleanMask2D mask2d = booleanMask.mask.firstEntry().getValue();
// test with every slice
for (BooleanMask2D m : mask.values())
if (m.intersects(mask2d))
return true;
return false;
}
// check for special MAX_INTEGER case (infinite Z dim)
if (bounds.sizeZ == Integer.MAX_VALUE)
{
// get the single Z slice
final BooleanMask2D mask2d = mask.firstEntry().getValue();
// test with every slice
for (BooleanMask2D m : booleanMask.mask.values())
if (m.intersects(mask2d))
return true;
return false;
}
final int offZ = booleanMask.bounds.z;
for (int z = offZ; z < offZ + sizeZ; z++)
if (intersects(booleanMask.getMask2D(z), z))
return true;
return false;
}
/**
* Optimize mask bounds so it fits mask content.
*/
public Rectangle3D.Integer getOptimizedBounds(boolean compute2DBounds)
{
final Rectangle3D.Integer result = new Rectangle3D.Integer();
if (mask.isEmpty())
return result;
Rectangle bounds2D = null;
for (BooleanMask2D m2d : mask.values())
{
// get optimized 2D bounds for each Z
final Rectangle optB2d;
if (compute2DBounds)
optB2d = m2d.getOptimizedBounds();
else
optB2d = new Rectangle(m2d.bounds);
// only add non empty bounds
if (!optB2d.isEmpty())
{
if (bounds2D == null)
bounds2D = optB2d;
else
bounds2D.add(optB2d);
}
}
// empty ?
if ((bounds2D == null) || bounds2D.isEmpty())
return result;
int minZ = mask.firstKey().intValue();
int maxZ = mask.lastKey().intValue();
// set 2D bounds to start with
result.setX(bounds2D.x);
result.setY(bounds2D.y);
result.setSizeX(bounds2D.width);
result.setSizeY(bounds2D.height);
// single Z --> check for special MAX_INTEGER case
if ((minZ == maxZ) && (bounds.sizeZ == Integer.MAX_VALUE))
{
result.setZ(Integer.MIN_VALUE);
result.setSizeZ(Integer.MAX_VALUE);
}
else
{
result.setZ(minZ);
result.setSizeZ((maxZ - minZ) + 1);
}
return result;
}
/**
* Optimize mask bounds so it fits mask content.
*/
public Rectangle3D.Integer getOptimizedBounds()
{
return getOptimizedBounds(true);
}
/**
* Optimize mask bounds so it fits mask content.
*/
public void optimizeBounds()
{
// start by optimizing 2D bounds
for (BooleanMask2D m : mask.values())
m.optimizeBounds();
moveBounds(getOptimizedBounds(false));
}
/**
* Change the bounds of BooleanMask.<br>
* Keep mask data intersecting from old bounds.
*/
public void moveBounds(Rectangle3D.Integer value)
{
// bounds changed ?
if (!bounds.equals(value))
{
// changed to empty mask
if (value.isEmpty())
{
// clear bounds and mask
bounds = new Rectangle3D.Integer();
mask.clear();
return;
}
final Rectangle bounds2D = new Rectangle(value.x, value.y, value.sizeX, value.sizeY);
// it was infinite Z dim ?
if (bounds.sizeZ == Integer.MAX_VALUE)
{
// get the single 2D mask
final BooleanMask2D m2d = mask.firstEntry().getValue();
// adjust 2D bounds if needed to the single 2D mask
m2d.moveBounds(bounds2D);
// we passed from infinite Z to defined Z range
if (value.sizeZ != Integer.MAX_VALUE)
{
// assign the same 2D mask for all Z position
mask.clear();
for (int z = 0; z <= value.sizeZ; z++)
mask.put(Integer.valueOf(z + value.z), (BooleanMask2D) m2d.clone());
}
}
// we pass to infinite Z dim
else if (value.sizeZ == Integer.MAX_VALUE)
{
// try to use the 2D mask at Z position
BooleanMask2D mask2D = getMask2D(value.z);
// otherwise we use the first found 2D mask
if ((mask2D == null) && !mask.isEmpty())
mask2D = mask.firstEntry().getValue();
// set new mask
mask.clear();
if (mask2D != null)
mask.put(Integer.valueOf(Integer.MIN_VALUE), mask2D);
}
else
{
// create new mask array
final BooleanMask2D[] newMask = new BooleanMask2D[value.sizeZ];
for (int z = 0; z < value.sizeZ; z++)
{
final BooleanMask2D mask2D = getMask2D(value.z + z);
if (mask2D != null)
// adjust 2D bounds
mask2D.moveBounds(bounds2D);
newMask[z] = mask2D;
}
// set new mask
mask.clear();
for (int z = 0; z < value.sizeZ; z++)
mask.put(Integer.valueOf(value.z + z), newMask[z]);
}
bounds = value;
}
}
/**
* Transforms the specified 3D coordinates int array [x,y,z] in 4D coordinates int array [x,y,z,t] with the
* specified T value.
*/
public static int[] toInt3D(int[] source2D, int z)
{
final int[] result = new int[(source2D.length * 3) / 2];
int pt = 0;
for (int i = 0; i < source2D.length; i += 2)
{
result[pt++] = source2D[i + 0];
result[pt++] = source2D[i + 1];
result[pt++] = z;
}
return result;
}
/**
* Return the number of points contained in this boolean mask.
*/
public int getNumberOfPoints()
{
int result = 0;
for (BooleanMask2D mask2d : mask.values())
result += mask2d.getNumberOfPoints();
return result;
}
/**
* Return an array of {@link icy.type.point.Point3D.Integer} representing all points of the
* current 3D mask.<br>
* Points are returned in ascending XYZ order.
*/
public Point3D.Integer[] getPoints()
{
return Point3D.Integer.toPoint3D(getPointsAsIntArray());
}
/**
* Return an array of integer representing all points of the current 3D mask.<br>
* <code>result.length</code> = number of point * 3<br>
* <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i>.<br>
* <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i>.<br>
* <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i>.<br>
* Points are returned in ascending XYZ order.
*/
public int[] getPointsAsIntArray()
{
final DynamicArray.Int result = new DynamicArray.Int(8);
for (Entry<Integer, BooleanMask2D> entry : mask.entrySet())
result.add(toInt3D(entry.getValue().getPointsAsIntArray(), entry.getKey().intValue()));
return result.asArray();
}
/**
* Return an array of {@link icy.type.point.Point3D.Integer} containing the contour/surface
* points of the 3D mask.<br>
* Points are returned in ascending XYZ order. <br>
* <br>
* WARNING: The default implementation is not totally accurate.<br>
* It returns all points from the first and the last Z slices + contour points for intermediate
* Z slices.
*
* @see #getContourPointsAsIntArray()
*/
public Point3D.Integer[] getContourPoints()
{
return Point3D.Integer.toPoint3D(getContourPointsAsIntArray());
}
/**
* Return an array of integer containing the contour/surface points of the 3D mask.<br>
* <code>result.length</code> = number of point * 3<br>
* <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i>.<br>
* <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i>.<br>
* <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i>.<br>
* Points are returned in ascending XYZ order.<br>
* <br>
* WARNING: The default implementation is not totally accurate.<br>
* It returns all points from the first and the last Z slices + contour points for intermediate
* Z slices.
*
* @see #getContourPoints()
*/
public int[] getContourPointsAsIntArray()
{
final DynamicArray.Int result = new DynamicArray.Int(8);
// TODO: fix this method and use real 3D contour point
if (mask.size() <= 2)
{
for (Entry<Integer, BooleanMask2D> entry : mask.entrySet())
result.add(toInt3D(entry.getValue().getPointsAsIntArray(), entry.getKey().intValue()));
}
else
{
final Entry<Integer, BooleanMask2D> firstEntry = mask.firstEntry();
final Entry<Integer, BooleanMask2D> lastEntry = mask.lastEntry();
final Integer firstKey = firstEntry.getKey();
final Integer lastKey = lastEntry.getKey();
result.add(toInt3D(firstEntry.getValue().getPointsAsIntArray(), firstKey.intValue()));
for (Entry<Integer, BooleanMask2D> entry : mask.subMap(firstKey, false, lastKey, false).entrySet())
result.add(toInt3D(entry.getValue().getContourPointsAsIntArray(), entry.getKey().intValue()));
result.add(toInt3D(lastEntry.getValue().getPointsAsIntArray(), lastKey.intValue()));
}
return result.asArray();
}
/**
* Computes and returns the length of the contour.<br/>
* This is different from the number of contour point as it takes care of approximating
* correctly distance between each contour point.
*
* @author Alexandre Dufour
* @author Stephane Dallongeville
* @return the length of the contour
*/
public double getContourLength()
{
double result = 0;
final int[] edge = getContourPointsAsIntArray();
// count the edges and corners in 2D/3D
double sideEdges = 0, cornerEdges = 0;
for (int i = 0; i < edge.length; i += 3)
{
final int x = edge[i + 0];
final int y = edge[i + 1];
final int z = edge[i + 2];
// start on current plan
BooleanMask2D mask2D = getMask2D(z);
final boolean leftConnected = mask2D.contains(x - 1, y);
final boolean rightConnected = mask2D.contains(x + 1, y);
final boolean topConnected = mask2D.contains(x, y - 1);
final boolean bottomConnected = mask2D.contains(x + 1, y + 1);
// lower plan
mask2D = getMask2D(z - 1);
final boolean southConnected = (mask2D != null) && mask2D.contains(x, y);
// upper plan
mask2D = getMask2D(z + 1);
final boolean northConnected = (mask2D != null) && mask2D.contains(x, y);
// count the connections (6 max)
int connection = 0;
if (leftConnected)
connection++;
if (rightConnected)
connection++;
if (topConnected)
connection++;
if (bottomConnected)
connection++;
if (southConnected)
connection++;
if (northConnected)
connection++;
switch (connection)
{
// case 0: // isolated point
// cornerEdges += 3;
// sideEdges++;
// result += 1 + Math.sqrt(2) + (2 * Math.sqrt(3));
// break;
//
// case 1: // filament end
// cornerEdges += 2;
// sideEdges++;
// result += 1 + (2 * Math.sqrt(3));
// break;
//
// case 2: // filament point
// if ((leftConnected && rightConnected) || (topConnected && bottomConnected)
// || (northConnected && southConnected))
// {
// // quadruple "side" edge
// sideEdges += 4;
// result += 4;
// }
// else
// {
// cornerEdges += 3;
// result += 3 * Math.sqrt(2);
// }
// // cornerEdges += 3;
// // perimeter += Math.sqrt(3);
// break;
//
// case 3: // "salient" point
// if ((leftConnected && rightConnected) || (topConnected && bottomConnected)
// || (northConnected && southConnected))
// {
// // triple "side" edge
// sideEdges += 3;
// result += 3;
// }
// else
// {
// cornerEdges += 2;
// result += 2 * Math.sqrt(2);
// }
default:
cornerEdges++;
result += Math.sqrt(3);
break;
case 4:
if (leftConnected && rightConnected && topConnected && bottomConnected)
{
// double "side" edge
sideEdges += 2;
result += 2;
}
else if (leftConnected && rightConnected && northConnected && southConnected)
{
// double "side" edge
sideEdges += 2;
result += 2;
}
else if (topConnected && bottomConnected && northConnected && southConnected)
{
// double "side" edge
sideEdges += 2;
result += 2;
}
else
{
// "corner" edge
cornerEdges++;
result += Math.sqrt(2);
}
break;
case 5: // "side" edge
sideEdges++;
result++;
break;
// internal point --> should not happen
case 6:
break;
}
// case 0:
// break;
// case 1:
// sideEdges++;
// perimeter++;
// break;
// case 2:
// cornerEdges++;
// perimeter += Math.sqrt(2);
// break;
// case 3:
// cornerEdges += 2;
// perimeter += 2 * Math.sqrt(2);
// break;
// default:
// cornerEdges += 3;
// perimeter += Math.sqrt(3);
}
// adjust the surface area empirically according to the edge distribution
double overShoot = Math.min(sideEdges / 10, cornerEdges);
return result - overShoot;
}
@Override
public Object clone()
{
final BooleanMask3D result = new BooleanMask3D();
result.bounds = new Rectangle3D.Integer(bounds);
for (Entry<Integer, BooleanMask2D> entry : mask.entrySet())
result.mask.put(entry.getKey(), (BooleanMask2D) entry.getValue().clone());
return result;
}
}