Squiz Matrix  4.12.2
 All Data Structures Namespaces Functions Variables Pages
EllipseFitter.java
1 package ij.process;
2 import ij.*;
3 import ij.gui.*;
4 import java.awt.*;
5 import ij.plugin.filter.*;
6 
7 /*
8 Best-fitting ellipse routines by:
9 
10  Bob Rodieck
11  Department of Ophthalmology, RJ-10
12  University of Washington,
13  Seattle, WA, 98195
14 
15 Notes on best-fitting ellipse:
16 
17  Consider some arbitrarily shaped closed profile, which we wish to
18  characterize in a quantitative manner by a series of terms, each
19  term providing a better approximation to the shape of the profile.
20  Assume also that we wish to include the orientation of the profile
21  (i.e. which way is up) in our characterization.
22 
23  One approach is to view the profile as formed by a series harmonic
24  components, much in the same way that one can decompose a waveform
25  over a fixed interval into a series of Fourier harmonics over that
26  interval. From this perspective the first term is the mean radius,
27  or some related value (i.e. the area). The second term is the
28  magnitude and phase of the first harmonic, which is equivalent to the
29  best-fitting ellipse.
30 
31  What constitutes the best-fitting ellipse? First, it should have the
32  same area. In statistics, the measure that attempts to characterize some
33  two-dimensional distribution of data points is the 'ellipse of
34  concentration' (see Cramer, Mathematical Methods of Statistics,
35  Princeton Univ. Press, 945, page 283). This measure equates the second
36  order central moments of the ellipse to those of the distribution,
37  and thereby effectively defines both the shape and size of the ellipse.
38 
39  This technique can be applied to a profile by assuming that it constitutes
40  a uniform distribution of points bounded by the perimeter of the profile.
41  For most 'blob-like' shapes the area of the ellipse is close to that
42  of the profile, differing by no more than about 4%. We can then make
43  a small adjustment to the size of the ellipse, so as to give it the
44  same area as that of the profile. This is what is done here, and
45  therefore this is what we mean by 'best-fitting'.
46 
47  For a real pathologic case, consider a dumbell shape formed by two small
48  circles separated by a thin line. Changing the distance between the
49  circles alters the second order moments, and thus the size of the ellipse
50  of concentration, without altering the area of the profile.
51 
52 public class Ellipse_Fitter implements PlugInFilter {
53  public int setup(String arg, ImagePlus imp) {
54  return DOES_ALL;
55  }
56  public void run(ImageProcessor ip) {
57  EllipseFitter ef = new EllipseFitter();
58  ef.fit(ip);
59  IJ.write(IJ.d2s(ef.major)+" "+IJ.d2s(ef.minor)+" "+IJ.d2s(ef.angle)+" "+IJ.d2s(ef.xCenter)+" "+IJ.d2s(ef.yCenter));
60  ef.drawEllipse(ip);
61  }
62 }
63 */
64 
65 
67 public class EllipseFitter {
68 
69  static final double HALFPI = 1.5707963267949;
70 
72  public double xCenter;
73 
75  public double yCenter;
76 
78  public double major;
79 
81  public double minor;
82 
84  public double angle;
85 
87  public double theta;
88 
90  public int[] xCoordinates;
92  public int[] yCoordinates;
94  public int nCoordinates = 0;
95 
96 
97  private int bitCount;
98  private double xsum, ysum, x2sum, y2sum, xysum;
99  private byte[] mask;
100  private int left, top, width, height;
101  private double n;
102  private double xm, ym; //mean values
103  private double u20, u02, u11; //central moments
104  private ImageProcessor ip;
105  private double pw, ph;
106  private boolean record;
107 
109  public void fit(ImageProcessor ip, ImageStatistics stats) {
110  this.ip = ip;
111  mask = ip.getMaskArray();
112  Rectangle r = ip.getRoi();
113  this.pw = stats.pw;
114  this.ph = stats.ph;
115  left = r.x;
116  top = r.y;
117  width = r.width;
118  height = r.height;
119  getEllipseParam();
120  }
121 
122  void getEllipseParam() {
123  double sqrtPi = 1.772453851;
124  double a11, a12, a22, m4, z, scale, tmp, xoffset, yoffset;
125  double RealAngle;
126 
127  if (mask==null) {
128  major = (width*2) / sqrtPi;
129  minor = (height*2) / sqrtPi; // * Info->PixelAspectRatio;
130  angle = 0.0;
131  theta = 0.0;
132  if (major < minor) {
133  tmp = major;
134  major = minor;
135  minor = tmp;
136  angle = 90.0;
137  theta = Math.PI/2.0;
138  }
139  xCenter = left + width / 2.0;
140  yCenter = top + height / 2.0;
141  return;
142  }
143 
144  computeSums();
145  getMoments();
146  m4 = 4.0 * Math.abs(u02 * u20 - u11 * u11);
147  if (m4 < 0.000001)
148  m4 = 0.000001;
149  a11 = u02 / m4;
150  a12 = u11 / m4;
151  a22 = u20 / m4;
152  xoffset = xm;
153  yoffset = ym;
154 
155  tmp = a11 - a22;
156  if (tmp == 0.0)
157  tmp = 0.000001;
158  theta = 0.5 * Math.atan(2.0 * a12 / tmp);
159  if (theta < 0.0)
160  theta += HALFPI;
161  if (a12 > 0.0)
162  theta += HALFPI;
163  else if (a12 == 0.0) {
164  if (a22 > a11) {
165  theta = 0.0;
166  tmp = a22;
167  a22 = a11;
168  a11 = tmp;
169  } else if (a11 != a22)
170  theta = HALFPI;
171  }
172  tmp = Math.sin(theta);
173  if (tmp == 0.0)
174  tmp = 0.000001;
175  z = a12 * Math.cos(theta) / tmp;
176  major = Math.sqrt (1.0 / Math.abs(a22 + z));
177  minor = Math.sqrt (1.0 / Math.abs(a11 - z));
178  scale = Math.sqrt (bitCount / (Math.PI * major * minor)); //equalize areas
179  major = major*scale*2.0;
180  minor = minor*scale*2.0;
181  angle = 180.0 * theta / Math.PI;
182  if (angle == 180.0)
183  angle = 0.0;
184  if (major < minor) {
185  tmp = major;
186  major = minor;
187  minor = tmp;
188  }
189  xCenter = left + xoffset + 0.5;
190  yCenter = top + yoffset + 0.5;
191  }
192 
193  void computeSums () {
194  xsum = 0.0;
195  ysum = 0.0;
196  x2sum = 0.0;
197  y2sum = 0.0;
198  xysum = 0.0;
199  int bitcountOfLine;
200  double xe, ye;
201  int xSumOfLine;
202  for (int y=0; y<height; y++) {
203  bitcountOfLine = 0;
204  xSumOfLine = 0;
205  int offset = y*width;
206  for (int x=0; x<width; x++) {
207  if (mask[offset+x] != 0) {
208  bitcountOfLine++;
209  xSumOfLine += x;
210  x2sum += x * x;
211  }
212  }
213  xsum += xSumOfLine;
214  ysum += bitcountOfLine * y;
215  ye = y;
216  xe = xSumOfLine;
217  xysum += xe*ye;
218  y2sum += ye*ye*bitcountOfLine;
219  bitCount += bitcountOfLine;
220  }
221  }
222 
223  void getMoments () {
224  double x1, y1, x2, y2, xy;
225 
226  if (bitCount == 0)
227  return;
228 
229  x2sum += 0.08333333 * bitCount;
230  y2sum += 0.08333333 * bitCount;
231  n = bitCount;
232  x1 = xsum/n;
233  y1 = ysum / n;
234  x2 = x2sum / n;
235  y2 = y2sum / n;
236  xy = xysum / n;
237  xm = x1;
238  ym = y1;
239  u20 = x2 - (x1 * x1);
240  u02 = y2 - (y1 * y1);
241  u11 = xy - x1 * y1;
242  }
243 
244  /*
245  basic equations:
246 
247  a: major axis
248  b: minor axis
249  t: theta, angle of major axis, clockwise with respect to x axis.
250 
251  g11*x^2 + 2*g12*x*y + g22*y^2 = 1 -- equation of ellipse
252 
253  g11:= ([cos(t)]/a)^2 + ([sin(t)]/b)^2
254  g12:= (1/a^2 - 1/b^2) * sin(t) * cos(t)
255  g22:= ([sin(t)]/a)^2 + ([cos(t)]/b)^2
256 
257  solving for x: x:= k1*y sqrt( k2*y^2 + k3 )
258 
259  where: k1:= -g12/g11
260  k2:= (g12^2 - g11*g22)/g11^2
261  k3:= 1/g11
262 
263  ymax or ymin occur when there is a single value for x, that is when:
264  k2*y^2 + k3 = 0
265  */
266 
268  public void drawEllipse(ImageProcessor ip) {
269  if (major==0.0 && minor==0.0)
270  return;
271  int xc = (int)Math.round(xCenter);
272  int yc = (int)Math.round(yCenter);
273  int maxY = ip.getHeight();
274  int xmin, xmax;
275  double sint, cost, rmajor2, rminor2, g11, g12, g22, k1, k2, k3;
276  int x, xsave, ymin, ymax;
277  int[] txmin = new int[maxY];
278  int[] txmax = new int[maxY];
279  double j1, j2, yr;
280 
281  sint = Math.sin(theta);
282  cost = Math.cos(theta);
283  rmajor2 = 1.0 / sqr(major/2);
284  rminor2 = 1.0 / sqr(minor/2);
285  g11 = rmajor2 * sqr(cost) + rminor2 * sqr(sint);
286  g12 = (rmajor2 - rminor2) * sint * cost;
287  g22 = rmajor2 * sqr(sint) + rminor2 * sqr(cost);
288  k1 = -g12 / g11;
289  k2 = (sqr(g12) - g11 * g22) / sqr(g11);
290  k3 = 1.0 / g11;
291  ymax = (int)Math.floor(Math.sqrt(Math.abs(k3 / k2)));
292  if (ymax>maxY)
293  ymax = maxY;
294  if (ymax<1)
295  ymax = 1;
296  ymin = -ymax;
297  // Precalculation and use of symmetry speed things up
298  for (int y=0; y<=ymax; y++) {
299  //GetMinMax(y, aMinMax);
300  j2 = Math.sqrt(k2 * sqr(y) + k3);
301  j1 = k1 * y;
302  txmin[y] = (int)Math.round(j1 - j2);
303  txmax[y] = (int)Math.round(j1 + j2);
304  }
305  if (record) {
306  xCoordinates[nCoordinates] = xc + txmin[ymax - 1];
307  yCoordinates[nCoordinates] = yc + ymin;
308  nCoordinates++;
309  } else
310  ip.moveTo(xc + txmin[ymax - 1], yc + ymin);
311  for (int y=ymin; y<ymax; y++) {
312  x = y<0?txmax[-y]:-txmin[y];
313  if (record) {
314  xCoordinates[nCoordinates] = xc + x;
315  yCoordinates[nCoordinates] = yc + y;
316  nCoordinates++;
317  } else
318  ip.lineTo(xc + x, yc + y);
319  }
320  for (int y=ymax; y>ymin; y--) {
321  x = y<0?txmin[-y]:-txmax[y];
322  if (record) {
323  xCoordinates[nCoordinates] = xc + x;
324  yCoordinates[nCoordinates] = yc + y;
325  nCoordinates++;
326  } else
327  ip.lineTo(xc + x, yc + y);
328  }
329  }
330 
333  public void makeRoi(ImageProcessor ip) {
334  record = true;
335  int size = ip.getHeight()*3;
336  xCoordinates = new int[size];
337  yCoordinates = new int[size];
338  nCoordinates = 0;
339  drawEllipse(ip);
340  record = false;
341  }
342 
343  private double sqr(double x) {
344  return x*x;
345  }
346 
347 }