-
Notifications
You must be signed in to change notification settings - Fork 227
/
RoiDecoder.java
603 lines (562 loc) · 18.1 KB
/
RoiDecoder.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
package ij.io;
import ij.gui.*;
import ij.ImagePlus;
import ij.process.*;
import java.io.*;
import java.util.*;
import java.net.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;
/** This class decodes an ImageJ .roi file.
<p>
Format of the original 64 byte ImageJ/NIH Image
.roi file header. Two byte numbers are big-endian
signed shorts. The JavaScript example at
http://wsr.imagej.net/macros/js/DecodeRoiFile.js
demonstrates how to use this information to
decode a .roi file.
<pre>
0-3 "Iout"
4-5 version (>=217)
6-7 roi type (encoded as one byte)
8-9 top
10-11 left
12-13 bottom
14-15 right
16-17 NCoordinates
18-33 x1,y1,x2,y2 (straight line) | x,y,width,height (double rect) | size (npoints)
34-35 stroke width (v1.43i or later)
36-39 ShapeRoi size (type must be 1 if this value>0)
40-43 stroke color (v1.43i or later)
44-47 fill color (v1.43i or later)
48-49 subtype (v1.43k or later)
50-51 options (v1.43k or later)
52-52 arrow style or aspect ratio (v1.43p or later)
53-53 arrow head size (v1.43p or later)
54-55 rounded rect arc size (v1.43p or later)
56-59 position
60-63 header2 offset
64- x-coordinates (short), followed by y-coordinates
<pre>
@see <a href="http://wsr.imagej.net/macros/js/DecodeRoiFile.js">DecodeRoiFile.js</a>
*/
public class RoiDecoder {
// offsets
public static final int VERSION_OFFSET = 4;
public static final int TYPE = 6;
public static final int TOP = 8;
public static final int LEFT = 10;
public static final int BOTTOM = 12;
public static final int RIGHT = 14;
public static final int N_COORDINATES = 16;
public static final int X1 = 18;
public static final int Y1 = 22;
public static final int X2 = 26;
public static final int Y2 = 30;
public static final int XD = 18;
public static final int YD = 22;
public static final int WIDTHD = 26;
public static final int HEIGHTD = 30;
public static final int SIZE = 18;
public static final int STROKE_WIDTH = 34;
public static final int SHAPE_ROI_SIZE = 36;
public static final int STROKE_COLOR = 40;
public static final int FILL_COLOR = 44;
public static final int SUBTYPE = 48;
public static final int OPTIONS = 50;
public static final int ARROW_STYLE = 52;
public static final int FLOAT_PARAM = 52; //ellipse ratio or rotated rect width
public static final int POINT_TYPE= 52;
public static final int ARROW_HEAD_SIZE = 53;
public static final int ROUNDED_RECT_ARC_SIZE = 54;
public static final int POSITION = 56;
public static final int HEADER2_OFFSET = 60;
public static final int COORDINATES = 64;
// header2 offsets
public static final int C_POSITION = 4;
public static final int Z_POSITION = 8;
public static final int T_POSITION = 12;
public static final int NAME_OFFSET = 16;
public static final int NAME_LENGTH = 20;
public static final int OVERLAY_LABEL_COLOR = 24;
public static final int OVERLAY_FONT_SIZE = 28; //short
public static final int GROUP = 30; //byte
public static final int IMAGE_OPACITY = 31; //byte
public static final int IMAGE_SIZE = 32; //int
public static final int FLOAT_STROKE_WIDTH = 36; //float
public static final int ROI_PROPS_OFFSET = 40;
public static final int ROI_PROPS_LENGTH = 44;
public static final int COUNTERS_OFFSET = 48;
// subtypes
public static final int TEXT = 1;
public static final int ARROW = 2;
public static final int ELLIPSE = 3;
public static final int IMAGE = 4;
public static final int ROTATED_RECT = 5;
// options
public static final int SPLINE_FIT = 1;
public static final int DOUBLE_HEADED = 2;
public static final int OUTLINE = 4;
public static final int OVERLAY_LABELS = 8;
public static final int OVERLAY_NAMES = 16;
public static final int OVERLAY_BACKGROUNDS = 32;
public static final int OVERLAY_BOLD = 64;
public static final int SUB_PIXEL_RESOLUTION = 128;
public static final int DRAW_OFFSET = 256;
public static final int ZERO_TRANSPARENT = 512;
public static final int SHOW_LABELS = 1024;
public static final int SCALE_LABELS = 2048;
public static final int PROMPT_BEFORE_DELETING = 4096; //points
public static final int SCALE_STROKE_WIDTH = 8192;
// types
private final int polygon=0, rect=1, oval=2, line=3, freeline=4, polyline=5, noRoi=6,
freehand=7, traced=8, angle=9, point=10;
private byte[] data;
private String path;
private InputStream is;
private String name;
private int size;
/** Constructs an RoiDecoder using a file path. */
public RoiDecoder(String path) {
this.path = path;
}
/** Constructs an RoiDecoder using a byte array. */
public RoiDecoder(byte[] bytes, String name) {
is = new ByteArrayInputStream(bytes);
this.name = name;
this.size = bytes.length;
}
/** Opens the Roi at the specified path. Returns null if there is an error. */
public static Roi open(String path) {
Roi roi = null;
RoiDecoder rd = new RoiDecoder(path);
try {
roi = rd.getRoi();
} catch (IOException e) { }
return roi;
}
/** Returns the ROI. */
public Roi getRoi() throws IOException {
if (path!=null) {
File f = new File(path);
size = (int)f.length();
if (!path.endsWith(".roi") && size>5242880)
throw new IOException("This is not an ROI or file size>5MB)");
name = f.getName();
is = new FileInputStream(path);
}
data = new byte[size];
int total = 0;
while (total<size)
total += is.read(data, total, size-total);
is.close();
if (getByte(0)!=73 || getByte(1)!=111) //"Iout"
throw new IOException("This is not an ImageJ ROI");
int version = getShort(VERSION_OFFSET);
int type = getByte(TYPE);
int subtype = getShort(SUBTYPE);
int top= getShort(TOP);
int left = getShort(LEFT);
int bottom = getShort(BOTTOM);
int right = getShort(RIGHT);
int width = right-left;
int height = bottom-top;
int n = getUnsignedShort(N_COORDINATES);
if (n==0)
n = getInt(SIZE);
int options = getShort(OPTIONS);
int position = getInt(POSITION);
int hdr2Offset = getInt(HEADER2_OFFSET);
int channel=0, slice=0, frame=0;
int overlayLabelColor=0;
int overlayFontSize=0;
int group=0;
int imageOpacity=0;
int imageSize=0;
boolean subPixelResolution = (options&SUB_PIXEL_RESOLUTION)!=0 && version>=222;
boolean drawOffset = subPixelResolution && (options&DRAW_OFFSET)!=0;
boolean scaleStrokeWidth = true;
if (version>=228)
scaleStrokeWidth = (options&SCALE_STROKE_WIDTH)!=0;
boolean subPixelRect = version>=223 && subPixelResolution && (type==rect||type==oval);
double xd=0.0, yd=0.0, widthd=0.0, heightd=0.0;
if (subPixelRect) {
xd = getFloat(XD);
yd = getFloat(YD);
widthd = getFloat(WIDTHD);
heightd = getFloat(HEIGHTD);
}
if (hdr2Offset>0 && hdr2Offset+IMAGE_SIZE+4<=size) {
channel = getInt(hdr2Offset+C_POSITION);
slice = getInt(hdr2Offset+Z_POSITION);
frame = getInt(hdr2Offset+T_POSITION);
overlayLabelColor = getInt(hdr2Offset+OVERLAY_LABEL_COLOR);
overlayFontSize = getShort(hdr2Offset+OVERLAY_FONT_SIZE);
imageOpacity = getByte(hdr2Offset+IMAGE_OPACITY);
imageSize = getInt(hdr2Offset+IMAGE_SIZE);
group = getByte(hdr2Offset+GROUP);
}
if (name!=null && name.endsWith(".roi"))
name = name.substring(0, name.length()-4);
boolean isComposite = getInt(SHAPE_ROI_SIZE)>0;
Roi roi = null;
if (isComposite) {
roi = getShapeRoi();
if (version>=218)
getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
roi.setPosition(position);
if (channel>0 || slice>0 || frame>0)
roi.setPosition(channel, slice, frame);
decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
if (version>=224) {
String props = getRoiProps();
if (props!=null)
roi.setProperties(props);
}
if (version>=228 && group>0)
roi.setGroup(group);
return roi;
}
switch (type) {
case rect:
if (subPixelRect)
roi = new Roi(xd, yd, widthd, heightd);
else
roi = new Roi(left, top, width, height);
int arcSize = getShort(ROUNDED_RECT_ARC_SIZE);
if (arcSize>0)
roi.setCornerDiameter(arcSize);
break;
case oval:
if (subPixelRect)
roi = new OvalRoi(xd, yd, widthd, heightd);
else
roi = new OvalRoi(left, top, width, height);
break;
case line:
double x1 = getFloat(X1);
double y1 = getFloat(Y1);
double x2 = getFloat(X2);
double y2 = getFloat(Y2);
if (subtype==ARROW) {
roi = new Arrow(x1, y1, x2, y2);
((Arrow)roi).setDoubleHeaded((options&DOUBLE_HEADED)!=0);
((Arrow)roi).setOutline((options&OUTLINE)!=0);
int style = getByte(ARROW_STYLE);
if (style>=Arrow.FILLED && style<=Arrow.BAR)
((Arrow)roi).setStyle(style);
int headSize = getByte(ARROW_HEAD_SIZE);
if (headSize>=0 && style<=30)
((Arrow)roi).setHeadSize(headSize);
} else {
roi = new Line(x1, y1, x2, y2);
roi.setDrawOffset(drawOffset);
}
break;
case polygon: case freehand: case traced: case polyline: case freeline: case angle: case point:
//IJ.log("type: "+type);
//IJ.log("n: "+n);
//ij.IJ.log("rect: "+left+","+top+" "+width+" "+height);
if (n==0 || n<0) break;
int[] x = new int[n];
int[] y = new int[n];
float[] xf = null;
float[] yf = null;
int base1 = COORDINATES;
int base2 = base1+2*n;
int xtmp, ytmp;
for (int i=0; i<n; i++) {
xtmp = getShort(base1+i*2);
if (xtmp<0) xtmp = 0;
ytmp = getShort(base2+i*2);
if (ytmp<0) ytmp = 0;
x[i] = left+xtmp;
y[i] = top+ytmp;
}
if (subPixelResolution) {
xf = new float[n];
yf = new float[n];
base1 = COORDINATES+4*n;
base2 = base1+4*n;
for (int i=0; i<n; i++) {
xf[i] = getFloat(base1+i*4);
yf[i] = getFloat(base2+i*4);
}
}
if (type==point) {
if (subPixelResolution)
roi = new PointRoi(xf, yf, n);
else
roi = new PointRoi(x, y, n);
if (version>=226) {
((PointRoi)roi).setPointType(getByte(POINT_TYPE));
((PointRoi)roi).setSize(getShort(STROKE_WIDTH));
}
if ((options&SHOW_LABELS)!=0 && !ij.Prefs.noPointLabels)
((PointRoi)roi).setShowLabels(true);
if ((options&PROMPT_BEFORE_DELETING)!=0)
((PointRoi)roi).promptBeforeDeleting(true);
break;
}
int roiType;
if (type==polygon)
roiType = Roi.POLYGON;
else if (type==freehand) {
roiType = Roi.FREEROI;
if (subtype==ELLIPSE || subtype==ROTATED_RECT) {
double ex1 = getFloat(X1);
double ey1 = getFloat(Y1);
double ex2 = getFloat(X2);
double ey2 = getFloat(Y2);
double param = getFloat(FLOAT_PARAM);
if (subtype==ROTATED_RECT)
roi = new RotatedRectRoi(ex1,ey1,ex2,ey2,param);
else
roi = new EllipseRoi(ex1,ey1,ex2,ey2,param);
break;
}
} else if (type==traced)
roiType = Roi.TRACED_ROI;
else if (type==polyline)
roiType = Roi.POLYLINE;
else if (type==freeline)
roiType = Roi.FREELINE;
else if (type==angle)
roiType = Roi.ANGLE;
else
roiType = Roi.FREEROI;
if (subPixelResolution) {
roi = new PolygonRoi(xf, yf, n, roiType);
roi.setDrawOffset(drawOffset);
} else
roi = new PolygonRoi(x, y, n, roiType);
break;
default:
throw new IOException("Unrecognized ROI type: "+type);
}
if (roi==null)
return null;
roi.setName(getRoiName());
// read stroke width, stroke color and fill color (1.43i or later)
if (version>=218) {
getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
if (type==point)
roi.setStrokeWidth(0);
boolean splineFit = (options&SPLINE_FIT)!=0;
if (splineFit && roi instanceof PolygonRoi)
((PolygonRoi)roi).fitSpline();
}
if (version>=218 && subtype==TEXT)
roi = getTextRoi(roi, version);
if (version>=221 && subtype==IMAGE)
roi = getImageRoi(roi, imageOpacity, imageSize, options);
if (version>=224) {
String props = getRoiProps();
if (props!=null)
roi.setProperties(props);
}
roi.setPosition(position);
if (channel>0 || slice>0 || frame>0)
roi.setPosition(channel, slice, frame);
if (version>=227) {
int[] counters = getPointCounters(n);
if (counters!=null && (roi instanceof PointRoi))
((PointRoi)roi).setCounters(counters); //must be after roi.setPosition()
}
// set group (1.52t or later)
if (version>=228 && group>0)
roi.setGroup(group);
decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
return roi;
}
void decodeOverlayOptions(Roi roi, int version, int options, int color, int fontSize) {
Overlay proto = new Overlay();
proto.drawLabels((options&OVERLAY_LABELS)!=0);
proto.drawNames((options&OVERLAY_NAMES)!=0);
proto.drawBackgrounds((options&OVERLAY_BACKGROUNDS)!=0);
if (version>=220 && color!=0)
proto.setLabelColor(new Color(color));
boolean bold = (options&OVERLAY_BOLD)!=0;
boolean scalable = (options&SCALE_LABELS)!=0;
if (fontSize>0 || bold || scalable) {
proto.setLabelFont(new Font("SansSerif", bold?Font.BOLD:Font.PLAIN, fontSize), scalable);
}
roi.setPrototypeOverlay(proto);
}
void getStrokeWidthAndColor(Roi roi, int hdr2Offset, boolean scaleStrokeWidth) {
double strokeWidth = getShort(STROKE_WIDTH);
if (hdr2Offset>0) {
double strokeWidthD = getFloat(hdr2Offset+FLOAT_STROKE_WIDTH);
if (strokeWidthD>0.0)
strokeWidth = strokeWidthD;
}
if (strokeWidth>0.0) {
if (scaleStrokeWidth)
roi.setStrokeWidth(strokeWidth);
else
roi.setUnscalableStrokeWidth(strokeWidth);
}
int fillColor = getInt(FILL_COLOR);
if (fillColor!=0) {
int alpha = (fillColor>>24)&0xff;
roi.setFillColor(new Color(fillColor, alpha!=255));
}
int strokeColor = getInt(STROKE_COLOR);
if (strokeColor!=0) {
int alpha = (strokeColor>>24)&0xff;
roi.setStrokeColor(new Color(strokeColor, alpha!=255));
}
}
public Roi getShapeRoi() throws IOException {
int type = getByte(TYPE);
if (type!=rect)
throw new IllegalArgumentException("Invalid composite ROI type");
int top= getShort(TOP);
int left = getShort(LEFT);
int bottom = getShort(BOTTOM);
int right = getShort(RIGHT);
int width = right-left;
int height = bottom-top;
int n = getInt(SHAPE_ROI_SIZE);
ShapeRoi roi = null;
float[] shapeArray = new float[n];
int base = COORDINATES;
for(int i=0; i<n; i++) {
shapeArray[i] = getFloat(base);
base += 4;
}
roi = new ShapeRoi(shapeArray);
roi.setName(getRoiName());
return roi;
}
Roi getTextRoi(Roi roi, int version) {
Rectangle r = roi.getBounds();
int hdrSize = RoiEncoder.HEADER_SIZE;
int size = getInt(hdrSize);
int styleAndJustification = getInt(hdrSize+4);
int style = styleAndJustification&255;
int justification = (styleAndJustification>>8) & 3;
boolean drawStringMode = (styleAndJustification&1024)!=0;
int nameLength = getInt(hdrSize+8);
int textLength = getInt(hdrSize+12);
char[] name = new char[nameLength];
char[] text = new char[textLength];
for (int i=0; i<nameLength; i++)
name[i] = (char)getShort(hdrSize+16+i*2);
for (int i=0; i<textLength; i++)
text[i] = (char)getShort(hdrSize+16+nameLength*2+i*2);
double angle = version>=225?getFloat(hdrSize+16+nameLength*2+textLength*2):0f;
Font font = new Font(new String(name), style, size);
TextRoi roi2 = null;
if (roi.subPixelResolution()) {
Rectangle2D fb = roi.getFloatBounds();
roi2 = new TextRoi(fb.getX(), fb.getY(), fb.getWidth(), fb.getHeight(), new String(text), font);
} else
roi2 = new TextRoi(r.x, r.y, r.width, r.height, new String(text), font);
roi2.setStrokeColor(roi.getStrokeColor());
roi2.setFillColor(roi.getFillColor());
roi2.setName(getRoiName());
roi2.setJustification(justification);
roi2.setDrawStringMode(drawStringMode);
roi2.setAngle(angle);
return roi2;
}
Roi getImageRoi(Roi roi, int opacity, int size, int options) {
if (size<=0)
return roi;
Rectangle r = roi.getBounds();
byte[] bytes = new byte[size];
for (int i=0; i<size; i++)
bytes[i] = (byte)getByte(COORDINATES+i);
ImagePlus imp = new Opener().deserialize(bytes);
ImageRoi roi2 = new ImageRoi(r.x, r.y, imp.getProcessor());
roi2.setOpacity(opacity/255.0);
if ((options&ZERO_TRANSPARENT)!=0)
roi2.setZeroTransparent(true);
return roi2;
}
String getRoiName() {
String fileName = name;
int hdr2Offset = getInt(HEADER2_OFFSET);
if (hdr2Offset==0)
return fileName;
int offset = getInt(hdr2Offset+NAME_OFFSET);
int length = getInt(hdr2Offset+NAME_LENGTH);
if (offset==0 || length==0)
return fileName;
if (offset+length*2>size)
return fileName;
char[] name = new char[length];
for (int i=0; i<length; i++)
name[i] = (char)getShort(offset+i*2);
return new String(name);
}
String getRoiProps() {
int hdr2Offset = getInt(HEADER2_OFFSET);
if (hdr2Offset==0)
return null;
int offset = getInt(hdr2Offset+ROI_PROPS_OFFSET);
int length = getInt(hdr2Offset+ROI_PROPS_LENGTH);
if (offset==0 || length==0)
return null;
if (offset+length*2>size)
return null;
char[] props = new char[length];
for (int i=0; i<length; i++)
props[i] = (char)getShort(offset+i*2);
return new String(props);
}
int[] getPointCounters(int n) {
int hdr2Offset = getInt(HEADER2_OFFSET);
if (hdr2Offset==0)
return null;
int offset = getInt(hdr2Offset+COUNTERS_OFFSET);
if (offset==0)
return null;
if (offset+n*4>data.length)
return null;
int[] counters = new int[n];
for (int i=0; i<n; i++)
counters[i] = getInt(offset+i*4);
return counters;
}
int getByte(int base) {
return data[base]&255;
}
int getShort(int base) {
int b0 = data[base]&255;
int b1 = data[base+1]&255;
int n = (short)((b0<<8) + b1);
if (n<-5000)
n = (b0<<8) + b1; // assume n>32767 and unsigned
return n;
}
int getUnsignedShort(int base) {
int b0 = data[base]&255;
int b1 = data[base+1]&255;
return (b0<<8) + b1;
}
int getInt(int base) {
int b0 = data[base]&255;
int b1 = data[base+1]&255;
int b2 = data[base+2]&255;
int b3 = data[base+3]&255;
return ((b0<<24) + (b1<<16) + (b2<<8) + b3);
}
float getFloat(int base) {
return Float.intBitsToFloat(getInt(base));
}
/** Opens an ROI from a byte array. */
public static Roi openFromByteArray(byte[] bytes) {
Roi roi = null;
if (bytes==null || bytes.length==0)
return roi;
try {
RoiDecoder decoder = new RoiDecoder(bytes, null);
roi = decoder.getRoi();
} catch (IOException e) {
return null;
}
return roi;
}
}