roifile 2025.12.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roifile/__init__.py +9 -0
- roifile/__main__.py +9 -0
- roifile/py.typed +0 -0
- roifile/roifile.py +1403 -0
- roifile-2025.12.12.dist-info/METADATA +229 -0
- roifile-2025.12.12.dist-info/RECORD +10 -0
- roifile-2025.12.12.dist-info/WHEEL +5 -0
- roifile-2025.12.12.dist-info/entry_points.txt +2 -0
- roifile-2025.12.12.dist-info/licenses/LICENSE +30 -0
- roifile-2025.12.12.dist-info/top_level.txt +1 -0
roifile/roifile.py
ADDED
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
# roifile.py
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2020-2025, Christoph Gohlke
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
10
|
+
# this list of conditions and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
# and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
23
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
24
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
25
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
26
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
27
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
28
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
29
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
30
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
|
|
32
|
+
"""Read and write ImageJ ROI format.
|
|
33
|
+
|
|
34
|
+
Roifile is a Python library to read, write, create, and plot `ImageJ`_ ROIs,
|
|
35
|
+
an undocumented and ImageJ application specific format to store regions of
|
|
36
|
+
interest, geometric shapes, paths, text, and whatnot for image overlays.
|
|
37
|
+
|
|
38
|
+
.. _ImageJ: https://imagej.net
|
|
39
|
+
|
|
40
|
+
:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
|
|
41
|
+
:License: BSD-3-Clause
|
|
42
|
+
:Version: 2025.12.12
|
|
43
|
+
:DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
|
|
44
|
+
|
|
45
|
+
Quickstart
|
|
46
|
+
----------
|
|
47
|
+
|
|
48
|
+
Install the roifile package and all dependencies from the
|
|
49
|
+
`Python Package Index <https://pypi.org/project/roifile/>`_::
|
|
50
|
+
|
|
51
|
+
python -m pip install -U "roifile[all]"
|
|
52
|
+
|
|
53
|
+
View overlays stored in a ROI, ZIP, or TIFF file::
|
|
54
|
+
|
|
55
|
+
python -m roifile file.roi
|
|
56
|
+
|
|
57
|
+
See `Examples`_ for using the programming interface.
|
|
58
|
+
|
|
59
|
+
Source code, examples, and support are available on
|
|
60
|
+
`GitHub <https://github.com/cgohlke/roifile>`_.
|
|
61
|
+
|
|
62
|
+
Requirements
|
|
63
|
+
------------
|
|
64
|
+
|
|
65
|
+
This revision was tested with the following requirements and dependencies
|
|
66
|
+
(other versions may work):
|
|
67
|
+
|
|
68
|
+
- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.11 3.14.2 64-bit
|
|
69
|
+
- `NumPy <https://pypi.org/project/numpy>`_ 2.3.5
|
|
70
|
+
- `Tifffile <https://pypi.org/project/tifffile/>`_ 2025.10.16 (optional)
|
|
71
|
+
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.7 (optional)
|
|
72
|
+
|
|
73
|
+
Revisions
|
|
74
|
+
---------
|
|
75
|
+
|
|
76
|
+
2025.12.12
|
|
77
|
+
|
|
78
|
+
- Move tests to separate module.
|
|
79
|
+
|
|
80
|
+
2025.5.10
|
|
81
|
+
|
|
82
|
+
- Support Python 3.14.
|
|
83
|
+
|
|
84
|
+
2025.2.20
|
|
85
|
+
|
|
86
|
+
- Drop support for Python 3.9.
|
|
87
|
+
|
|
88
|
+
2024.9.15
|
|
89
|
+
|
|
90
|
+
- Improve typing.
|
|
91
|
+
- Deprecate Python 3.9, support Python 3.13.
|
|
92
|
+
|
|
93
|
+
2024.5.24
|
|
94
|
+
|
|
95
|
+
- Fix docstring examples not correctly rendered on GitHub.
|
|
96
|
+
|
|
97
|
+
2024.3.20
|
|
98
|
+
|
|
99
|
+
- Fix writing generator of ROIs (#9).
|
|
100
|
+
|
|
101
|
+
2024.1.10
|
|
102
|
+
|
|
103
|
+
- Support text rotation.
|
|
104
|
+
- Improve text rendering.
|
|
105
|
+
- Avoid array copies.
|
|
106
|
+
- Limit size read from files.
|
|
107
|
+
|
|
108
|
+
2023.8.30
|
|
109
|
+
|
|
110
|
+
- Fix linting issues.
|
|
111
|
+
- Add py.typed marker.
|
|
112
|
+
|
|
113
|
+
2023.5.12
|
|
114
|
+
|
|
115
|
+
- Improve object repr and type hints.
|
|
116
|
+
- Drop support for Python 3.8 and numpy < 1.21 (NEP29).
|
|
117
|
+
|
|
118
|
+
2023.2.12
|
|
119
|
+
|
|
120
|
+
- Delay import of zipfile.
|
|
121
|
+
- Verify shape of coordinates on write.
|
|
122
|
+
|
|
123
|
+
2022.9.19
|
|
124
|
+
|
|
125
|
+
- Fix integer coordinates to -5000..60536 conforming with ImageJ (breaking).
|
|
126
|
+
- Add subpixel_coordinates in frompoints for out-of-range integer coordinates.
|
|
127
|
+
|
|
128
|
+
2022.7.29
|
|
129
|
+
|
|
130
|
+
- …
|
|
131
|
+
|
|
132
|
+
Refer to the CHANGES file for older revisions.
|
|
133
|
+
|
|
134
|
+
Notes
|
|
135
|
+
-----
|
|
136
|
+
|
|
137
|
+
The ImageJ ROI format cannot store integer coordinate values outside the
|
|
138
|
+
range of -5000..60536.
|
|
139
|
+
|
|
140
|
+
Refer to the ImageJ `RoiDecoder.java
|
|
141
|
+
<https://github.com/imagej/ImageJ/blob/master/ij/io/RoiDecoder.java>`_
|
|
142
|
+
source code for a reference implementation.
|
|
143
|
+
|
|
144
|
+
Other Python packages handling ImageJ ROIs:
|
|
145
|
+
|
|
146
|
+
- `ijpython_roi <https://github.com/dwaithe/ijpython_roi>`_
|
|
147
|
+
- `read-roi <https://github.com/hadim/read-roi/>`_
|
|
148
|
+
- `napari_jroitools <https://github.com/jayunruh/napari_jroitools>`_
|
|
149
|
+
|
|
150
|
+
Examples
|
|
151
|
+
--------
|
|
152
|
+
|
|
153
|
+
Create a new ImagejRoi instance from an array of x, y coordinates:
|
|
154
|
+
|
|
155
|
+
>>> roi = ImagejRoi.frompoints([[1.1, 2.2], [3.3, 4.4], [5.5, 6.6]])
|
|
156
|
+
>>> roi.roitype = ROI_TYPE.POINT
|
|
157
|
+
>>> roi.options |= ROI_OPTIONS.SHOW_LABELS
|
|
158
|
+
|
|
159
|
+
Export the instance to an ImageJ ROI formatted byte string or file:
|
|
160
|
+
|
|
161
|
+
>>> out = roi.tobytes()
|
|
162
|
+
>>> out[:4]
|
|
163
|
+
b'Iout'
|
|
164
|
+
>>> roi.tofile('_test.roi')
|
|
165
|
+
|
|
166
|
+
Read the ImageJ ROI from the file and verify the content:
|
|
167
|
+
|
|
168
|
+
>>> roi2 = ImagejRoi.fromfile('_test.roi')
|
|
169
|
+
>>> roi2 == roi
|
|
170
|
+
True
|
|
171
|
+
>>> roi.roitype == ROI_TYPE.POINT
|
|
172
|
+
True
|
|
173
|
+
>>> roi.subpixelresolution
|
|
174
|
+
True
|
|
175
|
+
>>> roi.coordinates()
|
|
176
|
+
array([[1.1, 2.2],
|
|
177
|
+
[3.3, 4.4],
|
|
178
|
+
[5.5, 6.6]], dtype=float32)
|
|
179
|
+
>>> roi.left, roi.top, roi.right, roi.bottom
|
|
180
|
+
(1, 2, 7, 8)
|
|
181
|
+
>>> roi2.name = 'test'
|
|
182
|
+
|
|
183
|
+
Plot the ROI using matplotlib:
|
|
184
|
+
|
|
185
|
+
>>> roi.plot()
|
|
186
|
+
|
|
187
|
+
Write the ROIs to a ZIP file:
|
|
188
|
+
|
|
189
|
+
>>> roiwrite('_test.zip', [roi, roi2], mode='w')
|
|
190
|
+
|
|
191
|
+
Read the ROIs from the ZIP file:
|
|
192
|
+
|
|
193
|
+
>>> rois = roiread('_test.zip')
|
|
194
|
+
>>> assert len(rois) == 2 and rois[0] == roi and rois[1].name == 'test'
|
|
195
|
+
|
|
196
|
+
Write the ROIs to an ImageJ formatted TIFF file:
|
|
197
|
+
|
|
198
|
+
>>> import numpy
|
|
199
|
+
>>> import tifffile
|
|
200
|
+
>>> tifffile.imwrite(
|
|
201
|
+
... '_test.tif',
|
|
202
|
+
... numpy.zeros((9, 9), 'u1'),
|
|
203
|
+
... imagej=True,
|
|
204
|
+
... metadata={'Overlays': [roi.tobytes(), roi2.tobytes()]},
|
|
205
|
+
... )
|
|
206
|
+
|
|
207
|
+
Read the ROIs embedded in an ImageJ formatted TIFF file:
|
|
208
|
+
|
|
209
|
+
>>> rois = roiread('_test.tif')
|
|
210
|
+
>>> assert len(rois) == 2 and rois[0] == roi and rois[1].name == 'test'
|
|
211
|
+
|
|
212
|
+
View the overlays stored in a ROI, ZIP, or TIFF file from a command line::
|
|
213
|
+
|
|
214
|
+
python -m roifile _test.roi
|
|
215
|
+
|
|
216
|
+
For an advanced example, see `roifile_demo.py` in the source distribution.
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
from __future__ import annotations
|
|
221
|
+
|
|
222
|
+
__version__ = '2025.12.12'
|
|
223
|
+
|
|
224
|
+
__all__ = [
|
|
225
|
+
'ROI_COLOR_NONE',
|
|
226
|
+
'ROI_OPTIONS',
|
|
227
|
+
'ROI_POINT_SIZE',
|
|
228
|
+
'ROI_POINT_TYPE',
|
|
229
|
+
'ROI_SUBTYPE',
|
|
230
|
+
'ROI_TYPE',
|
|
231
|
+
'ImagejRoi',
|
|
232
|
+
'__version__',
|
|
233
|
+
'roiread',
|
|
234
|
+
'roiwrite',
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
import enum
|
|
238
|
+
import logging
|
|
239
|
+
import os
|
|
240
|
+
import struct
|
|
241
|
+
import sys
|
|
242
|
+
import uuid
|
|
243
|
+
from dataclasses import dataclass
|
|
244
|
+
from typing import TYPE_CHECKING
|
|
245
|
+
|
|
246
|
+
import numpy
|
|
247
|
+
|
|
248
|
+
if TYPE_CHECKING:
|
|
249
|
+
from collections.abc import Iterable
|
|
250
|
+
from typing import Any, Literal
|
|
251
|
+
|
|
252
|
+
from matplotlib.axes import Axes
|
|
253
|
+
from numpy.typing import ArrayLike, NDArray
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def roiread(
|
|
257
|
+
filename: os.PathLike[Any] | str,
|
|
258
|
+
/,
|
|
259
|
+
*,
|
|
260
|
+
min_int_coord: int | None = None,
|
|
261
|
+
maxsize: int = 268435456, # 256 MB
|
|
262
|
+
) -> ImagejRoi | list[ImagejRoi]:
|
|
263
|
+
"""Return ImagejRoi instance(s) from ROI, ZIP, or TIFF file.
|
|
264
|
+
|
|
265
|
+
For ZIP or TIFF files, return a list of ImagejRoi.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
return ImagejRoi.fromfile(
|
|
269
|
+
filename, min_int_coord=min_int_coord, maxsize=maxsize
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def roiwrite(
|
|
274
|
+
filename: os.PathLike[Any] | str,
|
|
275
|
+
roi: ImagejRoi | Iterable[ImagejRoi],
|
|
276
|
+
/,
|
|
277
|
+
*,
|
|
278
|
+
name: str | Iterable[str] | None = None,
|
|
279
|
+
mode: Literal['r', 'w', 'x', 'a'] | None = None,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Write ImagejRoi instance(s) to ROI or ZIP file.
|
|
282
|
+
|
|
283
|
+
Write an ImagejRoi instance to a ROI file or write a sequence of ImagejRoi
|
|
284
|
+
instances to a ZIP file. Existing ZIP files are opened for append.
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
filename = os.fspath(filename)
|
|
288
|
+
|
|
289
|
+
if isinstance(roi, ImagejRoi):
|
|
290
|
+
assert name is None or isinstance(name, str)
|
|
291
|
+
return roi.tofile(filename, name=name, mode=mode)
|
|
292
|
+
|
|
293
|
+
if mode is None:
|
|
294
|
+
mode = 'a' if os.path.exists(filename) else 'w'
|
|
295
|
+
|
|
296
|
+
if name is not None:
|
|
297
|
+
if isinstance(name, str):
|
|
298
|
+
raise ValueError("'name' is not an iterable of str")
|
|
299
|
+
name = iter(name)
|
|
300
|
+
|
|
301
|
+
import zipfile
|
|
302
|
+
|
|
303
|
+
assert mode is not None
|
|
304
|
+
with zipfile.ZipFile(filename, mode) as zf:
|
|
305
|
+
for r in roi:
|
|
306
|
+
if name is None:
|
|
307
|
+
n = r.name if r.name else r.autoname
|
|
308
|
+
else:
|
|
309
|
+
n = next(name)
|
|
310
|
+
n = n if n[-4:].lower() == '.roi' else n + '.roi'
|
|
311
|
+
with zf.open(n, 'w') as fh:
|
|
312
|
+
fh.write(r.tobytes())
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ROI_TYPE(enum.IntEnum):
|
|
317
|
+
"""ImageJ ROI types."""
|
|
318
|
+
|
|
319
|
+
POLYGON = 0
|
|
320
|
+
RECT = 1
|
|
321
|
+
OVAL = 2
|
|
322
|
+
LINE = 3
|
|
323
|
+
FREELINE = 4
|
|
324
|
+
POLYLINE = 5
|
|
325
|
+
NOROI = 6
|
|
326
|
+
FREEHAND = 7
|
|
327
|
+
TRACED = 8
|
|
328
|
+
ANGLE = 9
|
|
329
|
+
POINT = 10
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class ROI_SUBTYPE(enum.IntEnum):
|
|
333
|
+
"""ImageJ ROI subtypes."""
|
|
334
|
+
|
|
335
|
+
UNDEFINED = 0
|
|
336
|
+
TEXT = 1
|
|
337
|
+
ARROW = 2
|
|
338
|
+
ELLIPSE = 3
|
|
339
|
+
IMAGE = 4
|
|
340
|
+
ROTATED_RECT = 5
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ROI_OPTIONS(enum.IntFlag):
|
|
344
|
+
"""ImageJ ROI options."""
|
|
345
|
+
|
|
346
|
+
NONE = 0
|
|
347
|
+
SPLINE_FIT = 1
|
|
348
|
+
DOUBLE_HEADED = 2
|
|
349
|
+
OUTLINE = 4
|
|
350
|
+
OVERLAY_LABELS = 8
|
|
351
|
+
OVERLAY_NAMES = 16
|
|
352
|
+
OVERLAY_BACKGROUNDS = 32
|
|
353
|
+
OVERLAY_BOLD = 64
|
|
354
|
+
SUB_PIXEL_RESOLUTION = 128
|
|
355
|
+
DRAW_OFFSET = 256
|
|
356
|
+
ZERO_TRANSPARENT = 512
|
|
357
|
+
SHOW_LABELS = 1024
|
|
358
|
+
SCALE_LABELS = 2048
|
|
359
|
+
PROMPT_BEFORE_DELETING = 4096
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class ROI_POINT_TYPE(enum.IntEnum):
|
|
363
|
+
"""ImageJ ROI point types."""
|
|
364
|
+
|
|
365
|
+
HYBRID = 0
|
|
366
|
+
CROSS = 1
|
|
367
|
+
# CROSSHAIR = 1
|
|
368
|
+
DOT = 2
|
|
369
|
+
CIRCLE = 3
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class ROI_POINT_SIZE(enum.IntEnum):
|
|
373
|
+
"""ImageJ ROI point sizes."""
|
|
374
|
+
|
|
375
|
+
TINY = 1
|
|
376
|
+
SMALL = 3
|
|
377
|
+
MEDIUM = 5
|
|
378
|
+
LARGE = 7
|
|
379
|
+
EXTRA_LARGE = 11
|
|
380
|
+
XXL = 17
|
|
381
|
+
XXXL = 25
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
ROI_COLOR_NONE = b'\x00\x00\x00\x00'
|
|
385
|
+
"""No color or black."""
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@dataclass
|
|
389
|
+
class ImagejRoi:
|
|
390
|
+
"""Read and write ImageJ ROI format."""
|
|
391
|
+
|
|
392
|
+
byteorder: Literal['>', '<'] = '>'
|
|
393
|
+
roitype: ROI_TYPE = ROI_TYPE.POLYGON
|
|
394
|
+
subtype: ROI_SUBTYPE = ROI_SUBTYPE.UNDEFINED
|
|
395
|
+
options: ROI_OPTIONS = ROI_OPTIONS.NONE
|
|
396
|
+
name: str = ''
|
|
397
|
+
props: str = ''
|
|
398
|
+
version: int = 217
|
|
399
|
+
top: int = 0
|
|
400
|
+
left: int = 0
|
|
401
|
+
bottom: int = 0
|
|
402
|
+
right: int = 0
|
|
403
|
+
n_coordinates: int = 0
|
|
404
|
+
stroke_width: int = 0
|
|
405
|
+
shape_roi_size: int = 0
|
|
406
|
+
stroke_color: bytes = ROI_COLOR_NONE
|
|
407
|
+
fill_color: bytes = ROI_COLOR_NONE
|
|
408
|
+
arrow_style_or_aspect_ratio: int = 0
|
|
409
|
+
arrow_head_size: int = 0
|
|
410
|
+
rounded_rect_arc_size: int = 0
|
|
411
|
+
position: int = 0
|
|
412
|
+
c_position: int = 0
|
|
413
|
+
z_position: int = 0
|
|
414
|
+
t_position: int = 0
|
|
415
|
+
x1: float = 0.0
|
|
416
|
+
y1: float = 0.0
|
|
417
|
+
x2: float = 0.0
|
|
418
|
+
y2: float = 0.0
|
|
419
|
+
xd: float = 0.0
|
|
420
|
+
yd: float = 0.0
|
|
421
|
+
widthd: float = 0.0
|
|
422
|
+
heightd: float = 0.0
|
|
423
|
+
overlay_label_color: bytes = ROI_COLOR_NONE
|
|
424
|
+
overlay_font_size: int = 0
|
|
425
|
+
group: int = 0
|
|
426
|
+
image_opacity: int = 0
|
|
427
|
+
image_size: int = 0
|
|
428
|
+
float_stroke_width: float = 0.0
|
|
429
|
+
text_size: int = 0
|
|
430
|
+
text_style: int = 0
|
|
431
|
+
text_justification: int = 0
|
|
432
|
+
text_angle: float = 0.0
|
|
433
|
+
text_name: str = ''
|
|
434
|
+
text: str = ''
|
|
435
|
+
counters: NDArray[numpy.uint8] | None = None
|
|
436
|
+
counter_positions: NDArray[numpy.uint32] | None = None
|
|
437
|
+
integer_coordinates: NDArray[numpy.int32] | None = None
|
|
438
|
+
subpixel_coordinates: NDArray[numpy.float32] | None = None
|
|
439
|
+
multi_coordinates: NDArray[numpy.float32] | None = None
|
|
440
|
+
|
|
441
|
+
@classmethod
|
|
442
|
+
def frompoints(
|
|
443
|
+
cls,
|
|
444
|
+
points: ArrayLike | None = None,
|
|
445
|
+
/,
|
|
446
|
+
*,
|
|
447
|
+
name: str | None = None,
|
|
448
|
+
position: int | None = None,
|
|
449
|
+
index: int | str | None = None,
|
|
450
|
+
c: int | None = None,
|
|
451
|
+
z: int | None = None,
|
|
452
|
+
t: int | None = None,
|
|
453
|
+
) -> ImagejRoi:
|
|
454
|
+
"""Return ImagejRoi instance from sequence of Point coordinates.
|
|
455
|
+
|
|
456
|
+
Use floating point coordinates for subpixel precision or values outside
|
|
457
|
+
the range -5000..60536.
|
|
458
|
+
|
|
459
|
+
A FREEHAND ROI with options OVERLAY_BACKGROUNDS and OVERLAY_LABELS is
|
|
460
|
+
returned.
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
if points is None:
|
|
464
|
+
return cls()
|
|
465
|
+
|
|
466
|
+
self = cls()
|
|
467
|
+
self.version = 226
|
|
468
|
+
self.roitype = ROI_TYPE.FREEHAND
|
|
469
|
+
self.options = (
|
|
470
|
+
ROI_OPTIONS.OVERLAY_BACKGROUNDS | ROI_OPTIONS.OVERLAY_LABELS
|
|
471
|
+
)
|
|
472
|
+
if position is not None:
|
|
473
|
+
self.position = position + 1
|
|
474
|
+
if c is not None:
|
|
475
|
+
self.c_position = c + 1
|
|
476
|
+
if z is not None:
|
|
477
|
+
self.z_position = z + 1
|
|
478
|
+
if t is not None:
|
|
479
|
+
self.t_position = t + 1
|
|
480
|
+
if name is None:
|
|
481
|
+
if index is None:
|
|
482
|
+
name = str(uuid.uuid1())
|
|
483
|
+
else:
|
|
484
|
+
name = f'F{self.t_position:02}-C{index}'
|
|
485
|
+
self.name = name
|
|
486
|
+
|
|
487
|
+
coords = numpy.array(points, copy=True)
|
|
488
|
+
if coords.dtype.kind == 'f' or (
|
|
489
|
+
numpy.any(coords > 60000) or numpy.any(coords < -5000)
|
|
490
|
+
):
|
|
491
|
+
self.options |= ROI_OPTIONS.SUB_PIXEL_RESOLUTION
|
|
492
|
+
self.subpixel_coordinates = coords.astype(numpy.float32, copy=True)
|
|
493
|
+
if coords.dtype.kind == 'f':
|
|
494
|
+
coords = numpy.round(coords)
|
|
495
|
+
|
|
496
|
+
coords = numpy.array(coords, dtype=numpy.int32)
|
|
497
|
+
|
|
498
|
+
left_top = coords.min(axis=0)
|
|
499
|
+
right_bottom = coords.max(axis=0)
|
|
500
|
+
right_bottom += [1, 1]
|
|
501
|
+
|
|
502
|
+
coords -= left_top
|
|
503
|
+
self.integer_coordinates = coords
|
|
504
|
+
self.n_coordinates = len(self.integer_coordinates)
|
|
505
|
+
self.left = int(left_top[0])
|
|
506
|
+
self.top = int(left_top[1])
|
|
507
|
+
self.right = int(right_bottom[0])
|
|
508
|
+
self.bottom = int(right_bottom[1])
|
|
509
|
+
|
|
510
|
+
return self
|
|
511
|
+
|
|
512
|
+
@classmethod
|
|
513
|
+
def fromfile(
|
|
514
|
+
cls,
|
|
515
|
+
filename: os.PathLike[Any] | str,
|
|
516
|
+
/,
|
|
517
|
+
*,
|
|
518
|
+
min_int_coord: int | None = None,
|
|
519
|
+
maxsize: int = 268435456, # 256 MB
|
|
520
|
+
) -> ImagejRoi | list[ImagejRoi]:
|
|
521
|
+
"""Return ImagejRoi instance from ROI, ZIP, or TIFF file.
|
|
522
|
+
|
|
523
|
+
For ZIP or TIFF files, return a list of ImagejRoi.
|
|
524
|
+
|
|
525
|
+
"""
|
|
526
|
+
filename = os.fspath(filename)
|
|
527
|
+
if filename[-4:].lower() == '.tif':
|
|
528
|
+
import tifffile
|
|
529
|
+
|
|
530
|
+
with tifffile.TiffFile(filename) as tif:
|
|
531
|
+
if tif.imagej_metadata is None:
|
|
532
|
+
raise ValueError('file does not contain ImagejRoi')
|
|
533
|
+
rois: list[bytes] = []
|
|
534
|
+
if 'Overlays' in tif.imagej_metadata:
|
|
535
|
+
overlays = tif.imagej_metadata['Overlays']
|
|
536
|
+
if isinstance(overlays, (list, tuple)):
|
|
537
|
+
rois.extend(overlays)
|
|
538
|
+
else:
|
|
539
|
+
rois.append(overlays)
|
|
540
|
+
if 'ROI' in tif.imagej_metadata:
|
|
541
|
+
roi = tif.imagej_metadata['ROI']
|
|
542
|
+
if isinstance(roi, (list, tuple)):
|
|
543
|
+
rois.extend(roi)
|
|
544
|
+
else:
|
|
545
|
+
rois.append(roi)
|
|
546
|
+
return [
|
|
547
|
+
cls.frombytes(roi, min_int_coord=min_int_coord)
|
|
548
|
+
for roi in rois
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
if filename[-4:].lower() == '.zip':
|
|
552
|
+
import zipfile
|
|
553
|
+
|
|
554
|
+
with zipfile.ZipFile(filename) as zf:
|
|
555
|
+
return [
|
|
556
|
+
cls.frombytes(
|
|
557
|
+
zf.open(name).read(maxsize),
|
|
558
|
+
min_int_coord=min_int_coord,
|
|
559
|
+
)
|
|
560
|
+
for name in zf.namelist()
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
with open(filename, 'rb') as fh:
|
|
564
|
+
data = fh.read(maxsize)
|
|
565
|
+
return cls.frombytes(data, min_int_coord=min_int_coord)
|
|
566
|
+
|
|
567
|
+
@classmethod
|
|
568
|
+
def frombytes(
|
|
569
|
+
cls,
|
|
570
|
+
data: bytes,
|
|
571
|
+
/,
|
|
572
|
+
*,
|
|
573
|
+
min_int_coord: int | None = None,
|
|
574
|
+
) -> ImagejRoi:
|
|
575
|
+
"""Return ImagejRoi instance from bytes."""
|
|
576
|
+
if data[:4] != b'Iout':
|
|
577
|
+
raise ValueError(f'not an ImageJ ROI {data[:4]!r}')
|
|
578
|
+
|
|
579
|
+
self = cls()
|
|
580
|
+
|
|
581
|
+
(
|
|
582
|
+
self.version,
|
|
583
|
+
roitype,
|
|
584
|
+
self.top,
|
|
585
|
+
self.left,
|
|
586
|
+
self.bottom,
|
|
587
|
+
self.right,
|
|
588
|
+
self.n_coordinates,
|
|
589
|
+
# skip 16 bytes: x1,y1,x2,y2 or x,y,width,height or size
|
|
590
|
+
self.stroke_width,
|
|
591
|
+
self.shape_roi_size,
|
|
592
|
+
self.stroke_color,
|
|
593
|
+
self.fill_color,
|
|
594
|
+
subtype,
|
|
595
|
+
options,
|
|
596
|
+
self.arrow_style_or_aspect_ratio,
|
|
597
|
+
self.arrow_head_size,
|
|
598
|
+
self.rounded_rect_arc_size,
|
|
599
|
+
self.position,
|
|
600
|
+
header2_offset,
|
|
601
|
+
) = struct.unpack(
|
|
602
|
+
self.byteorder + 'hBxhhhhH16xhi4s4shhBBhii', data[4:64]
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
min_int_coord = ImagejRoi.min_int_coord(min_int_coord)
|
|
606
|
+
|
|
607
|
+
if self.top < min_int_coord:
|
|
608
|
+
self.top += 65536
|
|
609
|
+
if self.bottom < min_int_coord:
|
|
610
|
+
self.bottom += 65536
|
|
611
|
+
if self.bottom < 0 and self.bottom < self.top:
|
|
612
|
+
self.bottom += 65536
|
|
613
|
+
|
|
614
|
+
if self.left < min_int_coord:
|
|
615
|
+
self.left += 65536
|
|
616
|
+
if self.right < min_int_coord:
|
|
617
|
+
self.right += 65536
|
|
618
|
+
if self.right < 0 and self.right < self.left:
|
|
619
|
+
self.right += 65536
|
|
620
|
+
|
|
621
|
+
self.roitype = ROI_TYPE(roitype)
|
|
622
|
+
self.subtype = ROI_SUBTYPE(subtype)
|
|
623
|
+
self.options = ROI_OPTIONS(options)
|
|
624
|
+
|
|
625
|
+
if self.subpixelrect:
|
|
626
|
+
(self.xd, self.yd, self.widthd, self.heightd) = struct.unpack(
|
|
627
|
+
self.byteorder + 'ffff', data[18:34]
|
|
628
|
+
)
|
|
629
|
+
elif self.roitype == ROI_TYPE.LINE or (
|
|
630
|
+
self.roitype == ROI_TYPE.FREEHAND
|
|
631
|
+
and self.subtype in {ROI_SUBTYPE.ELLIPSE, ROI_SUBTYPE.ROTATED_RECT}
|
|
632
|
+
):
|
|
633
|
+
(self.x1, self.y1, self.x2, self.y2) = struct.unpack(
|
|
634
|
+
self.byteorder + 'ffff', data[18:34]
|
|
635
|
+
)
|
|
636
|
+
elif self.n_coordinates == 0:
|
|
637
|
+
self.n_coordinates = struct.unpack(
|
|
638
|
+
self.byteorder + 'i', data[18:22]
|
|
639
|
+
)[0]
|
|
640
|
+
|
|
641
|
+
if 0 < header2_offset < len(data) - 52:
|
|
642
|
+
(
|
|
643
|
+
self.c_position,
|
|
644
|
+
self.z_position,
|
|
645
|
+
self.t_position,
|
|
646
|
+
name_offset,
|
|
647
|
+
name_length,
|
|
648
|
+
self.overlay_label_color,
|
|
649
|
+
self.overlay_font_size,
|
|
650
|
+
self.group,
|
|
651
|
+
self.image_opacity,
|
|
652
|
+
self.image_size,
|
|
653
|
+
self.float_stroke_width,
|
|
654
|
+
roi_props_offset,
|
|
655
|
+
roi_props_length,
|
|
656
|
+
counters_offset,
|
|
657
|
+
) = struct.unpack(
|
|
658
|
+
self.byteorder + '4xiiiii4shBBifiii',
|
|
659
|
+
data[header2_offset : header2_offset + 52],
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
if name_offset > 0 and name_length > 0:
|
|
663
|
+
name = data[name_offset : name_offset + name_length * 2]
|
|
664
|
+
self.name = name.decode(self.utf16)
|
|
665
|
+
|
|
666
|
+
if roi_props_offset > 0 and roi_props_length > 0:
|
|
667
|
+
props = data[
|
|
668
|
+
roi_props_offset : name_offset + roi_props_length * 2
|
|
669
|
+
]
|
|
670
|
+
self.props = props.decode(self.utf16)
|
|
671
|
+
|
|
672
|
+
if counters_offset > 0:
|
|
673
|
+
counters: NDArray[numpy.uint32] = numpy.ndarray(
|
|
674
|
+
shape=self.n_coordinates,
|
|
675
|
+
dtype=self.byteorder + 'u4',
|
|
676
|
+
buffer=data,
|
|
677
|
+
offset=counters_offset,
|
|
678
|
+
)
|
|
679
|
+
self.counters = (counters & 0xFF).astype(numpy.uint8)
|
|
680
|
+
self.counter_positions = (counters >> 8).astype(
|
|
681
|
+
numpy.uint32, copy=False
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if self.version >= 218 and self.subtype == ROI_SUBTYPE.TEXT:
|
|
685
|
+
(
|
|
686
|
+
self.text_size,
|
|
687
|
+
style_and_justification,
|
|
688
|
+
name_length,
|
|
689
|
+
text_length,
|
|
690
|
+
) = struct.unpack(self.byteorder + 'iiii', data[64:80])
|
|
691
|
+
self.text_style = style_and_justification & 255
|
|
692
|
+
self.text_justification = (style_and_justification >> 8) & 3
|
|
693
|
+
off = 80
|
|
694
|
+
self.text_name = data[off : off + name_length * 2].decode(
|
|
695
|
+
self.utf16
|
|
696
|
+
)
|
|
697
|
+
off += name_length * 2
|
|
698
|
+
self.text = data[off : off + text_length * 2].decode(self.utf16)
|
|
699
|
+
if self.version >= 225:
|
|
700
|
+
off += text_length * 2
|
|
701
|
+
self.text_angle = struct.unpack(
|
|
702
|
+
self.byteorder + 'f', data[off : off + 4]
|
|
703
|
+
)[0]
|
|
704
|
+
|
|
705
|
+
elif self.roitype in (
|
|
706
|
+
ROI_TYPE.POLYGON,
|
|
707
|
+
ROI_TYPE.FREEHAND,
|
|
708
|
+
ROI_TYPE.TRACED,
|
|
709
|
+
ROI_TYPE.POLYLINE,
|
|
710
|
+
ROI_TYPE.FREELINE,
|
|
711
|
+
ROI_TYPE.ANGLE,
|
|
712
|
+
ROI_TYPE.POINT,
|
|
713
|
+
):
|
|
714
|
+
self.integer_coordinates = numpy.ndarray(
|
|
715
|
+
shape=(self.n_coordinates, 2),
|
|
716
|
+
dtype=self.byteorder + 'i2',
|
|
717
|
+
buffer=data,
|
|
718
|
+
offset=64,
|
|
719
|
+
order='F',
|
|
720
|
+
).astype(numpy.int32)
|
|
721
|
+
|
|
722
|
+
select = self.integer_coordinates < min_int_coord
|
|
723
|
+
self.integer_coordinates[select] += 65536
|
|
724
|
+
|
|
725
|
+
if self.subpixelresolution:
|
|
726
|
+
self.subpixel_coordinates = numpy.ndarray(
|
|
727
|
+
shape=(self.n_coordinates, 2),
|
|
728
|
+
dtype=self.byteorder + 'f4',
|
|
729
|
+
buffer=data,
|
|
730
|
+
offset=64 + self.n_coordinates * 4,
|
|
731
|
+
order='F',
|
|
732
|
+
).copy()
|
|
733
|
+
|
|
734
|
+
elif self.composite and self.roitype == ROI_TYPE.RECT:
|
|
735
|
+
self.multi_coordinates = numpy.ndarray(
|
|
736
|
+
shape=self.shape_roi_size,
|
|
737
|
+
dtype=self.byteorder + 'f4',
|
|
738
|
+
buffer=data,
|
|
739
|
+
offset=64,
|
|
740
|
+
).copy()
|
|
741
|
+
|
|
742
|
+
elif self.roitype not in (ROI_TYPE.RECT, ROI_TYPE.LINE, ROI_TYPE.OVAL):
|
|
743
|
+
logger().warning(f'cannot handle ImagejRoi type {self.roitype!r}')
|
|
744
|
+
|
|
745
|
+
return self
|
|
746
|
+
|
|
747
|
+
def tofile(
|
|
748
|
+
self,
|
|
749
|
+
filename: os.PathLike[Any] | str,
|
|
750
|
+
name: str | None = None,
|
|
751
|
+
mode: Literal['r', 'w', 'x', 'a'] | None = None,
|
|
752
|
+
) -> None:
|
|
753
|
+
"""Write ImagejRoi to ROI or ZIP file.
|
|
754
|
+
|
|
755
|
+
Existing ZIP files are opened for append.
|
|
756
|
+
|
|
757
|
+
"""
|
|
758
|
+
filename = os.fspath(filename)
|
|
759
|
+
if filename[-4:].lower() == '.zip':
|
|
760
|
+
if name is None:
|
|
761
|
+
name = self.name if self.name else self.autoname
|
|
762
|
+
if name[-4:].lower() != '.roi':
|
|
763
|
+
name += '.roi'
|
|
764
|
+
if mode is None:
|
|
765
|
+
mode = 'a' if os.path.exists(filename) else 'w'
|
|
766
|
+
import zipfile
|
|
767
|
+
|
|
768
|
+
assert mode is not None
|
|
769
|
+
with (
|
|
770
|
+
zipfile.ZipFile(filename, mode) as zf,
|
|
771
|
+
zf.open(name, 'w') as fh,
|
|
772
|
+
):
|
|
773
|
+
fh.write(self.tobytes())
|
|
774
|
+
else:
|
|
775
|
+
with open(filename, 'wb') as fh:
|
|
776
|
+
fh.write(self.tobytes())
|
|
777
|
+
|
|
778
|
+
def tobytes(self) -> bytes:
|
|
779
|
+
"""Return ImagejRoi as bytes."""
|
|
780
|
+
result = [b'Iout']
|
|
781
|
+
|
|
782
|
+
result.append(
|
|
783
|
+
struct.pack(
|
|
784
|
+
self.byteorder + 'hBxhhhhH',
|
|
785
|
+
self.version,
|
|
786
|
+
self.roitype.value,
|
|
787
|
+
numpy.array(self.top).astype(numpy.int16),
|
|
788
|
+
numpy.array(self.left).astype(numpy.int16),
|
|
789
|
+
numpy.array(self.bottom).astype(numpy.int16),
|
|
790
|
+
numpy.array(self.right).astype(numpy.int16),
|
|
791
|
+
self.n_coordinates if self.n_coordinates < 2**16 else 0,
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
if self.subpixelrect:
|
|
796
|
+
result.append(
|
|
797
|
+
struct.pack(
|
|
798
|
+
self.byteorder + 'ffff',
|
|
799
|
+
self.xd,
|
|
800
|
+
self.yd,
|
|
801
|
+
self.widthd,
|
|
802
|
+
self.heightd,
|
|
803
|
+
)
|
|
804
|
+
)
|
|
805
|
+
elif self.roitype == ROI_TYPE.LINE or (
|
|
806
|
+
self.roitype == ROI_TYPE.FREEHAND
|
|
807
|
+
and self.subtype in {ROI_SUBTYPE.ELLIPSE, ROI_SUBTYPE.ROTATED_RECT}
|
|
808
|
+
):
|
|
809
|
+
result.append(
|
|
810
|
+
struct.pack(
|
|
811
|
+
self.byteorder + 'ffff', self.x1, self.y1, self.x2, self.y2
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
elif self.n_coordinates >= 2**16:
|
|
815
|
+
result.append(
|
|
816
|
+
struct.pack(self.byteorder + 'i12x', self.n_coordinates)
|
|
817
|
+
)
|
|
818
|
+
else:
|
|
819
|
+
result.append(b'\x00' * 16)
|
|
820
|
+
|
|
821
|
+
result.append(
|
|
822
|
+
struct.pack(
|
|
823
|
+
self.byteorder + 'hi4s4shhBBhi',
|
|
824
|
+
self.stroke_width,
|
|
825
|
+
self.shape_roi_size,
|
|
826
|
+
self.stroke_color,
|
|
827
|
+
self.fill_color,
|
|
828
|
+
self.subtype.value,
|
|
829
|
+
self.options.value,
|
|
830
|
+
self.arrow_style_or_aspect_ratio,
|
|
831
|
+
self.arrow_head_size,
|
|
832
|
+
self.rounded_rect_arc_size,
|
|
833
|
+
self.position,
|
|
834
|
+
)
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
extradata = b''
|
|
838
|
+
|
|
839
|
+
if self.version >= 218 and self.subtype == ROI_SUBTYPE.TEXT:
|
|
840
|
+
style_and_justification = self.text_style
|
|
841
|
+
style_and_justification |= self.text_justification << 8
|
|
842
|
+
extradata = struct.pack(
|
|
843
|
+
self.byteorder + 'iiii',
|
|
844
|
+
self.text_size,
|
|
845
|
+
style_and_justification,
|
|
846
|
+
len(self.text_name),
|
|
847
|
+
len(self.text),
|
|
848
|
+
)
|
|
849
|
+
extradata += self.text_name.encode(self.utf16)
|
|
850
|
+
extradata += self.text.encode(self.utf16)
|
|
851
|
+
extradata += struct.pack(self.byteorder + 'f', self.text_angle)
|
|
852
|
+
|
|
853
|
+
elif self.roitype in (
|
|
854
|
+
ROI_TYPE.POLYGON,
|
|
855
|
+
ROI_TYPE.FREEHAND,
|
|
856
|
+
ROI_TYPE.TRACED,
|
|
857
|
+
ROI_TYPE.POLYLINE,
|
|
858
|
+
ROI_TYPE.FREELINE,
|
|
859
|
+
ROI_TYPE.ANGLE,
|
|
860
|
+
ROI_TYPE.POINT,
|
|
861
|
+
):
|
|
862
|
+
if self.integer_coordinates is not None:
|
|
863
|
+
if self.integer_coordinates.shape != (self.n_coordinates, 2):
|
|
864
|
+
raise ValueError(
|
|
865
|
+
'integer_coordinates.shape '
|
|
866
|
+
f'{self.integer_coordinates.shape} '
|
|
867
|
+
f'!= ({self.n_coordinates}, 2)'
|
|
868
|
+
)
|
|
869
|
+
coord = self.integer_coordinates.astype(
|
|
870
|
+
self.byteorder + 'i2', copy=False
|
|
871
|
+
)
|
|
872
|
+
extradata = coord.tobytes(order='F')
|
|
873
|
+
if self.subpixel_coordinates is not None:
|
|
874
|
+
if self.subpixel_coordinates.shape != (self.n_coordinates, 2):
|
|
875
|
+
raise ValueError(
|
|
876
|
+
'subpixel_coordinates.shape '
|
|
877
|
+
f'{self.subpixel_coordinates.shape} '
|
|
878
|
+
f'!= ({self.n_coordinates}, 2)'
|
|
879
|
+
)
|
|
880
|
+
coord = self.subpixel_coordinates.astype(
|
|
881
|
+
self.byteorder + 'f4', copy=False
|
|
882
|
+
)
|
|
883
|
+
extradata += coord.tobytes(order='F')
|
|
884
|
+
|
|
885
|
+
elif self.composite and self.roitype == ROI_TYPE.RECT:
|
|
886
|
+
assert self.multi_coordinates is not None
|
|
887
|
+
coord = self.multi_coordinates.astype(
|
|
888
|
+
self.byteorder + 'f4', copy=False
|
|
889
|
+
)
|
|
890
|
+
extradata += coord.tobytes()
|
|
891
|
+
|
|
892
|
+
header2_offset = 64 + len(extradata)
|
|
893
|
+
result.append(struct.pack(self.byteorder + 'i', header2_offset))
|
|
894
|
+
result.append(extradata)
|
|
895
|
+
|
|
896
|
+
offset = header2_offset + 64
|
|
897
|
+
name_length = len(self.name)
|
|
898
|
+
name_offset = offset if name_length > 0 else 0
|
|
899
|
+
offset += name_length * 2
|
|
900
|
+
roi_props_length = len(self.props)
|
|
901
|
+
roi_props_offset = offset if roi_props_length > 0 else 0
|
|
902
|
+
offset += roi_props_length * 2
|
|
903
|
+
counters_offset = offset if self.counters is not None else 0
|
|
904
|
+
|
|
905
|
+
result.append(
|
|
906
|
+
struct.pack(
|
|
907
|
+
self.byteorder + '4xiiiii4shBBifiii12x',
|
|
908
|
+
self.c_position,
|
|
909
|
+
self.z_position,
|
|
910
|
+
self.t_position,
|
|
911
|
+
name_offset,
|
|
912
|
+
name_length,
|
|
913
|
+
self.overlay_label_color,
|
|
914
|
+
self.overlay_font_size,
|
|
915
|
+
self.group,
|
|
916
|
+
self.image_opacity,
|
|
917
|
+
self.image_size,
|
|
918
|
+
self.float_stroke_width,
|
|
919
|
+
roi_props_offset,
|
|
920
|
+
roi_props_length,
|
|
921
|
+
counters_offset,
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if name_length > 0:
|
|
926
|
+
result.append(self.name.encode(self.utf16))
|
|
927
|
+
if roi_props_length > 0:
|
|
928
|
+
result.append(self.props.encode(self.utf16))
|
|
929
|
+
if self.counters is not None:
|
|
930
|
+
counters = numpy.asarray(
|
|
931
|
+
self.counters, dtype=self.byteorder + 'u4'
|
|
932
|
+
)
|
|
933
|
+
if self.counter_positions is not None:
|
|
934
|
+
indices = numpy.asarray(
|
|
935
|
+
self.counter_positions, dtype=self.byteorder + 'u4'
|
|
936
|
+
)
|
|
937
|
+
counters = counters & 0xFF | indices << 8
|
|
938
|
+
counters = counters.astype(self.byteorder + 'u4', copy=False)
|
|
939
|
+
result.append(counters.tobytes())
|
|
940
|
+
|
|
941
|
+
return b''.join(result)
|
|
942
|
+
|
|
943
|
+
def plot(
|
|
944
|
+
self,
|
|
945
|
+
ax: Axes | None = None,
|
|
946
|
+
*,
|
|
947
|
+
rois: Iterable[ImagejRoi] | None = None,
|
|
948
|
+
title: str | None = None,
|
|
949
|
+
bounds: bool = False,
|
|
950
|
+
invert_yaxis: bool | None = None,
|
|
951
|
+
show: bool = True,
|
|
952
|
+
**kwargs: Any,
|
|
953
|
+
) -> None:
|
|
954
|
+
"""Plot draft of coordinates using matplotlib."""
|
|
955
|
+
fig: Any
|
|
956
|
+
roitype = self.roitype
|
|
957
|
+
subtype = self.subtype
|
|
958
|
+
|
|
959
|
+
if ax is None:
|
|
960
|
+
from matplotlib import pyplot
|
|
961
|
+
from matplotlib.patches import Rectangle
|
|
962
|
+
|
|
963
|
+
fig, ax = pyplot.subplots()
|
|
964
|
+
ax.set_aspect('equal')
|
|
965
|
+
ax.set_facecolor((0.8, 0.8, 0.8))
|
|
966
|
+
if title is None:
|
|
967
|
+
fig.suptitle(f'{self.name!r}')
|
|
968
|
+
else:
|
|
969
|
+
fig.suptitle(title)
|
|
970
|
+
if bounds and rois is None:
|
|
971
|
+
ax.set_title(f'{roitype.name} {subtype.name}')
|
|
972
|
+
ax.add_patch(
|
|
973
|
+
Rectangle(
|
|
974
|
+
(self.left, self.top),
|
|
975
|
+
self.right - self.left,
|
|
976
|
+
self.bottom - self.top,
|
|
977
|
+
linewidth=1,
|
|
978
|
+
edgecolor='0.9',
|
|
979
|
+
facecolor='none',
|
|
980
|
+
)
|
|
981
|
+
)
|
|
982
|
+
if invert_yaxis is None:
|
|
983
|
+
invert_yaxis = True
|
|
984
|
+
else:
|
|
985
|
+
fig = None
|
|
986
|
+
if invert_yaxis is None:
|
|
987
|
+
invert_yaxis = False
|
|
988
|
+
|
|
989
|
+
if rois is not None:
|
|
990
|
+
for roi in rois:
|
|
991
|
+
roi.plot(ax=ax, **kwargs)
|
|
992
|
+
if invert_yaxis:
|
|
993
|
+
ax.invert_yaxis()
|
|
994
|
+
if show:
|
|
995
|
+
pyplot.show()
|
|
996
|
+
return
|
|
997
|
+
|
|
998
|
+
if 'color' not in kwargs and 'c' not in kwargs:
|
|
999
|
+
kwargs['color'] = self.hexcolor(self.stroke_color)
|
|
1000
|
+
if 'linewidth' not in kwargs and 'lw' not in kwargs:
|
|
1001
|
+
# TODO: use data units
|
|
1002
|
+
if self.float_stroke_width > 0.0:
|
|
1003
|
+
kwargs['linewidth'] = self.float_stroke_width
|
|
1004
|
+
elif self.stroke_width > 0:
|
|
1005
|
+
kwargs['linewidth'] = self.stroke_width
|
|
1006
|
+
if roitype == ROI_TYPE.POINT:
|
|
1007
|
+
if 'marker' not in kwargs:
|
|
1008
|
+
kwargs['marker'] = 'x'
|
|
1009
|
+
if 'linestyle' not in kwargs:
|
|
1010
|
+
kwargs['linestyle'] = ''
|
|
1011
|
+
|
|
1012
|
+
if roitype == ROI_TYPE.LINE and subtype == ROI_SUBTYPE.ARROW:
|
|
1013
|
+
line = self.coordinates()
|
|
1014
|
+
x, y = line[0]
|
|
1015
|
+
dx, dy = line[1] - line[0]
|
|
1016
|
+
if 'head_width' not in kwargs and self.arrow_head_size > 0:
|
|
1017
|
+
kwargs['head_width'] = self.arrow_head_size
|
|
1018
|
+
kwargs['length_includes_head'] = True
|
|
1019
|
+
ax.arrow(x, y, dx, dy, **kwargs)
|
|
1020
|
+
if self.options & ROI_OPTIONS.DOUBLE_HEADED:
|
|
1021
|
+
x, y = line[1]
|
|
1022
|
+
ax.arrow(x, y, -dx, -dy, **kwargs)
|
|
1023
|
+
elif roitype == ROI_TYPE.RECT and subtype == ROI_SUBTYPE.TEXT:
|
|
1024
|
+
coords = self.coordinates(multi=True)[0]
|
|
1025
|
+
if 'fontsize' not in kwargs and self.text_size > 0:
|
|
1026
|
+
kwargs['fontsize'] = self.text_size
|
|
1027
|
+
text = ax.text(
|
|
1028
|
+
coords[1][0],
|
|
1029
|
+
coords[1][1],
|
|
1030
|
+
self.text,
|
|
1031
|
+
va='center_baseline',
|
|
1032
|
+
rotation=self.text_angle,
|
|
1033
|
+
rotation_mode='anchor',
|
|
1034
|
+
**kwargs,
|
|
1035
|
+
)
|
|
1036
|
+
scale_text(text, width=abs(coords[2, 0] - coords[0, 0]))
|
|
1037
|
+
# ax.plot(
|
|
1038
|
+
# coords[:, 0],
|
|
1039
|
+
# coords[:, 1],
|
|
1040
|
+
# linewidth=1,
|
|
1041
|
+
# color=kwargs.get('color', 0.9),
|
|
1042
|
+
# ls=':',
|
|
1043
|
+
# )
|
|
1044
|
+
else:
|
|
1045
|
+
for coords in self.coordinates(multi=True):
|
|
1046
|
+
ax.plot(coords[:, 0], coords[:, 1], **kwargs)
|
|
1047
|
+
|
|
1048
|
+
# integer limits might be bogus
|
|
1049
|
+
if (
|
|
1050
|
+
self.left < self.right
|
|
1051
|
+
and self.top < self.bottom
|
|
1052
|
+
and self.left > -256
|
|
1053
|
+
and self.left < 24576
|
|
1054
|
+
and self.bottom > -256
|
|
1055
|
+
and self.bottom < 24576
|
|
1056
|
+
and self.right > -256
|
|
1057
|
+
and self.right < 24576
|
|
1058
|
+
and self.top > -256
|
|
1059
|
+
and self.top < 24576
|
|
1060
|
+
):
|
|
1061
|
+
ax.plot(self.left, self.bottom, '')
|
|
1062
|
+
ax.plot(self.right, self.top, '')
|
|
1063
|
+
|
|
1064
|
+
if invert_yaxis:
|
|
1065
|
+
ax.invert_yaxis()
|
|
1066
|
+
|
|
1067
|
+
if show and fig is not None:
|
|
1068
|
+
pyplot.show()
|
|
1069
|
+
|
|
1070
|
+
def coordinates(
|
|
1071
|
+
self,
|
|
1072
|
+
*,
|
|
1073
|
+
multi: bool = False,
|
|
1074
|
+
) -> NDArray[Any] | list[NDArray[Any]]:
|
|
1075
|
+
"""Return x, y coordinates as numpy array for display."""
|
|
1076
|
+
coords: Any
|
|
1077
|
+
if self.subpixel_coordinates is not None:
|
|
1078
|
+
coords = self.subpixel_coordinates.copy()
|
|
1079
|
+
elif self.integer_coordinates is not None:
|
|
1080
|
+
coords = self.integer_coordinates + [ # noqa: RUF005
|
|
1081
|
+
self.left,
|
|
1082
|
+
self.top,
|
|
1083
|
+
]
|
|
1084
|
+
elif self.multi_coordinates is not None:
|
|
1085
|
+
coordslist = self.path2coords(self.multi_coordinates)
|
|
1086
|
+
if not multi:
|
|
1087
|
+
return sorted(coordslist, key=lambda x: x.size)[-1]
|
|
1088
|
+
return coordslist
|
|
1089
|
+
elif self.roitype == ROI_TYPE.LINE:
|
|
1090
|
+
coords = numpy.array(
|
|
1091
|
+
[[self.x1, self.y1], [self.x2, self.y2]], numpy.float32
|
|
1092
|
+
)
|
|
1093
|
+
elif self.roitype == ROI_TYPE.OVAL:
|
|
1094
|
+
coords = oval([[self.left, self.top], [self.right, self.bottom]])
|
|
1095
|
+
elif self.roitype == ROI_TYPE.RECT:
|
|
1096
|
+
coords = numpy.array(
|
|
1097
|
+
[
|
|
1098
|
+
[self.left, self.top],
|
|
1099
|
+
[self.left, self.bottom],
|
|
1100
|
+
[self.right, self.bottom],
|
|
1101
|
+
[self.right, self.top],
|
|
1102
|
+
[self.left, self.top],
|
|
1103
|
+
],
|
|
1104
|
+
numpy.float32,
|
|
1105
|
+
)
|
|
1106
|
+
else:
|
|
1107
|
+
coords = numpy.empty((0, 2), dtype=self.byteorder + 'i4')
|
|
1108
|
+
return [coords] if multi else coords
|
|
1109
|
+
|
|
1110
|
+
def hexcolor(self, b: bytes, /, default: str | None = None) -> str | None:
|
|
1111
|
+
"""Return color (bytes) as hex triplet or None if black."""
|
|
1112
|
+
if b == ROI_COLOR_NONE:
|
|
1113
|
+
return default
|
|
1114
|
+
if self.byteorder == '>':
|
|
1115
|
+
return f'#{b[1]:02x}{b[2]:02x}{b[3]:02x}'
|
|
1116
|
+
return f'#{b[3]:02x}{b[2]:02x}{b[1]:02x}'
|
|
1117
|
+
|
|
1118
|
+
@staticmethod
|
|
1119
|
+
def path2coords(
|
|
1120
|
+
multi_coordinates: NDArray[numpy.float32], /
|
|
1121
|
+
) -> list[NDArray[numpy.float32]]:
|
|
1122
|
+
"""Return list of coordinate arrays from 2D geometric path."""
|
|
1123
|
+
coordinates: list[NDArray[numpy.float32]] = []
|
|
1124
|
+
points: list[tuple[float, float]] = []
|
|
1125
|
+
path: list[float] = []
|
|
1126
|
+
|
|
1127
|
+
path = multi_coordinates.tolist()
|
|
1128
|
+
n = 0
|
|
1129
|
+
m = 0
|
|
1130
|
+
while n < len(path):
|
|
1131
|
+
op = int(path[n])
|
|
1132
|
+
if op == 0:
|
|
1133
|
+
# MOVETO
|
|
1134
|
+
if n > 0:
|
|
1135
|
+
coordinates.append(
|
|
1136
|
+
numpy.array(points, dtype=numpy.float32)
|
|
1137
|
+
)
|
|
1138
|
+
points = []
|
|
1139
|
+
points.append((path[n + 1], path[n + 2]))
|
|
1140
|
+
m = len(points) - 1
|
|
1141
|
+
n += 3
|
|
1142
|
+
elif op == 1:
|
|
1143
|
+
# LINETO
|
|
1144
|
+
points.append((path[n + 1], path[n + 2]))
|
|
1145
|
+
n += 3
|
|
1146
|
+
elif op == 4:
|
|
1147
|
+
# CLOSE
|
|
1148
|
+
points.append(points[m])
|
|
1149
|
+
n += 1
|
|
1150
|
+
elif op == 2 or op == 3: # noqa: PLR1714
|
|
1151
|
+
# QUADTO or CUBICTO
|
|
1152
|
+
raise NotImplementedError(
|
|
1153
|
+
f'PathIterator command {op!r} not supported'
|
|
1154
|
+
)
|
|
1155
|
+
else:
|
|
1156
|
+
raise RuntimeError(f'invalid PathIterator command {op!r}')
|
|
1157
|
+
|
|
1158
|
+
coordinates.append(numpy.array(points, dtype=numpy.float32))
|
|
1159
|
+
return coordinates
|
|
1160
|
+
|
|
1161
|
+
@staticmethod
|
|
1162
|
+
def min_int_coord(value: int | None = None) -> int:
|
|
1163
|
+
"""Return minimum integer coordinate value.
|
|
1164
|
+
|
|
1165
|
+
The default, -5000, is used by ImageJ.
|
|
1166
|
+
A value of -32768 means to use int16 range, 0 means uint16 range.
|
|
1167
|
+
|
|
1168
|
+
"""
|
|
1169
|
+
if value is None:
|
|
1170
|
+
return -5000
|
|
1171
|
+
if -32768 <= value <= 0:
|
|
1172
|
+
return int(value)
|
|
1173
|
+
raise ValueError(f'{value=} out of range')
|
|
1174
|
+
|
|
1175
|
+
@property
|
|
1176
|
+
def composite(self) -> bool:
|
|
1177
|
+
"""ROI is composite shape."""
|
|
1178
|
+
return self.shape_roi_size > 0
|
|
1179
|
+
|
|
1180
|
+
@property
|
|
1181
|
+
def subpixelresolution(self) -> bool:
|
|
1182
|
+
"""ROI has subpixel resolution."""
|
|
1183
|
+
return self.version >= 222 and bool(
|
|
1184
|
+
self.options & ROI_OPTIONS.SUB_PIXEL_RESOLUTION
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
@property
|
|
1188
|
+
def drawoffset(self) -> bool:
|
|
1189
|
+
"""ROI has draw offset."""
|
|
1190
|
+
return self.subpixelresolution and bool(
|
|
1191
|
+
self.options & ROI_OPTIONS.DRAW_OFFSET
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
@property
|
|
1195
|
+
def subpixelrect(self) -> bool:
|
|
1196
|
+
"""ROI has subpixel rectangle."""
|
|
1197
|
+
return (
|
|
1198
|
+
self.version >= 223
|
|
1199
|
+
and self.subpixelresolution
|
|
1200
|
+
and self.roitype in (ROI_TYPE.RECT, ROI_TYPE.OVAL)
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
@property
|
|
1204
|
+
def autoname(self) -> str:
|
|
1205
|
+
"""Name generated from positions."""
|
|
1206
|
+
y = (self.bottom - self.top) // 2
|
|
1207
|
+
x = (self.right - self.left) // 2
|
|
1208
|
+
name = f'{y:05}-{x:05}'
|
|
1209
|
+
if self.counter_positions is not None:
|
|
1210
|
+
tzc = int(self.counter_positions.max())
|
|
1211
|
+
name = f'{tzc:05}-' + name
|
|
1212
|
+
return name
|
|
1213
|
+
|
|
1214
|
+
@property
|
|
1215
|
+
def utf16(self) -> str:
|
|
1216
|
+
"""UTF-16 codec depending on byteorder."""
|
|
1217
|
+
return 'utf-16' + ('be' if self.byteorder == '>' else 'le')
|
|
1218
|
+
|
|
1219
|
+
def __hash__(self) -> int:
|
|
1220
|
+
"""Return hash of ImagejRoi."""
|
|
1221
|
+
return hash(
|
|
1222
|
+
(
|
|
1223
|
+
self.tobytes(),
|
|
1224
|
+
self.left,
|
|
1225
|
+
self.top,
|
|
1226
|
+
self.right,
|
|
1227
|
+
self.bottom,
|
|
1228
|
+
)
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
def __eq__(self, other: object) -> bool:
|
|
1232
|
+
"""Return True if two ImagejRoi are the same."""
|
|
1233
|
+
return (
|
|
1234
|
+
isinstance(other, ImagejRoi)
|
|
1235
|
+
and self.tobytes() == other.tobytes()
|
|
1236
|
+
and self.left == other.left
|
|
1237
|
+
and self.top == other.top
|
|
1238
|
+
and self.right == other.right
|
|
1239
|
+
and self.bottom == other.bottom
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
def __repr__(self) -> str:
|
|
1243
|
+
info = [f'{self.__class__.__name__}(']
|
|
1244
|
+
for name, value in self.__dict__.items():
|
|
1245
|
+
if isinstance(value, numpy.ndarray):
|
|
1246
|
+
v = repr(value).replace(' ', ' ')
|
|
1247
|
+
v = v.replace('([[', '([\n [')
|
|
1248
|
+
info.append(f'{name}=numpy.{v},')
|
|
1249
|
+
elif value == getattr(ImagejRoi, name):
|
|
1250
|
+
pass
|
|
1251
|
+
elif isinstance(value, enum.Enum):
|
|
1252
|
+
info.append(f'{name}={enumstr(value)},')
|
|
1253
|
+
else:
|
|
1254
|
+
info.append(f'{name}={value!r},')
|
|
1255
|
+
return indent(*info, end='\n)')
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def scale_text(
|
|
1259
|
+
text: Any,
|
|
1260
|
+
width: float,
|
|
1261
|
+
*,
|
|
1262
|
+
offset: tuple[float, float] | None = None,
|
|
1263
|
+
) -> None:
|
|
1264
|
+
"""Scale matplotlib text to width in data coordinates."""
|
|
1265
|
+
from matplotlib.patheffects import AbstractPathEffect
|
|
1266
|
+
from matplotlib.transforms import Bbox
|
|
1267
|
+
|
|
1268
|
+
class TextScaler(AbstractPathEffect):
|
|
1269
|
+
def __init__(
|
|
1270
|
+
self,
|
|
1271
|
+
text: Any,
|
|
1272
|
+
width: float,
|
|
1273
|
+
offset: tuple[float, float] | None = None,
|
|
1274
|
+
) -> None:
|
|
1275
|
+
if offset is None:
|
|
1276
|
+
offset = (0.0, 0.0)
|
|
1277
|
+
super().__init__(offset)
|
|
1278
|
+
self._text = text
|
|
1279
|
+
self._width = width
|
|
1280
|
+
|
|
1281
|
+
def draw_path(
|
|
1282
|
+
self,
|
|
1283
|
+
renderer: Any,
|
|
1284
|
+
gc: Any,
|
|
1285
|
+
tpath: Any,
|
|
1286
|
+
affine: Any,
|
|
1287
|
+
rgbFace: Any = None, # noqa: N803
|
|
1288
|
+
) -> None:
|
|
1289
|
+
ax = self._text.axes
|
|
1290
|
+
renderer = ax.get_figure().canvas.get_renderer()
|
|
1291
|
+
bbox = text.get_window_extent(renderer=renderer)
|
|
1292
|
+
bbox = Bbox(ax.transData.inverted().transform(bbox))
|
|
1293
|
+
if self._width > 0 and bbox.width > 0:
|
|
1294
|
+
scale = self._width / bbox.width
|
|
1295
|
+
affine = affine.from_values(scale, 0, 0, scale, 0, 0) + affine
|
|
1296
|
+
renderer.draw_path(gc, tpath, affine, rgbFace)
|
|
1297
|
+
|
|
1298
|
+
text.set_path_effects([TextScaler(text, width, offset)])
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def oval(rect: ArrayLike, /, points: int = 33) -> NDArray[numpy.float32]:
|
|
1302
|
+
"""Return coordinates of oval from rectangle corners."""
|
|
1303
|
+
arr = numpy.asarray(rect, dtype=numpy.float32)
|
|
1304
|
+
c = numpy.linspace(0.0, 2.0 * numpy.pi, num=points, dtype=numpy.float32)
|
|
1305
|
+
c = numpy.array([numpy.cos(c), numpy.sin(c)]).T
|
|
1306
|
+
r = arr[1] - arr[0]
|
|
1307
|
+
r /= 2.0
|
|
1308
|
+
c *= r
|
|
1309
|
+
c += arr[0] + r
|
|
1310
|
+
return c
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def indent(*args: Any, sep: str = '', end: str = '') -> str:
|
|
1314
|
+
"""Return joined string representations of objects with indented lines."""
|
|
1315
|
+
text: str = (sep + '\n').join(
|
|
1316
|
+
arg if isinstance(arg, str) else repr(arg) for arg in args
|
|
1317
|
+
)
|
|
1318
|
+
return (
|
|
1319
|
+
'\n'.join(
|
|
1320
|
+
(' ' + line if line else line)
|
|
1321
|
+
for line in text.splitlines()
|
|
1322
|
+
if line
|
|
1323
|
+
)[4:]
|
|
1324
|
+
+ end
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def enumstr(v: enum.Enum | None, /) -> str:
|
|
1329
|
+
"""Return IntEnum or IntFlag as str."""
|
|
1330
|
+
# repr() and str() of enums are type, value, and version dependent
|
|
1331
|
+
if v is None:
|
|
1332
|
+
return 'None'
|
|
1333
|
+
# return f'{v.__class__.__name__}({v.value})'
|
|
1334
|
+
s = repr(v)
|
|
1335
|
+
s = s[1:].split(':', 1)[0]
|
|
1336
|
+
if 'UNKNOWN' in s:
|
|
1337
|
+
s = f'{v.__class__.__name__}({v.value})'
|
|
1338
|
+
elif '|' in s:
|
|
1339
|
+
# IntFlag combination
|
|
1340
|
+
s = s.replace('|', ' | ' + v.__class__.__name__ + '.')
|
|
1341
|
+
elif not hasattr(v, 'name') or v.name is None:
|
|
1342
|
+
s = f'{v.__class__.__name__}({v.value})'
|
|
1343
|
+
return s
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def logger() -> logging.Logger:
|
|
1347
|
+
"""Return logger for roifile module."""
|
|
1348
|
+
return logging.getLogger('roifile')
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1352
|
+
"""Roifile command line usage main function.
|
|
1353
|
+
|
|
1354
|
+
Show all ImageJ ROIs in file or all files in directory::
|
|
1355
|
+
|
|
1356
|
+
python -m roifile file_or_directory
|
|
1357
|
+
|
|
1358
|
+
"""
|
|
1359
|
+
from glob import glob
|
|
1360
|
+
|
|
1361
|
+
if argv is None:
|
|
1362
|
+
argv = sys.argv
|
|
1363
|
+
|
|
1364
|
+
if len(argv) == 1:
|
|
1365
|
+
files = glob('*.roi')
|
|
1366
|
+
files += glob('*.zip')
|
|
1367
|
+
files += glob('*.tif')
|
|
1368
|
+
elif '*' in argv[1]:
|
|
1369
|
+
files = glob(argv[1])
|
|
1370
|
+
elif os.path.isdir(argv[1]):
|
|
1371
|
+
files = []
|
|
1372
|
+
for ext in ('roi', 'zip', 'tif'):
|
|
1373
|
+
files += glob(f'{argv[1]}/*.{ext}')
|
|
1374
|
+
else:
|
|
1375
|
+
files = argv[1:]
|
|
1376
|
+
|
|
1377
|
+
for fname in files:
|
|
1378
|
+
print(fname) # noqa: T201
|
|
1379
|
+
try:
|
|
1380
|
+
rois = ImagejRoi.fromfile(fname)
|
|
1381
|
+
title = os.path.split(fname)[-1]
|
|
1382
|
+
if isinstance(rois, list):
|
|
1383
|
+
for roi in rois:
|
|
1384
|
+
print(roi, '\n') # noqa: T201
|
|
1385
|
+
if sys.flags.dev_mode:
|
|
1386
|
+
assert roi == ImagejRoi.frombytes(roi.tobytes())
|
|
1387
|
+
if rois:
|
|
1388
|
+
rois[0].plot(rois=rois, title=title)
|
|
1389
|
+
else:
|
|
1390
|
+
print(rois, '\n') # noqa: T201
|
|
1391
|
+
if sys.flags.dev_mode:
|
|
1392
|
+
assert rois == ImagejRoi.frombytes(rois.tobytes())
|
|
1393
|
+
rois.plot(title=title)
|
|
1394
|
+
except ValueError as exc:
|
|
1395
|
+
if sys.flags.dev_mode:
|
|
1396
|
+
raise
|
|
1397
|
+
print(fname, exc) # noqa: T201
|
|
1398
|
+
continue
|
|
1399
|
+
return 0
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
if __name__ == '__main__':
|
|
1403
|
+
sys.exit(main())
|