/
Roi.java
3007 lines (2764 loc) · 87.8 KB
/
Roi.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package ij.gui;
import ij.*;
import ij.process.*;
import ij.measure.*;
import ij.plugin.*;
import ij.plugin.frame.Recorder;
import ij.plugin.frame.RoiManager;
import ij.plugin.filter.Analyzer;
import ij.plugin.filter.ThresholdToSelection;
import ij.macro.Interpreter;
import ij.io.RoiDecoder;
import java.awt.*;
import java.util.*;
import java.io.*;
import java.awt.image.*;
import java.awt.event.*;
import java.awt.geom.*;
/**
* A rectangular region of interest and superclass for the other ROI classes.
*
* This class implements {@code Iterable<Point>} and can thus be
* used to iterate over the contained coordinates. Usage example:
* <pre>
* Roi roi = ...;
* for (Point p : roi) {
* // process p
* }
* </pre>
* <b>
* Convention for subpixel resolution and zooming in:
* </b><ul>
* <li> Area ROIs: Integer coordinates refer to the top-left corner of the pixel with these coordinates.
* Thus, pixel (0,0) is enclosed by the rectangle spanned between points (0,0) and (1,1),
* i.e., a rectangle at (0,0) with width = height = 1 pixel.
* <li> Line and Point Rois: Integer coordinates refer to the center of a pixel.
* Thus, a line from (0,0) to (1,0) has its start and end points in the center of
* pixels (0,0) and (1,0), respectively, and drawing the line should affect both
* pixels. For images dispplayed at high zoom levels, this means that (open) lines
* and single points are displayed 0.5 pixels further to the right and bottom than
* the outlines of area ROIs (closed lines) with the same coordinates.
* </ul>
* Note that rectangular and (nonrotated) oval ROIs do not support subpixel resolution.
* Since ImageJ 1.52t, this convention does not depend on the Prefs.subpixelResolution
* (previously accessible via Edit>Options>Plot) and this flag has no effect any more.
*
*/
public class Roi extends Object implements Cloneable, java.io.Serializable, Iterable<Point> {
public static final int CONSTRUCTING=0, MOVING=1, RESIZING=2, NORMAL=3, MOVING_HANDLE=4; // States
public static final int RECTANGLE=0, OVAL=1, POLYGON=2, FREEROI=3, TRACED_ROI=4, LINE=5,
POLYLINE=6, FREELINE=7, ANGLE=8, COMPOSITE=9, POINT=10; // Types
public static final int HANDLE_SIZE = 5; // replaced by getHandleSize()
public static final int NOT_PASTING = -1;
public static final int FERET_ARRAYSIZE = 16; // Size of array with Feret values
public static final int FERET_ARRAY_POINTOFFSET = 8; // Where point coordinates start in Feret array
private static final String NAMES_KEY = "group.names";
static final int NO_MODS=0, ADD_TO_ROI=1, SUBTRACT_FROM_ROI=2; // modification states
int startX, startY, x, y, width, height;
double startXD, startYD;
Rectangle2D.Double bounds;
int activeHandle;
int state;
int modState = NO_MODS;
int cornerDiameter; //for rounded rectangle
int previousSX, previousSY; //remember for aborting moving with esc and constrain
public static final BasicStroke onePixelWide = new BasicStroke(1);
protected static Color ROIColor = Prefs.getColor(Prefs.ROICOLOR,Color.yellow);
protected static int pasteMode = Blitter.COPY;
protected static int lineWidth = 1;
protected static Color defaultFillColor;
private static Vector listeners = new Vector();
private static LUT glasbeyLut;
private static int defaultGroup; // zero is no specific group
private static Color groupColor;
private static double defaultStrokeWidth;
private static String groupNamesString = Prefs.get(NAMES_KEY, null);
private static String[] groupNames;
private static boolean groupNamesChanged;
/** Get using getPreviousRoi() and set using setPreviousRoi() */
public static Roi previousRoi;
protected int type;
protected int xMax, yMax;
protected ImagePlus imp;
private int imageID;
protected ImageCanvas ic;
protected int oldX, oldY, oldWidth, oldHeight; //remembers previous clip rect
protected int clipX, clipY, clipWidth, clipHeight;
protected ImagePlus clipboard;
protected boolean constrain; // to be square or limit to horizontal/vertical motion
protected boolean center;
protected boolean aspect;
protected boolean updateFullWindow;
protected double mag = 1.0;
protected double asp_bk; //saves aspect ratio if resizing takes roi very small
protected ImageProcessor cachedMask;
protected Color handleColor = Color.white;
protected Color strokeColor;
protected Color instanceColor; //obsolete; replaced by strokeColor
protected Color fillColor;
protected BasicStroke stroke;
protected boolean nonScalable;
protected boolean overlay;
protected boolean wideLine;
protected boolean ignoreClipRect;
protected double flattenScale = 1.0;
protected static Color defaultColor;
private String name;
private int position;
private int channel, slice, frame;
private boolean hyperstackPosition;
private Overlay prototypeOverlay;
private boolean subPixel;
private boolean activeOverlayRoi;
private Properties props;
private boolean isCursor;
private double xcenter = Double.NaN;
private double ycenter;
private boolean listenersNotified;
private boolean antiAlias = true;
private int group;
private boolean usingDefaultStroke;
private static int defaultHandleSize;
private int handleSize = -1;
private boolean scaleStrokeWidth; // Scale stroke width when zooming images?
/** Creates a rectangular ROI. */
public Roi(int x, int y, int width, int height) {
this(x, y, width, height, 0);
}
/** Creates a rectangular ROI using double arguments. */
public Roi(double x, double y, double width, double height) {
this(x, y, width, height, 0);
}
/** Creates a new rounded rectangular ROI. */
public Roi(int x, int y, int width, int height, int cornerDiameter) {
setImage(null);
if (width<1) width = 1;
if (height<1) height = 1;
if (width>xMax) width = xMax;
if (height>yMax) height = yMax;
this.cornerDiameter = cornerDiameter;
this.x = x;
this.y = y;
startX = x; startY = y;
oldX = x; oldY = y; oldWidth=0; oldHeight=0;
this.width = width;
this.height = height;
oldWidth=width;
oldHeight=height;
clipX = x;
clipY = y;
clipWidth = width;
clipHeight = height;
state = NORMAL;
type = RECTANGLE;
if (ic!=null) {
Graphics g = ic.getGraphics();
draw(g);
g.dispose();
}
double defaultWidth = defaultStrokeWidth();
if (defaultWidth>0) {
stroke = new BasicStroke((float)defaultWidth);
usingDefaultStroke = true;
}
fillColor = defaultFillColor;
this.group = defaultGroup; //initialize with current group and associated color
if (defaultGroup>0)
this.strokeColor = groupColor;
}
/** Creates a rounded rectangular ROI using double arguments. */
public Roi(double x, double y, double width, double height, int cornerDiameter) {
this((int)x, (int)y, (int)Math.ceil(width), (int)Math.ceil(height), cornerDiameter);
bounds = new Rectangle2D.Double(x, y, width, height);
subPixel = true;
}
/** Creates a new rectangular Roi. */
public Roi(Rectangle r) {
this(r.x, r.y, r.width, r.height);
}
/** Starts the process of creating a user-defined rectangular Roi,
where sx and sy are the starting screen coordinates. */
public Roi(int sx, int sy, ImagePlus imp) {
this(sx, sy, imp, 0);
}
/** Starts the process of creating a user-defined rectangular Roi,
where sx and sy are the starting screen coordinates.
For rectangular rois, also a corner diameter may be specified to
make it a rounded rectangle */
public Roi(int sx, int sy, ImagePlus imp, int cornerDiameter) {
setImage(imp);
int ox=sx, oy=sy;
if (ic!=null) {
ox = ic.offScreenX2(sx);
oy = ic.offScreenY2(sy);
}
setLocation(ox, oy);
this.cornerDiameter = cornerDiameter;
width = 0;
height = 0;
state = CONSTRUCTING;
type = RECTANGLE;
if (cornerDiameter>0) {
double swidth = RectToolOptions.getDefaultStrokeWidth();
if (swidth>0.0)
setStrokeWidth(swidth);
Color scolor = RectToolOptions.getDefaultStrokeColor();
if (scolor!=null)
setStrokeColor(scolor);
}
double defaultWidth = defaultStrokeWidth();
//if (defaultWidth>0) setStrokeWidth(defaultWidth);
if (defaultWidth>0) {
stroke = new BasicStroke((float)defaultWidth);
usingDefaultStroke = true;
}
fillColor = defaultFillColor;
this.group = defaultGroup;
if (defaultGroup>0)
this.strokeColor = groupColor;
}
/** Creates a rectangular ROI. */
public static Roi create(double x, double y, double width, double height) {
return new Roi(x, y, width, height);
}
/** Creates a rounded rectangular ROI. */
public static Roi create(double x, double y, double width, double height, int cornerDiameter) {
return new Roi(x, y, width, height, cornerDiameter);
}
/** @deprecated */
public Roi(int x, int y, int width, int height, ImagePlus imp) {
this(x, y, width, height);
setImage(imp);
}
/** Set the location of the ROI in image coordinates. */
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
startX = x; startY = y;
oldX = x; oldY = y; oldWidth=0; oldHeight=0;
if (bounds!=null) {
if (!isInteger(bounds.x) || !isInteger(bounds.y)) {
cachedMask = null;
width = (int)Math.ceil(bounds.width);
height = (int)Math.ceil(bounds.height);
}
bounds.x = x;
bounds.y = y;
if (this instanceof PolygonRoi) setIntBounds(bounds);
}
}
/** Set the location of the ROI in image coordinates. */
public void setLocation(double x, double y) {
setLocation((int)x, (int)y);
if (isInteger(x) && isInteger(y))
return;
if (bounds!=null) {
if (!isInteger(x-bounds.x) || !isInteger(y-bounds.y)) {
cachedMask = null;
width = (int)Math.ceil(bounds.x + bounds.width) - this.x; //ensure that all pixels are inside
height = (int)Math.ceil(bounds.y + bounds.height) - this.y;
}
bounds.x = x;
bounds.y = y;
} else {
cachedMask = null;
bounds = new Rectangle2D.Double(x, y, width, height);
}
if (this instanceof PolygonRoi) setIntBounds(bounds);
subPixel = true;
}
public void translate(double dx, double dy) {
boolean intArgs = (int)dx==dx && (int)dy==dy;
if (subPixelResolution() || !intArgs) {
Rectangle2D r = getFloatBounds();
setLocation(r.getX()+dx, r.getY()+dy);
} else {
Rectangle r = getBounds();
setLocation(r.x+(int)dx, r.y+(int)dy);
}
}
/** Sets the ImagePlus associated with this ROI.
* <code>imp</code> may be null to remove the association to an image. */
public void setImage(ImagePlus imp) {
this.imp = imp;
cachedMask = null;
if (imp==null) {
ic = null;
clipboard = null;
xMax = yMax = Integer.MAX_VALUE;
} else {
ic = imp.getCanvas();
xMax = imp.getWidth();
yMax = imp.getHeight();
}
}
/** Returns the ImagePlus associated with this ROI, or null. */
public ImagePlus getImage() {
return imp;
}
/** Returns the ID of the image associated with this ROI. */
public int getImageID() {
ImagePlus imp = this.imp;
return imp!=null?imp.getID():imageID;
}
public int getType() {
return type;
}
public int getState() {
return state;
}
/** Returns the perimeter length. */
public double getLength() {
double pw=1.0, ph=1.0;
if (imp!=null) {
Calibration cal = imp.getCalibration();
pw = cal.pixelWidth;
ph = cal.pixelHeight;
}
double perimeter = 2.0*width*pw + 2.0*height*ph;
if (cornerDiameter > 0) { //using Ramanujan's approximation for the circumference of an ellipse
double a = 0.5*Math.min(cornerDiameter, width)*pw;
double b = 0.5*Math.min(cornerDiameter, height)*ph;
perimeter += Math.PI*(3*(a + b) - Math.sqrt((3*a + b)*(a + 3*b))) -4*(a+b);
}
return perimeter;
}
/** Returns Feret's diameter, the greatest distance between
any two points along the ROI boundary. */
public double getFeretsDiameter() {
double[] a = getFeretValues();
return a!=null?a[0]:0.0;
}
/** Returns an array with the following values:
* <br>[0] "Feret" (maximum caliper width)
* <br>[1] "FeretAngle" (angle of diameter with maximum caliper width, between 0 and 180 deg)
* <br>[2] "MinFeret" (minimum caliper width)
* <br>[3][4] , "FeretX" and "FeretY", the X and Y coordinates of the starting point
* (leftmost point) of the maximum-caliper-width diameter.
* <br>[5-7] reserved
* <br>All these values and point coordinates are in calibrated image coordinates.
* <p>
* The following array elements are end points of the maximum and minimum caliper diameter,
* in unscaled image pixel coordinates:
* <br>[8][9] "FeretX1", "FeretY1"; unscaled versions of "FeretX" and "FeretY"
* (subclasses may use any end of the diameter, not necessarily the left one)
* <br>[10][11] "FeretX2", "FeretY2", end point of the maxium-caliper-width diameter.
* Both of these points are vertices of the convex hull.
* <br> The final four array elements are the starting and end points of the minimum caliper width,
* <br>[12],[13] "MinFeretX", "MinFeretY", and
* <br>[14],[15] "MinFeretX2", "MinFeretY2". These two pooints are not sorted by x,
* but the first point point (MinFeretX, MinFeretY) is guaranteed to be a vertex of the convex hull,
* while second point (MinFeretX2, MinFeretY2) usually is not a vertex point but at a
* boundary line of the convex hull. */
public double[] getFeretValues() {
double pw=1.0, ph=1.0;
if (imp!=null) {
Calibration cal = imp.getCalibration();
pw = cal.pixelWidth;
ph = cal.pixelHeight;
}
FloatPolygon poly = getFloatConvexHull();
if (poly==null || poly.npoints==0) return null;
double[] a = new double[FERET_ARRAYSIZE];
// calculate maximum Feret diameter: largest distance between any two points
int p1=0, p2=0;
double diameterSqr = 0.0; //square of maximum Feret diameter
for (int i=0; i<poly.npoints; i++) {
for (int j=i+1; j<poly.npoints; j++) {
double dx = (poly.xpoints[i] - poly.xpoints[j])*pw;
double dy = (poly.ypoints[i] - poly.ypoints[j])*ph;
double dsqr = dx*dx + dy*dy;
if (dsqr>diameterSqr) {diameterSqr=dsqr; p1=i; p2=j;}
}
}
if (poly.xpoints[p1] > poly.xpoints[p2]) {
int p2swap = p1; p1 = p2; p2 = p2swap;
}
double xf1=poly.xpoints[p1], yf1=poly.ypoints[p1];
double xf2=poly.xpoints[p2], yf2=poly.ypoints[p2];
double angle = (180.0/Math.PI)*Math.atan2((yf1-yf2)*ph, (xf2-xf1)*pw);
if (angle < 0.0)
angle += 180.0;
a[0] = Math.sqrt(diameterSqr);
a[1] = angle;
a[3] = xf1; a[4] = yf1;
{ int i = FERET_ARRAY_POINTOFFSET; //array elements 8-11 are start and end points of max Feret diameter
a[i++] = poly.xpoints[p1]; a[i++] = poly.ypoints[p1];
a[i++] = poly.xpoints[p2]; a[i++] = poly.ypoints[p2];
}
// Calculate minimum Feret diameter:
// For all pairs of points on the convex hull:
// Get the point with the largest distance from the line between these two points
// Of all these pairs, take the one where the distance is the lowest
// The following code requires a counterclockwise convex hull with no duplicate points
double x0 = poly.xpoints[poly.npoints-1];
double y0 = poly.ypoints[poly.npoints-1];
double minFeret = Double.MAX_VALUE;
double[] xyEnd = new double[4]; //start and end points of the minFeret diameter, uncalibrated
double[] xyEi = new double[4]; //intermediate values of xyEnd
for (int i=0; i<poly.npoints; i++) { //find caliper width for one side of calipers touching points i-1, i
double xprev = x0;
double yprev = y0;
x0 = poly.xpoints[i];
y0 = poly.ypoints[i];
double xnorm = (y0 - yprev) * ph;
double ynorm = (xprev - x0) * pw;
double normalizationFactor = 1/Math.sqrt(xnorm*xnorm + ynorm*ynorm);
xnorm *= normalizationFactor * pw; //normalized vector perpendicular to line between i-1, i; * scale factor for product below
ynorm *= normalizationFactor * ph;
double maxDist = 0;
for (int j=0; j<poly.npoints; j++) {
double x1 = poly.xpoints[j];
double y1 = poly.ypoints[j];
double dx = x1 - x0;
double dy = y1 - y0;
double dist = dx*xnorm + dy*ynorm;
if (dist > maxDist) {
maxDist = dist;
xyEi[0] = x1;
xyEi[1] = y1;
xyEi[2] = xyEi[0] - (xnorm/pw * dist)/pw;
xyEi[3] = xyEi[1] - (ynorm/ph * dist)/ph;
}
}
if (maxDist < minFeret) {
minFeret = maxDist;
System.arraycopy(xyEi, 0, xyEnd, 0, 4);
}
}
a[2] = minFeret;
System.arraycopy(xyEnd, 0, a, FERET_ARRAY_POINTOFFSET+4, 4); //a[12]-a[15] are minFeretX, Y, X2, Y2
return a;
}
/** Returns the convex hull of this Roi as a Polygon with integer coordinates
* by rounding the floating-point values.
* Coordinates of the convex hull are image pixel coordinates. */
public Polygon getConvexHull() {
FloatPolygon fp = getFloatConvexHull();
return new Polygon(toIntR(fp.xpoints), toIntR(fp.ypoints), fp.npoints);
}
/** Returns the convex hull of this Roi as a FloatPolygon.
* Coordinates of the convex hull are image pixel coordinates. */
public FloatPolygon getFloatConvexHull() {
FloatPolygon fp = getFloatPolygon(""); //no duplicate closing points, no path-separating NaNs needed
return fp == null ? null : fp.getConvexHull();
}
double getFeretBreadth(Shape shape, double angle, double x1, double y1, double x2, double y2) {
double cx = x1 + (x2-x1)/2;
double cy = y1 + (y2-y1)/2;
AffineTransform at = new AffineTransform();
at.rotate(angle*Math.PI/180.0, cx, cy);
Shape s = at.createTransformedShape(shape);
Rectangle2D r = s.getBounds2D();
return Math.min(r.getWidth(), r.getHeight());
}
/** Returns this selection's bounding rectangle. */
public Rectangle getBounds() {
return new Rectangle(x, y, width, height);
}
/** Returns this selection's bounding rectangle (with subpixel accuracy). */
public Rectangle2D.Double getFloatBounds() {
if (bounds!=null)
return new Rectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height);
else
return new Rectangle2D.Double(x, y, width, height);
}
/** Sets the bounds of rectangular, oval or text selections.
* Note that for these types, subpixel resolution is ignored,
* and the x,y values are rounded down, the width and height values rounded up.
* Do not use for other ROI types since their width and height are results of
* a calculation.
* For translating ROIs, use setLocation. */
public void setBounds(Rectangle2D.Double b) {
if (!(type==RECTANGLE||type==OVAL||(this instanceof TextRoi)))
return;
this.x = (int)b.x;
this.y = (int)b.y;
this.width = (int)Math.ceil(b.width);
this.height = (int)Math.ceil(b.height);
bounds = new Rectangle2D.Double(b.x, b.y, b.width, b.height);
cachedMask = null;
}
/** Sets the integer boundaries x, y, width, height from given sub-pixel
* boundaries, such that all points are within the integer bounding rectangle.
* For open line selections and (multi)Point Rois, note that integer Roi
* coordinates correspond to the center of the 1x1 rectangle enclosing a pixel.
* Points at the boundary of such a rectangle are counted for the higher x or y
* value, in agreement to how (poly-)line or PointRois are displayed at the
* screen at high zoom levels. (For lines and points, it should include all
* pixels affected by 'draw' */
void setIntBounds(Rectangle2D.Double bounds) {
if (useLineSubpixelConvention()) { //for PointRois & open lines, ensure the 'draw' area is enclosed
x = (int)Math.floor(bounds.x + 0.5);
y = (int)Math.floor(bounds.y + 0.5);
width = (int)Math.floor(bounds.x + bounds.width + 1.5) - x;
height = (int)Math.floor(bounds.y + bounds.height + 1.5) - y;
} else { //for area Rois, the subpixel bounds must be enclosed in the int bounds
x = (int)Math.floor(bounds.x);
y = (int)Math.floor(bounds.y);
width = (int)Math.ceil(bounds.x + bounds.width) - x;
height = (int)Math.ceil(bounds.y + bounds.height) - y;
}
}
/**
* @deprecated
* replaced by getBounds()
*/
public Rectangle getBoundingRect() {
return getBounds();
}
/** Returns the outline of this selection as a Polygon.
@see ij.process.ImageProcessor#setRoi
@see ij.process.ImageProcessor#drawPolygon
@see ij.process.ImageProcessor#fillPolygon
*/
public Polygon getPolygon() {
int[] xpoints = new int[4];
int[] ypoints = new int[4];
xpoints[0] = x;
ypoints[0] = y;
xpoints[1] = x+width;
ypoints[1] = y;
xpoints[2] = x+width;
ypoints[2] = y+height;
xpoints[3] = x;
ypoints[3] = y+height;
return new Polygon(xpoints, ypoints, 4);
}
/** Returns the outline of this selection as a FloatPolygon */
public FloatPolygon getFloatPolygon() {
if (cornerDiameter>0) { // Rounded Rectangle
ShapeRoi s = new ShapeRoi(this);
return s.getFloatPolygon();
} else if (subPixelResolution() && bounds!=null) {
float[] xpoints = new float[4];
float[] ypoints = new float[4];
xpoints[0] = (float)bounds.x;
ypoints[0] = (float)bounds.y;
xpoints[1] = (float)(bounds.x+bounds.width);
ypoints[1] = (float)bounds.y;
xpoints[2] = (float)(bounds.x+bounds.width);
ypoints[2] = (float)(bounds.y+bounds.height);
xpoints[3] = (float)bounds.x;
ypoints[3] = (float)(bounds.y+bounds.height);
return new FloatPolygon(xpoints, ypoints);
} else {
Polygon p = getPolygon();
return new FloatPolygon(toFloat(p.xpoints), toFloat(p.ypoints), p.npoints);
}
}
/** Returns the outline in image pixel coordinates,
* where options may include "close" to add a point to close the outline
* if this is an area roi and the outline is not closed yet.
* (For ShapeRois, "separate" inserts NaN values between subpaths). */
public FloatPolygon getFloatPolygon(String options) {
options = options.toLowerCase();
boolean addPointForClose = options.indexOf("close") >= 0;
FloatPolygon fp = getFloatPolygon();
int n = fp.npoints;
if (isArea() && n > 1) {
boolean isClosed = fp.xpoints[0] == fp.xpoints[n-1] && fp.ypoints[0] == fp.ypoints[n-1];
if (addPointForClose && !isClosed)
fp.addPoint(fp.xpoints[0], fp.ypoints[0]);
else if (!addPointForClose && isClosed)
fp.npoints--;
}
return fp;
}
/** Returns, as a FloatPolygon, an interpolated version
* of this selection that has points spaced 1.0 pixel apart.
*/
public FloatPolygon getInterpolatedPolygon() {
return getInterpolatedPolygon(1.0, false);
}
/** Returns, as a FloatPolygon, an interpolated version of
* this selection with points spaced 'interval' pixels apart.
* If 'smooth' is true, traced and freehand selections are
* first smoothed using a 3 point running average.
*/
public FloatPolygon getInterpolatedPolygon(double interval, boolean smooth) {
FloatPolygon p = (this instanceof Line)?((Line)this).getFloatPoints():getFloatPolygon();
return getInterpolatedPolygon(p, interval, smooth);
}
/**
* Returns, as a FloatPolygon, an interpolated version of this selection
* with points spaced abs('interval') pixels apart. If 'smooth' is true, traced
* and freehand selections are first smoothed using a 3 point running
* average.
* If 'interval' is negative, the program is allowed to decrease abs('interval')
* so that the last segment will hit the end point
*/
protected FloatPolygon getInterpolatedPolygon(FloatPolygon p, double interval, boolean smooth) {
boolean allowToAdjust = interval < 0;
interval = Math.abs(interval);
boolean isLine = this.isLine();
double length = p.getLength(isLine);
int npoints = p.npoints;
if (npoints<2)
return p;
if (Math.abs(interval)<0.01) {
IJ.error("Interval must be >= 0.01");
return p;
}
if (!isLine) {//**append (and later remove) closing point to end of array
npoints++;
p.xpoints = java.util.Arrays.copyOf(p.xpoints, npoints);
p.xpoints[npoints - 1] = p.xpoints[0];
p.ypoints = java.util.Arrays.copyOf(p.ypoints, npoints);
p.ypoints[npoints - 1] = p.ypoints[0];
}
int npoints2 = (int) (10 + (length * 1.5) / interval);//allow some headroom
double tryInterval = interval;
double minDiff = 1e9;
double bestInterval = 0;
int srcPtr = 0;//index of source polygon
int destPtr = 0;//index of destination polygon
double[] destXArr = new double[npoints2];
double[] destYArr = new double[npoints2];
int nTrials = 50;
int trial = 0;
while (trial <= nTrials) {
destXArr[0] = p.xpoints[0];
destYArr[0] = p.ypoints[0];
srcPtr = 0;
destPtr = 0;
double xA = p.xpoints[0];//start of current segment
double yA = p.ypoints[0];
while (srcPtr < npoints - 1) {//collect vertices
double xC = destXArr[destPtr];//center circle
double yC = destYArr[destPtr];
double xB = p.xpoints[srcPtr + 1];//end of current segment
double yB = p.ypoints[srcPtr + 1];
double[] intersections = lineCircleIntersection(xA, yA, xB, yB, xC, yC, tryInterval, true);
if (intersections.length >= 2) {
xA = intersections[0];//only use first of two intersections
yA = intersections[1];
destPtr++;
destXArr[destPtr] = xA;
destYArr[destPtr] = yA;
} else {
srcPtr++;//no intersection found, pick next segment
xA = p.xpoints[srcPtr];
yA = p.ypoints[srcPtr];
}
}
destPtr++;
destXArr[destPtr] = p.xpoints[npoints - 1];
destYArr[destPtr] = p.ypoints[npoints - 1];
destPtr++;
if (!allowToAdjust) {
if (isLine)
destPtr--;
break;
}
int nSegments = destPtr - 1;
double dx = destXArr[destPtr - 2] - destXArr[destPtr - 1];
double dy = destYArr[destPtr - 2] - destYArr[destPtr - 1];
double lastSeg = Math.sqrt(dx * dx + dy * dy);
double diff = lastSeg - tryInterval;//always <= 0
if (Math.abs(diff) < minDiff) {
minDiff = Math.abs(diff);
bestInterval = tryInterval;
}
double feedBackFactor = 0.66;//factor <1: applying soft successive approximation
tryInterval = tryInterval + feedBackFactor * diff / nSegments;
//stop if tryInterval < 80% of interval, OR if last segment differs < 0.05 pixels
if ((tryInterval < 0.8 * interval || Math.abs(diff) < 0.05 || trial == nTrials - 1) && trial < nTrials) {
trial = nTrials;//run one more loop with bestInterval to get best polygon
tryInterval = bestInterval;
} else
trial++;
}
if (!isLine) //**remove closing point from end of array
destPtr--;
float[] xPoints = new float[destPtr];
float[] yPoints = new float[destPtr];
for (int jj = 0; jj < destPtr; jj++) {
xPoints[jj] = (float) destXArr[jj];
yPoints[jj] = (float) destYArr[jj];
}
FloatPolygon fPoly = new FloatPolygon(xPoints, yPoints);
return fPoly;
}
/** Returns the coordinates of the pixels inside this ROI as an array of Points.
* @see #getContainedFloatPoints
* @see #iterator
*/
public Point[] getContainedPoints() {
Roi roi = this;
if (isLine())
roi = convertLineToArea(this);
ImageProcessor mask = roi.getMask();
Rectangle bounds = roi.getBounds();
ArrayList points = new ArrayList();
for (int y=0; y<bounds.height; y++) {
for (int x=0; x<bounds.width; x++) {
if (mask==null || mask.getPixel(x,y)!=0)
points.add(new Point(roi.x+x,roi.y+y));
}
}
return (Point[])points.toArray(new Point[points.size()]);
}
/** Returns the coordinates of the pixels inside this ROI as a FloatPolygon.
* @see #getContainedPoints
* @see #iterator
*/
public FloatPolygon getContainedFloatPoints() {
Roi roi2 = this;
if (isLine()) {
if (getStrokeWidth()<=1)
return roi2.getInterpolatedPolygon();
else
roi2 = convertLineToArea(this);
}
ImageProcessor mask = roi2.getMask();
Rectangle bounds = roi2.getBounds();
FloatPolygon points = new FloatPolygon();
for (int y=0; y<bounds.height; y++) {
for (int x=0; x<bounds.width; x++) {
if (mask==null || mask.getPixel(x,y)!=0)
points.addPoint((float)(bounds.x+x),(float)(bounds.y+y));
}
}
return points;
}
/**
* <pre>
* Calculates intersections of a line segment with a circle
* Author N.Vischer
* ax, ay, bx, by: points A and B of line segment
* cx, cy, rad: Circle center and radius.
* ignoreOutside: if true, ignores intersections outside the line segment A-B
* Returns an array of 0, 2 or 4 coordinates (for 0, 1, or 2 intersection
* points). If two intersection points are returned, they are listed in travel
* direction A->B
* </pre>
*/
public static double[] lineCircleIntersection(double ax, double ay, double bx, double by, double cx, double cy, double rad, boolean ignoreOutside) {
//rotates & translates points A, B and C, creating new points A2, B2 and C2.
//A2 is then on origin, and B2 is on positive x-axis
double dxAC = cx - ax;
double dyAC = cy - ay;
double lenAC = Math.sqrt(dxAC * dxAC + dyAC * dyAC);
double dxAB = bx - ax;
double dyAB = by - ay;
//calculate B2 and C2:
double xB2 = Math.sqrt(dxAB * dxAB + dyAB * dyAB);
double phi1 = Math.atan2(dyAB, dxAB);//amount of rotation
double phi2 = Math.atan2(dyAC, dxAC);
double phi3 = phi1 - phi2;
double xC2 = lenAC * Math.cos(phi3);
double yC2 = lenAC * Math.sin(phi3);//rotation & translation is done
if (Math.abs(yC2) > rad)
return new double[0];//no intersection found
double halfChord = Math.sqrt(rad * rad - yC2 * yC2);
double sectOne = xC2 - halfChord;//first intersection point, still on x axis
double sectTwo = xC2 + halfChord;//second intersection point, still on x axis
double[] xyCoords = new double[4];
int ptr = 0;
if ((sectOne >= 0 && sectOne <= xB2) || !ignoreOutside) {
double sectOneX = Math.cos(phi1) * sectOne + ax;//undo rotation and translation
double sectOneY = Math.sin(phi1) * sectOne + ay;
xyCoords[ptr++] = sectOneX;
xyCoords[ptr++] = sectOneY;
}
if ((sectTwo >= 0 && sectTwo <= xB2) || !ignoreOutside) {
double sectTwoX = Math.cos(phi1) * sectTwo + ax;//undo rotation and translation
double sectTwoY = Math.sin(phi1) * sectTwo + ay;
xyCoords[ptr++] = sectTwoX;
xyCoords[ptr++] = sectTwoY;
}
if (halfChord == 0 && ptr > 2) //tangent line returns only one intersection
ptr = 2;
xyCoords = java.util.Arrays.copyOf(xyCoords,ptr);
return xyCoords;
}
/** Returns a copy of this roi. See Thinking is Java by Bruce Eckel
(www.eckelobjects.com) for a good description of object cloning. */
public synchronized Object clone() {
try {
Roi r = (Roi)super.clone();
r.setImage(null);
if (!usingDefaultStroke)
r.setStroke(getStroke());
Color strokeColor2 = getStrokeColor();
r.setFillColor(getFillColor());
r.setStrokeColor(strokeColor2);
r.imageID = getImageID();
r.listenersNotified = false;
if (bounds!=null)
r.bounds = (Rectangle2D.Double)bounds.clone();
return r;
}
catch (CloneNotSupportedException e) {return null;}
}
/** Aborts constructing or modifying the roi (called by the ImageJ class on escape) */
public void abortModification(ImagePlus imp) {
if (state == CONSTRUCTING) {
setImage(null);
if (imp!=null) {
Roi savedPreviousRoi = getPreviousRoi();
imp.setRoi(previousRoi!=null && previousRoi.getImage() == imp ? previousRoi : null);
setPreviousRoi(savedPreviousRoi); //(overrule saving this aborted roi as previousRoi)
}
} else if (state==MOVING)
move(previousSX, previousSY); //move back to starting point
else if (state == MOVING_HANDLE)
moveHandle(previousSX, previousSY); //move handle back to starting point
state = NORMAL;
}
protected void grow(int sx, int sy) {
if (clipboard!=null) return;
int xNew = ic.offScreenX2(sx);
int yNew = ic.offScreenY2(sy);
if (type==RECTANGLE) {
if (xNew < 0) xNew = 0;
if (yNew < 0) yNew = 0;
}
if (constrain) {
// constrain selection to be square
if (!center)
{growConstrained(xNew, yNew); return;}
int dx, dy, d;
dx = xNew - x;
dy = yNew - y;
if (dx<dy)
d = dx;
else
d = dy;
xNew = x + d;
yNew = y + d;
}
if (center) {
width = Math.abs(xNew - startX)*2;
height = Math.abs(yNew - startY)*2;
x = startX - width/2;
y = startY - height/2;
} else {
width = Math.abs(xNew - startX);
height = Math.abs(yNew - startY);
x = (xNew>=startX)?startX:startX - width;
y = (yNew>=startY)?startY:startY - height;
if (type==RECTANGLE) {
if ((x+width) > xMax) width = xMax-x;
if ((y+height) > yMax) height = yMax-y;
}
}
updateClipRect();
imp.draw(clipX, clipY, clipWidth, clipHeight);
oldX = x;
oldY = y;
oldWidth = width;
oldHeight = height;
bounds = null;
}
private void growConstrained(int xNew, int yNew) {
int dx = xNew - startX;
int dy = yNew - startY;
width = height = (int)Math.round(Math.sqrt(dx*dx + dy*dy));
if (type==RECTANGLE) {
x = (xNew>=startX)?startX:startX - width;
y = (yNew>=startY)?startY:startY - height;
if (x<0) x = 0;
if (y<0) y = 0;
if ((x+width) > xMax) width = xMax-x;
if ((y+height) > yMax) height = yMax-y;
} else {
x = startX + dx/2 - width/2;
y = startY + dy/2 - height/2;
}
updateClipRect();
imp.draw(clipX, clipY, clipWidth, clipHeight);
oldX = x;
oldY = y;
oldWidth = width;
oldHeight = height;
}
protected void moveHandle(int sx, int sy) {
double asp;
if (clipboard!=null) return;
int ox = ic.offScreenX2(sx);
int oy = ic.offScreenY2(sy);
if (ox<0) ox=0; if (oy<0) oy=0;
if (ox>xMax) ox=xMax; if (oy>yMax) oy=yMax;
int x1=x, y1=y, x2=x1+width, y2=y+height, xc=x+width/2, yc=y+height/2;
if (width > 7 && height > 7) {
asp = (double)width/(double)height;
asp_bk = asp;
} else
asp = asp_bk;
switch (activeHandle) {
case 0:
x=ox; y=oy;
break;
case 1:
y=oy;
break;
case 2:
x2=ox; y=oy;
break;
case 3:
x2=ox;
break;
case 4:
x2=ox; y2=oy;
break;
case 5:
y2=oy;
break;
case 6:
x=ox; y2=oy;
break;
case 7:
x=ox;
break;
}
if (x<x2)
width=x2-x;
else
{width=1; x=x2;}
if (y<y2)
height = y2-y;
else
{height=1; y=y2;}
if (center) {
switch (activeHandle){
case 0:
width=(xc-x)*2;
height=(yc-y)*2;
break;
case 1:
height=(yc-y)*2;
break;
case 2: