package gdsc.foci;
/*-----------------------------------------------------------------------------
* GDSC Plugins for ImageJ
*
* Copyright (C) 2011 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program 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 2 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
import ij.process.ImageProcessor;
import java.util.Arrays;
/**
* Find objects defined by contiguous pixels of the same value
*/
public class ObjectAnalyzer
{
private ImageProcessor ip;
private boolean eightConnected;
private int[] objectMask;
private int maxObject;
private int minObjectSize = 0;
public ObjectAnalyzer(ImageProcessor ip)
{
this(ip, false);
}
public ObjectAnalyzer(ImageProcessor ip, boolean eightConnected)
{
this.ip = ip;
this.eightConnected = eightConnected;
}
/**
* @return A pixel array containing the object number for each pixel in the input image
*/
public int[] getObjectMask()
{
analyseObjects();
return objectMask;
}
/**
* @return The maximum object number
*/
public int getMaxObject()
{
analyseObjects();
return maxObject;
}
private void analyseObjects()
{
if (objectMask != null)
return;
final int[] maskImage = new int[ip.getPixelCount()];
for (int i = 0; i < maskImage.length; i++)
maskImage[i] = ip.get(i);
// Perform a search for objects.
// Expand any non-zero pixel value into all 8-connected pixels of the same value.
objectMask = new int[maskImage.length];
maxObject = 0;
int[][] ppList = new int[1][];
ppList[0] = new int[100];
initialise(ip);
int[] sizes = new int[100];
for (int i = 0; i < maskImage.length; i++)
{
// Look for non-zero values that are not already in an object
if (maskImage[i] != 0 && objectMask[i] == 0)
{
maxObject++;
int size = expandObjectXY(maskImage, objectMask, i, maxObject, ppList);
if (sizes.length == maxObject)
sizes = Arrays.copyOf(sizes, (int) (maxObject * 1.5));
sizes[maxObject] = size;
}
}
// Remove objects that are too small
if (minObjectSize > 0)
{
int[] map = new int[maxObject + 1];
maxObject = 0;
for (int i = 1; i < map.length; i++)
{
if (sizes[i] >= minObjectSize)
map[i] = ++maxObject;
}
for (int i = 0; i < objectMask.length; i++)
{
if (objectMask[i] != 0)
objectMask[i] = map[objectMask[i]];
}
}
}
/**
* Searches from the specified point to find all coordinates of the same value and assigns them to given maximum ID.
*/
private int expandObjectXY(final int[] image, final int[] objectMask, final int index0, final int id, int[][] ppList)
{
objectMask[index0] = id; // mark first point
int listI = 0; // index of current search element in the list
int listLen = 1; // number of elements in the list
final int neighbours = (eightConnected) ? 8 : 4;
// we create a list of connected points and start the list at the current point
int[] pList = ppList[0];
pList[listI] = index0;
final int v0 = image[index0];
do
{
final int index1 = pList[listI];
final int x1 = index1 % maxx;
final int y1 = index1 / maxx;
boolean isInnerXY = (y1 != 0 && y1 != ylimit) && (x1 != 0 && x1 != xlimit);
for (int d = neighbours; d-- > 0;)
{
if (isInnerXY || isWithinXY(x1, y1, d))
{
int index2 = index1 + offset[d];
if (objectMask[index2] != 0)
{
// This has been done already, ignore this point
continue;
}
int v2 = image[index2];
if (v2 == v0)
{
// Add this to the search
pList[listLen++] = index2;
objectMask[index2] = id;
if (pList.length == listLen)
pList = Arrays.copyOf(pList, (int) (listLen * 1.5));
}
}
}
listI++;
} while (listI < listLen);
ppList[0] = pList;
return listLen;
}
private int maxx, maxy;
private int xlimit, ylimit;
private int[] offset;
private final int[] DIR_X_OFFSET = new int[] { 0, 1, 0, -1, 1, 1, -1, -1 };
private final int[] DIR_Y_OFFSET = new int[] { -1, 0, 1, 0, -1, 1, 1, -1 };
/**
* Creates the direction offset tables.
*/
private void initialise(ImageProcessor ip)
{
maxx = ip.getWidth();
maxy = ip.getHeight();
xlimit = maxx - 1;
ylimit = maxy - 1;
// Create the offset table (for single array 3D neighbour comparisons)
offset = new int[DIR_X_OFFSET.length];
for (int d = offset.length; d-- > 0;)
{
offset[d] = maxx * DIR_Y_OFFSET[d] + DIR_X_OFFSET[d];
}
}
/**
* returns whether the neighbour in a given direction is within the image. NOTE: it is assumed that the pixel x,y
* itself is within the image! Uses class variables xlimit, ylimit: (dimensions of the image)-1
*
* @param x
* x-coordinate of the pixel that has a neighbour in the given direction
* @param y
* y-coordinate of the pixel that has a neighbour in the given direction
* @param direction
* the direction from the pixel towards the neighbour
* @return true if the neighbour is within the image (provided that x, y is within)
*/
private boolean isWithinXY(int x, int y, int direction)
{
switch (direction)
{
// 4-connected directions
case 0:
return (y > 0);
case 1:
return (x < xlimit);
case 2:
return (y < ylimit);
case 3:
return (x > 0);
// Then remaining 8-connected directions
case 4:
return (y > 0 && x < xlimit);
case 5:
return (y < ylimit && x < xlimit);
case 6:
return (y < ylimit && x > 0);
case 7:
return (y > 0 && x > 0);
default:
return false;
}
}
/**
* @return The image width
*/
public int getWidth()
{
return ip.getWidth();
}
/**
* @return The image height
*/
public int getHeight()
{
return ip.getHeight();
}
/**
* Get the centre-of-mass and pixel count of each object. Data is stored indexed by the object value so processing
* of results should start from 1.
*
* @return The centre-of-mass of each object (plus the pixel count) [object][cx,cy,n]
*/
public double[][] getObjectCentres()
{
int[] count = new int[maxObject + 1];
double[] sumx = new double[count.length];
double[] sumy = new double[count.length];
final int maxy = getHeight();
final int maxx = getWidth();
for (int y = 0, i = 0; y < maxy; y++)
for (int x = 0; x < maxx; x++, i++)
{
final int value = objectMask[i];
if (value != 0)
{
sumx[value] += x;
sumy[value] += y;
count[value]++;
}
}
double[][] data = new double[count.length][3];
for (int i = 1; i < count.length; i++)
{
data[i][0] = sumx[i] / count[i];
data[i][1] = sumy[i] / count[i];
data[i][2] = count[i];
}
return data;
}
/**
* @return The minimum object size. Objects below this are removed.
*/
public int getMinObjectSize()
{
return minObjectSize;
}
/**
* @param minObjectSize
* The minimum object size. Objects below this are removed.
*/
public void setMinObjectSize(int minObjectSize)
{
this.minObjectSize = minObjectSize;
}
/**
* @return True if objects should use 8-connected pixels. The default is 4-connected.
*/
public boolean isEightConnected()
{
return eightConnected;
}
/**
* @param eightConnected
* True if objects should use 8-connected pixels. The default is 4-connected.
*/
public void setEightConnected(boolean eightConnected)
{
this.eightConnected = eightConnected;
}
}