roifile 2023.8.30__py3-none-any.whl → 2024.3.20__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.
Potentially problematic release.
This version of roifile might be problematic. Click here for more details.
- roifile/roifile.py +165 -96
- {roifile-2023.8.30.dist-info → roifile-2024.3.20.dist-info}/LICENSE +1 -1
- {roifile-2023.8.30.dist-info → roifile-2024.3.20.dist-info}/METADATA +20 -47
- roifile-2024.3.20.dist-info/RECORD +10 -0
- {roifile-2023.8.30.dist-info → roifile-2024.3.20.dist-info}/WHEEL +1 -1
- roifile-2023.8.30.dist-info/RECORD +0 -10
- {roifile-2023.8.30.dist-info → roifile-2024.3.20.dist-info}/entry_points.txt +0 -0
- {roifile-2023.8.30.dist-info → roifile-2024.3.20.dist-info}/top_level.txt +0 -0
roifile/roifile.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# roifile.py
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2020-
|
|
3
|
+
# Copyright (c) 2020-2024, Christoph Gohlke
|
|
4
4
|
# All rights reserved.
|
|
5
5
|
#
|
|
6
6
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -39,7 +39,7 @@ interest, geometric shapes, paths, text, and whatnot for image overlays.
|
|
|
39
39
|
|
|
40
40
|
:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
|
|
41
41
|
:License: BSD 3-Clause
|
|
42
|
-
:Version:
|
|
42
|
+
:Version: 2024.3.20
|
|
43
43
|
:DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
|
|
44
44
|
|
|
45
45
|
Quickstart
|
|
@@ -48,7 +48,7 @@ Quickstart
|
|
|
48
48
|
Install the roifile package and all dependencies from the
|
|
49
49
|
`Python Package Index <https://pypi.org/project/roifile/>`_::
|
|
50
50
|
|
|
51
|
-
python -m pip install -U roifile[all]
|
|
51
|
+
python -m pip install -U "roifile[all]"
|
|
52
52
|
|
|
53
53
|
View overlays stored in a ROI, ZIP, or TIFF file::
|
|
54
54
|
|
|
@@ -65,14 +65,25 @@ Requirements
|
|
|
65
65
|
This revision was tested with the following requirements and dependencies
|
|
66
66
|
(other versions may work):
|
|
67
67
|
|
|
68
|
-
- `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.
|
|
69
|
-
- `Numpy <https://pypi.org/project/numpy/>`_ 1.
|
|
70
|
-
- `Tifffile <https://pypi.org/project/tifffile/>`_
|
|
71
|
-
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.
|
|
68
|
+
- `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.8, 3.12.2
|
|
69
|
+
- `Numpy <https://pypi.org/project/numpy/>`_ 1.26.4
|
|
70
|
+
- `Tifffile <https://pypi.org/project/tifffile/>`_ 2024.2.12 (optional)
|
|
71
|
+
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.8.3 (optional)
|
|
72
72
|
|
|
73
73
|
Revisions
|
|
74
74
|
---------
|
|
75
75
|
|
|
76
|
+
2024.3.20
|
|
77
|
+
|
|
78
|
+
- Fix writing generator of ROIs (#9).
|
|
79
|
+
|
|
80
|
+
2024.1.10
|
|
81
|
+
|
|
82
|
+
- Support text rotation.
|
|
83
|
+
- Improve text rendering.
|
|
84
|
+
- Avoid array copies.
|
|
85
|
+
- Limit size read from files.
|
|
86
|
+
|
|
76
87
|
2023.8.30
|
|
77
88
|
|
|
78
89
|
- Fix linting issues.
|
|
@@ -95,47 +106,9 @@ Revisions
|
|
|
95
106
|
|
|
96
107
|
2022.7.29
|
|
97
108
|
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
2022.3.18
|
|
101
|
-
|
|
102
|
-
- Fix creating ROIs from float coordinates exceeding int16 range (#7).
|
|
103
|
-
- Fix bottom-right bounds in ImagejRoi.frompoints.
|
|
104
|
-
|
|
105
|
-
2022.2.2
|
|
106
|
-
|
|
107
|
-
- Add type hints.
|
|
108
|
-
- Change ImagejRoi to dataclass.
|
|
109
|
-
- Drop support for Python 3.7 and numpy < 1.19 (NEP29).
|
|
110
|
-
|
|
111
|
-
2021.6.6
|
|
112
|
-
|
|
113
|
-
- Add enums for point types and sizes.
|
|
114
|
-
|
|
115
|
-
2020.11.28
|
|
116
|
-
|
|
117
|
-
- Support group attribute.
|
|
118
|
-
- Add roiread and roiwrite functions (#3).
|
|
119
|
-
- Use UUID as default name of ROI in ImagejRoi.frompoints (#2).
|
|
120
|
-
|
|
121
|
-
2020.8.13
|
|
109
|
+
- …
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
- Support os.PathLike file names.
|
|
125
|
-
|
|
126
|
-
2020.5.28
|
|
127
|
-
|
|
128
|
-
- Fix int32 to hex color conversion.
|
|
129
|
-
- Fix coordinates of closing path.
|
|
130
|
-
- Fix reading TIFF files with no overlays.
|
|
131
|
-
|
|
132
|
-
2020.5.1
|
|
133
|
-
|
|
134
|
-
- Split positions from counters.
|
|
135
|
-
|
|
136
|
-
2020.2.12
|
|
137
|
-
|
|
138
|
-
- Initial release.
|
|
111
|
+
Refer to the CHANGES file for older revisions.
|
|
139
112
|
|
|
140
113
|
Notes
|
|
141
114
|
-----
|
|
@@ -197,7 +170,7 @@ View the overlays stored in a ROI, ZIP, or TIFF file from a command line::
|
|
|
197
170
|
|
|
198
171
|
from __future__ import annotations
|
|
199
172
|
|
|
200
|
-
__version__ = '
|
|
173
|
+
__version__ = '2024.3.20'
|
|
201
174
|
|
|
202
175
|
__all__ = [
|
|
203
176
|
'roiread',
|
|
@@ -213,31 +186,37 @@ __all__ = [
|
|
|
213
186
|
|
|
214
187
|
import dataclasses
|
|
215
188
|
import enum
|
|
189
|
+
import logging
|
|
216
190
|
import os
|
|
217
191
|
import struct
|
|
218
192
|
import sys
|
|
193
|
+
import uuid
|
|
219
194
|
from typing import TYPE_CHECKING
|
|
220
195
|
|
|
221
196
|
import numpy
|
|
222
197
|
|
|
223
198
|
if TYPE_CHECKING:
|
|
224
199
|
from collections.abc import Iterable
|
|
225
|
-
from typing import Any, Literal
|
|
200
|
+
from typing import Any, Literal
|
|
226
201
|
|
|
227
202
|
from numpy.typing import ArrayLike, NDArray
|
|
228
203
|
|
|
229
|
-
ZipFileMode = Union[Literal['r'], Literal['w'], Literal['x'], Literal['a']]
|
|
230
|
-
|
|
231
204
|
|
|
232
205
|
def roiread(
|
|
233
|
-
filename: os.PathLike[Any] | str,
|
|
206
|
+
filename: os.PathLike[Any] | str,
|
|
207
|
+
/,
|
|
208
|
+
*,
|
|
209
|
+
min_int_coord: int | None = None,
|
|
210
|
+
maxsize: int = 268435456, # 256 MB
|
|
234
211
|
) -> ImagejRoi | list[ImagejRoi]:
|
|
235
212
|
"""Return ImagejRoi instance(s) from ROI, ZIP, or TIFF file.
|
|
236
213
|
|
|
237
214
|
For ZIP or TIFF files, return a list of ImagejRoi.
|
|
238
215
|
|
|
239
216
|
"""
|
|
240
|
-
return ImagejRoi.fromfile(
|
|
217
|
+
return ImagejRoi.fromfile(
|
|
218
|
+
filename, min_int_coord=min_int_coord, maxsize=maxsize
|
|
219
|
+
)
|
|
241
220
|
|
|
242
221
|
|
|
243
222
|
def roiwrite(
|
|
@@ -246,7 +225,7 @@ def roiwrite(
|
|
|
246
225
|
/,
|
|
247
226
|
*,
|
|
248
227
|
name: str | Iterable[str] | None = None,
|
|
249
|
-
mode:
|
|
228
|
+
mode: Literal['r', 'w', 'x', 'a'] | None = None,
|
|
250
229
|
) -> None:
|
|
251
230
|
"""Write ImagejRoi instance(s) to ROI or ZIP file.
|
|
252
231
|
|
|
@@ -263,14 +242,20 @@ def roiwrite(
|
|
|
263
242
|
if mode is None:
|
|
264
243
|
mode = 'a' if os.path.exists(filename) else 'w'
|
|
265
244
|
|
|
266
|
-
if name is None:
|
|
267
|
-
|
|
268
|
-
|
|
245
|
+
if name is not None:
|
|
246
|
+
if isinstance(name, str):
|
|
247
|
+
raise ValueError("'name' is not an iterable of str")
|
|
248
|
+
name = iter(name)
|
|
269
249
|
|
|
270
250
|
import zipfile
|
|
271
251
|
|
|
272
252
|
with zipfile.ZipFile(filename, mode) as zf:
|
|
273
|
-
for
|
|
253
|
+
for r in roi:
|
|
254
|
+
if name is None:
|
|
255
|
+
n = r.name if r.name else r.autoname
|
|
256
|
+
else:
|
|
257
|
+
n = next(name)
|
|
258
|
+
n = n if n[-4:].lower() == '.roi' else n + '.roi'
|
|
274
259
|
with zf.open(n, 'w') as fh:
|
|
275
260
|
fh.write(r.tobytes())
|
|
276
261
|
return None
|
|
@@ -351,7 +336,7 @@ ROI_COLOR_NONE = b'\x00\x00\x00\x00'
|
|
|
351
336
|
class ImagejRoi:
|
|
352
337
|
"""Read and write ImageJ ROI format."""
|
|
353
338
|
|
|
354
|
-
byteorder: Literal['>'
|
|
339
|
+
byteorder: Literal['>', '<'] = '>'
|
|
355
340
|
roitype: ROI_TYPE = ROI_TYPE.POLYGON
|
|
356
341
|
subtype: ROI_SUBTYPE = ROI_SUBTYPE.UNDEFINED
|
|
357
342
|
options: ROI_OPTIONS = ROI_OPTIONS(0)
|
|
@@ -391,6 +376,7 @@ class ImagejRoi:
|
|
|
391
376
|
text_size: int = 0
|
|
392
377
|
text_style: int = 0
|
|
393
378
|
text_justification: int = 0
|
|
379
|
+
text_angle: float = 0.0
|
|
394
380
|
text_name: str = ''
|
|
395
381
|
text: str = ''
|
|
396
382
|
counters: NDArray[numpy.uint8] | None = None
|
|
@@ -440,8 +426,6 @@ class ImagejRoi:
|
|
|
440
426
|
self.t_position = t + 1
|
|
441
427
|
if name is None:
|
|
442
428
|
if index is None:
|
|
443
|
-
import uuid
|
|
444
|
-
|
|
445
429
|
name = str(uuid.uuid1())
|
|
446
430
|
else:
|
|
447
431
|
name = f'F{self.t_position:02}-C{index}'
|
|
@@ -452,11 +436,11 @@ class ImagejRoi:
|
|
|
452
436
|
numpy.any(coords > 60000) or numpy.any(coords < -5000)
|
|
453
437
|
):
|
|
454
438
|
self.options |= ROI_OPTIONS.SUB_PIXEL_RESOLUTION
|
|
455
|
-
self.subpixel_coordinates = coords.astype(
|
|
439
|
+
self.subpixel_coordinates = coords.astype(numpy.float32, copy=True)
|
|
456
440
|
if coords.dtype.kind == 'f':
|
|
457
441
|
coords = numpy.round(coords)
|
|
458
442
|
|
|
459
|
-
coords = numpy.array(coords, dtype=
|
|
443
|
+
coords = numpy.array(coords, dtype=numpy.int32)
|
|
460
444
|
|
|
461
445
|
left_top = coords.min(axis=0)
|
|
462
446
|
right_bottom = coords.max(axis=0)
|
|
@@ -479,6 +463,7 @@ class ImagejRoi:
|
|
|
479
463
|
/,
|
|
480
464
|
*,
|
|
481
465
|
min_int_coord: int | None = None,
|
|
466
|
+
maxsize: int = 268435456, # 256 MB
|
|
482
467
|
) -> ImagejRoi | list[ImagejRoi]:
|
|
483
468
|
"""Return ImagejRoi instance from ROI, ZIP, or TIFF file.
|
|
484
469
|
|
|
@@ -516,13 +501,14 @@ class ImagejRoi:
|
|
|
516
501
|
with zipfile.ZipFile(filename) as zf:
|
|
517
502
|
return [
|
|
518
503
|
cls.frombytes(
|
|
519
|
-
zf.open(name).read(),
|
|
504
|
+
zf.open(name).read(maxsize),
|
|
505
|
+
min_int_coord=min_int_coord,
|
|
520
506
|
)
|
|
521
507
|
for name in zf.namelist()
|
|
522
508
|
]
|
|
523
509
|
|
|
524
510
|
with open(filename, 'rb') as fh:
|
|
525
|
-
data = fh.read()
|
|
511
|
+
data = fh.read(maxsize)
|
|
526
512
|
return cls.frombytes(data, min_int_coord=min_int_coord)
|
|
527
513
|
|
|
528
514
|
@classmethod
|
|
@@ -535,7 +521,7 @@ class ImagejRoi:
|
|
|
535
521
|
) -> ImagejRoi:
|
|
536
522
|
"""Return ImagejRoi instance from bytes."""
|
|
537
523
|
if data[:4] != b'Iout':
|
|
538
|
-
raise ValueError('not an ImageJ ROI')
|
|
524
|
+
raise ValueError(f'not an ImageJ ROI {data[:4]!r}')
|
|
539
525
|
|
|
540
526
|
self = cls()
|
|
541
527
|
|
|
@@ -638,8 +624,10 @@ class ImagejRoi:
|
|
|
638
624
|
buffer=data,
|
|
639
625
|
offset=counters_offset,
|
|
640
626
|
)
|
|
641
|
-
self.counters = (counters & 0xFF).astype(
|
|
642
|
-
self.counter_positions = (counters >> 8).astype(
|
|
627
|
+
self.counters = (counters & 0xFF).astype(numpy.uint8)
|
|
628
|
+
self.counter_positions = (counters >> 8).astype(
|
|
629
|
+
numpy.uint32, copy=False
|
|
630
|
+
)
|
|
643
631
|
|
|
644
632
|
if self.version >= 218 and self.subtype == ROI_SUBTYPE.TEXT:
|
|
645
633
|
(
|
|
@@ -656,6 +644,11 @@ class ImagejRoi:
|
|
|
656
644
|
)
|
|
657
645
|
off += name_length * 2
|
|
658
646
|
self.text = data[off : off + text_length * 2].decode(self.utf16)
|
|
647
|
+
if self.version >= 225:
|
|
648
|
+
off += text_length * 2
|
|
649
|
+
self.text_angle = struct.unpack(
|
|
650
|
+
self.byteorder + 'f', data[off : off + 4]
|
|
651
|
+
)[0]
|
|
659
652
|
|
|
660
653
|
elif self.roitype in (
|
|
661
654
|
ROI_TYPE.POLYGON,
|
|
@@ -672,7 +665,7 @@ class ImagejRoi:
|
|
|
672
665
|
buffer=data,
|
|
673
666
|
offset=64,
|
|
674
667
|
order='F',
|
|
675
|
-
).astype(
|
|
668
|
+
).astype(numpy.int32)
|
|
676
669
|
|
|
677
670
|
select = self.integer_coordinates < min_int_coord
|
|
678
671
|
self.integer_coordinates[select] += 65536
|
|
@@ -695,7 +688,7 @@ class ImagejRoi:
|
|
|
695
688
|
).copy()
|
|
696
689
|
|
|
697
690
|
elif self.roitype not in (ROI_TYPE.RECT, ROI_TYPE.LINE, ROI_TYPE.OVAL):
|
|
698
|
-
|
|
691
|
+
logger().warning(f'cannot handle ImagejRoi type {self.roitype!r}')
|
|
699
692
|
|
|
700
693
|
return self
|
|
701
694
|
|
|
@@ -703,7 +696,7 @@ class ImagejRoi:
|
|
|
703
696
|
self,
|
|
704
697
|
filename: os.PathLike[Any] | str,
|
|
705
698
|
name: str | None = None,
|
|
706
|
-
mode:
|
|
699
|
+
mode: Literal['r', 'w', 'x', 'a'] | None = None,
|
|
707
700
|
) -> None:
|
|
708
701
|
"""Write ImagejRoi to ROI or ZIP file.
|
|
709
702
|
|
|
@@ -736,10 +729,10 @@ class ImagejRoi:
|
|
|
736
729
|
self.byteorder + 'hBxhhhhH',
|
|
737
730
|
self.version,
|
|
738
731
|
self.roitype.value,
|
|
739
|
-
numpy.array(self.top).astype(
|
|
740
|
-
numpy.array(self.left).astype(
|
|
741
|
-
numpy.array(self.bottom).astype(
|
|
742
|
-
numpy.array(self.right).astype(
|
|
732
|
+
numpy.array(self.top).astype(numpy.int16),
|
|
733
|
+
numpy.array(self.left).astype(numpy.int16),
|
|
734
|
+
numpy.array(self.bottom).astype(numpy.int16),
|
|
735
|
+
numpy.array(self.right).astype(numpy.int16),
|
|
743
736
|
self.n_coordinates if self.n_coordinates < 2**16 else 0,
|
|
744
737
|
)
|
|
745
738
|
)
|
|
@@ -801,7 +794,7 @@ class ImagejRoi:
|
|
|
801
794
|
)
|
|
802
795
|
extradata += self.text_name.encode(self.utf16)
|
|
803
796
|
extradata += self.text.encode(self.utf16)
|
|
804
|
-
extradata +=
|
|
797
|
+
extradata += struct.pack(self.byteorder + 'f', self.text_angle)
|
|
805
798
|
|
|
806
799
|
elif self.roitype in (
|
|
807
800
|
ROI_TYPE.POLYGON,
|
|
@@ -819,7 +812,9 @@ class ImagejRoi:
|
|
|
819
812
|
f'{self.integer_coordinates.shape} '
|
|
820
813
|
f'!= ({self.n_coordinates}, 2)'
|
|
821
814
|
)
|
|
822
|
-
coord = self.integer_coordinates.astype(
|
|
815
|
+
coord = self.integer_coordinates.astype(
|
|
816
|
+
self.byteorder + 'i2', copy=False
|
|
817
|
+
)
|
|
823
818
|
extradata = coord.tobytes(order='F')
|
|
824
819
|
if self.subpixel_coordinates is not None:
|
|
825
820
|
if self.subpixel_coordinates.shape != (self.n_coordinates, 2):
|
|
@@ -828,12 +823,16 @@ class ImagejRoi:
|
|
|
828
823
|
f'{self.subpixel_coordinates.shape} '
|
|
829
824
|
f'!= ({self.n_coordinates}, 2)'
|
|
830
825
|
)
|
|
831
|
-
coord = self.subpixel_coordinates.astype(
|
|
826
|
+
coord = self.subpixel_coordinates.astype(
|
|
827
|
+
self.byteorder + 'f4', copy=False
|
|
828
|
+
)
|
|
832
829
|
extradata += coord.tobytes(order='F')
|
|
833
830
|
|
|
834
831
|
elif self.composite and self.roitype == ROI_TYPE.RECT:
|
|
835
832
|
assert self.multi_coordinates is not None
|
|
836
|
-
coord = self.multi_coordinates.astype(
|
|
833
|
+
coord = self.multi_coordinates.astype(
|
|
834
|
+
self.byteorder + 'f4', copy=False
|
|
835
|
+
)
|
|
837
836
|
extradata += coord.tobytes()
|
|
838
837
|
|
|
839
838
|
header2_offset = 64 + len(extradata)
|
|
@@ -882,12 +881,12 @@ class ImagejRoi:
|
|
|
882
881
|
self.counter_positions, dtype=self.byteorder + 'u4'
|
|
883
882
|
)
|
|
884
883
|
counters = counters & 0xFF | indices << 8
|
|
885
|
-
counters = counters.astype(self.byteorder + 'u4')
|
|
884
|
+
counters = counters.astype(self.byteorder + 'u4', copy=False)
|
|
886
885
|
result.append(counters.tobytes())
|
|
887
886
|
|
|
888
887
|
return b''.join(result)
|
|
889
888
|
|
|
890
|
-
def plot(
|
|
889
|
+
def plot(
|
|
891
890
|
self,
|
|
892
891
|
ax: Any | None = None,
|
|
893
892
|
*,
|
|
@@ -897,7 +896,7 @@ class ImagejRoi:
|
|
|
897
896
|
invert_yaxis: bool | None = None,
|
|
898
897
|
**kwargs,
|
|
899
898
|
) -> None:
|
|
900
|
-
"""Plot coordinates using matplotlib."""
|
|
899
|
+
"""Plot a draft of coordinates using matplotlib."""
|
|
901
900
|
roitype = self.roitype
|
|
902
901
|
subtype = self.subtype
|
|
903
902
|
|
|
@@ -967,10 +966,25 @@ class ImagejRoi:
|
|
|
967
966
|
x, y = line[1]
|
|
968
967
|
ax.arrow(x, y, -dx, -dy, **kwargs)
|
|
969
968
|
elif roitype == ROI_TYPE.RECT and subtype == ROI_SUBTYPE.TEXT:
|
|
970
|
-
|
|
969
|
+
coords = self.coordinates(True)[0]
|
|
971
970
|
if 'fontsize' not in kwargs and self.text_size > 0:
|
|
972
971
|
kwargs['fontsize'] = self.text_size
|
|
973
|
-
|
|
972
|
+
text = ax.text(
|
|
973
|
+
*coords[1],
|
|
974
|
+
self.text,
|
|
975
|
+
va='center_baseline',
|
|
976
|
+
rotation=self.text_angle,
|
|
977
|
+
rotation_mode='anchor',
|
|
978
|
+
**kwargs,
|
|
979
|
+
)
|
|
980
|
+
scale_text(text, width=abs(coords[2, 0] - coords[0, 0]))
|
|
981
|
+
# ax.plot(
|
|
982
|
+
# coords[:, 0],
|
|
983
|
+
# coords[:, 1],
|
|
984
|
+
# linewidth=1,
|
|
985
|
+
# color=kwargs.get('color', 0.9),
|
|
986
|
+
# ls=':',
|
|
987
|
+
# )
|
|
974
988
|
else:
|
|
975
989
|
for coords in self.coordinates(multi=True):
|
|
976
990
|
ax.plot(coords[:, 0], coords[:, 1], **kwargs)
|
|
@@ -1013,7 +1027,7 @@ class ImagejRoi:
|
|
|
1013
1027
|
return coordslist
|
|
1014
1028
|
elif self.roitype == ROI_TYPE.LINE:
|
|
1015
1029
|
coords = numpy.array(
|
|
1016
|
-
[[self.x1, self.y1], [self.x2, self.y2]],
|
|
1030
|
+
[[self.x1, self.y1], [self.x2, self.y2]], numpy.float32
|
|
1017
1031
|
)
|
|
1018
1032
|
elif self.roitype == ROI_TYPE.OVAL:
|
|
1019
1033
|
coords = oval([[self.left, self.top], [self.right, self.bottom]])
|
|
@@ -1026,7 +1040,7 @@ class ImagejRoi:
|
|
|
1026
1040
|
[self.right, self.top],
|
|
1027
1041
|
[self.left, self.top],
|
|
1028
1042
|
],
|
|
1029
|
-
|
|
1043
|
+
numpy.float32,
|
|
1030
1044
|
)
|
|
1031
1045
|
else:
|
|
1032
1046
|
coords = numpy.empty((0, 2), dtype=self.byteorder + 'i4')
|
|
@@ -1055,7 +1069,9 @@ class ImagejRoi:
|
|
|
1055
1069
|
if op == 0:
|
|
1056
1070
|
# MOVETO
|
|
1057
1071
|
if n > 0:
|
|
1058
|
-
coordinates.append(
|
|
1072
|
+
coordinates.append(
|
|
1073
|
+
numpy.array(points, dtype=numpy.float32)
|
|
1074
|
+
)
|
|
1059
1075
|
points = []
|
|
1060
1076
|
points.append([path[n + 1], path[n + 2]])
|
|
1061
1077
|
m = len(points) - 1
|
|
@@ -1076,7 +1092,7 @@ class ImagejRoi:
|
|
|
1076
1092
|
else:
|
|
1077
1093
|
raise RuntimeError(f'invalid PathIterator command {op!r}')
|
|
1078
1094
|
|
|
1079
|
-
coordinates.append(numpy.array(points, dtype=
|
|
1095
|
+
coordinates.append(numpy.array(points, dtype=numpy.float32))
|
|
1080
1096
|
return coordinates
|
|
1081
1097
|
|
|
1082
1098
|
@staticmethod
|
|
@@ -1091,7 +1107,7 @@ class ImagejRoi:
|
|
|
1091
1107
|
return -5000
|
|
1092
1108
|
if -32768 <= value <= 0:
|
|
1093
1109
|
return int(value)
|
|
1094
|
-
raise ValueError('
|
|
1110
|
+
raise ValueError(f'{value=} out of range')
|
|
1095
1111
|
|
|
1096
1112
|
@property
|
|
1097
1113
|
def composite(self) -> bool:
|
|
@@ -1160,6 +1176,29 @@ class ImagejRoi:
|
|
|
1160
1176
|
return indent(*info, end='\n)')
|
|
1161
1177
|
|
|
1162
1178
|
|
|
1179
|
+
def scale_text(text: Any, width: float) -> None:
|
|
1180
|
+
"""Scale matplotlib text to width in data coordinates."""
|
|
1181
|
+
from matplotlib.patheffects import AbstractPathEffect
|
|
1182
|
+
from matplotlib.transforms import Bbox
|
|
1183
|
+
|
|
1184
|
+
class TextScaler(AbstractPathEffect):
|
|
1185
|
+
def __init__(self, text, width):
|
|
1186
|
+
self._text = text
|
|
1187
|
+
self._width = width
|
|
1188
|
+
|
|
1189
|
+
def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
|
|
1190
|
+
ax = self._text.axes
|
|
1191
|
+
renderer = ax.get_figure().canvas.get_renderer()
|
|
1192
|
+
bbox = text.get_window_extent(renderer=renderer)
|
|
1193
|
+
bbox = Bbox(ax.transData.inverted().transform(bbox))
|
|
1194
|
+
if self._width > 0 and bbox.width > 0:
|
|
1195
|
+
scale = self._width / bbox.width
|
|
1196
|
+
affine = affine.from_values(scale, 0, 0, scale, 0, 0) + affine
|
|
1197
|
+
renderer.draw_path(gc, tpath, affine, rgbFace)
|
|
1198
|
+
|
|
1199
|
+
text.set_path_effects([TextScaler(text, width)])
|
|
1200
|
+
|
|
1201
|
+
|
|
1163
1202
|
def oval(rect: ArrayLike, /, points: int = 33) -> NDArray[numpy.float32]:
|
|
1164
1203
|
"""Return coordinates of oval from rectangle corners."""
|
|
1165
1204
|
arr = numpy.array(rect, dtype=numpy.float32)
|
|
@@ -1205,11 +1244,9 @@ def enumstr(v: enum.Enum | None, /) -> str:
|
|
|
1205
1244
|
return s
|
|
1206
1245
|
|
|
1207
1246
|
|
|
1208
|
-
def
|
|
1209
|
-
"""
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
logging.getLogger(__name__).warning(msg, *args, **kwargs)
|
|
1247
|
+
def logger() -> logging.Logger:
|
|
1248
|
+
"""Return logging.getLogger('roifile')."""
|
|
1249
|
+
return logging.getLogger(__name__.replace('roifile.roifile', 'roifile'))
|
|
1213
1250
|
|
|
1214
1251
|
|
|
1215
1252
|
def test(verbose: bool = False) -> None:
|
|
@@ -1230,7 +1267,12 @@ def test(verbose: bool = False) -> None:
|
|
|
1230
1267
|
os.remove('_test.zip')
|
|
1231
1268
|
except OSError:
|
|
1232
1269
|
pass
|
|
1233
|
-
|
|
1270
|
+
|
|
1271
|
+
def roi_iter():
|
|
1272
|
+
# issue #9
|
|
1273
|
+
yield from rois
|
|
1274
|
+
|
|
1275
|
+
roiwrite('_test.zip', roi_iter())
|
|
1234
1276
|
assert roiread('_test.zip') == rois
|
|
1235
1277
|
|
|
1236
1278
|
# verify box_combined
|
|
@@ -1250,6 +1292,9 @@ def test(verbose: bool = False) -> None:
|
|
|
1250
1292
|
assert coords[-1][-1][-1] == 587.0
|
|
1251
1293
|
assert roi.multi_coordinates is not None
|
|
1252
1294
|
assert roi.multi_coordinates[0] == 0.0
|
|
1295
|
+
with open('tests/box_combined.roi', 'rb') as fh:
|
|
1296
|
+
expected = fh.read()
|
|
1297
|
+
assert roi.tobytes() == expected
|
|
1253
1298
|
str(roi)
|
|
1254
1299
|
|
|
1255
1300
|
roi = ImagejRoi.frompoints([[1, 2], [3, 4], [5, 6]])
|
|
@@ -1274,7 +1319,9 @@ def test(verbose: bool = False) -> None:
|
|
|
1274
1319
|
assert roi.bottom == 65535, roi.bottom
|
|
1275
1320
|
|
|
1276
1321
|
# issue #7
|
|
1277
|
-
roi = ImagejRoi.frompoints(
|
|
1322
|
+
roi = ImagejRoi.frompoints(
|
|
1323
|
+
numpy.load('tests/issue7.npy').astype(numpy.float32)
|
|
1324
|
+
)
|
|
1278
1325
|
assert roi == ImagejRoi.frombytes(roi.tobytes())
|
|
1279
1326
|
assert roi.left == 28357, roi.left
|
|
1280
1327
|
assert roi.top == 42200, roi.top # not -23336
|
|
@@ -1288,6 +1335,28 @@ def test(verbose: bool = False) -> None:
|
|
|
1288
1335
|
assert roi.subpixel_coordinates[0, 0] == 28357.0
|
|
1289
1336
|
assert roi.subpixel_coordinates[0, 1] == 42215.0
|
|
1290
1337
|
|
|
1338
|
+
# rotated text
|
|
1339
|
+
rois = roiread('tests/text_rotated.roi')
|
|
1340
|
+
assert isinstance(rois, ImagejRoi)
|
|
1341
|
+
roi = rois
|
|
1342
|
+
if verbose:
|
|
1343
|
+
print(roi)
|
|
1344
|
+
assert roi == ImagejRoi.frombytes(roi.tobytes())
|
|
1345
|
+
assert roi.name == 'Rotated'
|
|
1346
|
+
assert roi.roitype == ROI_TYPE.RECT
|
|
1347
|
+
assert roi.subtype == ROI_SUBTYPE.TEXT
|
|
1348
|
+
assert roi.version == 228
|
|
1349
|
+
assert (roi.top, roi.left, roi.bottom, roi.right) == (252, 333, 280, 438)
|
|
1350
|
+
assert roi.stroke_color == b'\xff\x00\x00\xff'
|
|
1351
|
+
assert roi.text_size == 20
|
|
1352
|
+
assert roi.text_justification == 1
|
|
1353
|
+
assert roi.text_name == 'SansSerif'
|
|
1354
|
+
assert roi.text == 'Enter text...\n'
|
|
1355
|
+
with open('tests/text_rotated.roi', 'rb') as fh:
|
|
1356
|
+
expected = fh.read()
|
|
1357
|
+
assert roi.tobytes() == expected
|
|
1358
|
+
str(roi)
|
|
1359
|
+
|
|
1291
1360
|
# read a ROI from a TIFF file
|
|
1292
1361
|
rois = roiread('tests/IJMetadata.tif')
|
|
1293
1362
|
assert isinstance(rois, list)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: roifile
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2024.3.20
|
|
4
4
|
Summary: Read and write ImageJ ROI format
|
|
5
5
|
Home-page: https://www.cgohlke.com
|
|
6
6
|
Author: Christoph Gohlke
|
|
@@ -38,7 +38,7 @@ interest, geometric shapes, paths, text, and whatnot for image overlays.
|
|
|
38
38
|
|
|
39
39
|
:Author: `Christoph Gohlke <https://www.cgohlke.com>`_
|
|
40
40
|
:License: BSD 3-Clause
|
|
41
|
-
:Version:
|
|
41
|
+
:Version: 2024.3.20
|
|
42
42
|
:DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
|
|
43
43
|
|
|
44
44
|
Quickstart
|
|
@@ -47,7 +47,7 @@ Quickstart
|
|
|
47
47
|
Install the roifile package and all dependencies from the
|
|
48
48
|
`Python Package Index <https://pypi.org/project/roifile/>`_::
|
|
49
49
|
|
|
50
|
-
python -m pip install -U roifile[all]
|
|
50
|
+
python -m pip install -U "roifile[all]"
|
|
51
51
|
|
|
52
52
|
View overlays stored in a ROI, ZIP, or TIFF file::
|
|
53
53
|
|
|
@@ -64,14 +64,25 @@ Requirements
|
|
|
64
64
|
This revision was tested with the following requirements and dependencies
|
|
65
65
|
(other versions may work):
|
|
66
66
|
|
|
67
|
-
- `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.
|
|
68
|
-
- `Numpy <https://pypi.org/project/numpy/>`_ 1.
|
|
69
|
-
- `Tifffile <https://pypi.org/project/tifffile/>`_
|
|
70
|
-
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.
|
|
67
|
+
- `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.8, 3.12.2
|
|
68
|
+
- `Numpy <https://pypi.org/project/numpy/>`_ 1.26.4
|
|
69
|
+
- `Tifffile <https://pypi.org/project/tifffile/>`_ 2024.2.12 (optional)
|
|
70
|
+
- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.8.3 (optional)
|
|
71
71
|
|
|
72
72
|
Revisions
|
|
73
73
|
---------
|
|
74
74
|
|
|
75
|
+
2024.3.20
|
|
76
|
+
|
|
77
|
+
- Fix writing generator of ROIs (#9).
|
|
78
|
+
|
|
79
|
+
2024.1.10
|
|
80
|
+
|
|
81
|
+
- Support text rotation.
|
|
82
|
+
- Improve text rendering.
|
|
83
|
+
- Avoid array copies.
|
|
84
|
+
- Limit size read from files.
|
|
85
|
+
|
|
75
86
|
2023.8.30
|
|
76
87
|
|
|
77
88
|
- Fix linting issues.
|
|
@@ -94,47 +105,9 @@ Revisions
|
|
|
94
105
|
|
|
95
106
|
2022.7.29
|
|
96
107
|
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
2022.3.18
|
|
100
|
-
|
|
101
|
-
- Fix creating ROIs from float coordinates exceeding int16 range (#7).
|
|
102
|
-
- Fix bottom-right bounds in ImagejRoi.frompoints.
|
|
103
|
-
|
|
104
|
-
2022.2.2
|
|
105
|
-
|
|
106
|
-
- Add type hints.
|
|
107
|
-
- Change ImagejRoi to dataclass.
|
|
108
|
-
- Drop support for Python 3.7 and numpy < 1.19 (NEP29).
|
|
109
|
-
|
|
110
|
-
2021.6.6
|
|
111
|
-
|
|
112
|
-
- Add enums for point types and sizes.
|
|
113
|
-
|
|
114
|
-
2020.11.28
|
|
115
|
-
|
|
116
|
-
- Support group attribute.
|
|
117
|
-
- Add roiread and roiwrite functions (#3).
|
|
118
|
-
- Use UUID as default name of ROI in ImagejRoi.frompoints (#2).
|
|
119
|
-
|
|
120
|
-
2020.8.13
|
|
121
|
-
|
|
122
|
-
- Support writing to ZIP file.
|
|
123
|
-
- Support os.PathLike file names.
|
|
124
|
-
|
|
125
|
-
2020.5.28
|
|
126
|
-
|
|
127
|
-
- Fix int32 to hex color conversion.
|
|
128
|
-
- Fix coordinates of closing path.
|
|
129
|
-
- Fix reading TIFF files with no overlays.
|
|
130
|
-
|
|
131
|
-
2020.5.1
|
|
132
|
-
|
|
133
|
-
- Split positions from counters.
|
|
134
|
-
|
|
135
|
-
2020.2.12
|
|
108
|
+
- …
|
|
136
109
|
|
|
137
|
-
|
|
110
|
+
Refer to the CHANGES file for older revisions.
|
|
138
111
|
|
|
139
112
|
Notes
|
|
140
113
|
-----
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
roifile/__init__.py,sha256=leII9J_JWVpi0O9sAnG8s4SpcdwdqEeisFEFsnmN56c,101
|
|
2
|
+
roifile/__main__.py,sha256=Mfn3wm-4cRRChIRonbEcZZT7eRX6RFlS31S8wS1JAVM,132
|
|
3
|
+
roifile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
roifile/roifile.py,sha256=E2-7K86Bk28V7OL75dNDjxfVF3CPaHfBVpt1mU3MXN4,47036
|
|
5
|
+
roifile-2024.3.20.dist-info/LICENSE,sha256=4bDcZTEFGF584Dw8M-07T5qfat7bsrfr8uuffsbFJYU,1559
|
|
6
|
+
roifile-2024.3.20.dist-info/METADATA,sha256=qqhZMep2Kjp_DDjC3HYC6tZhsylVxgXguyTGpf92XAc,4566
|
|
7
|
+
roifile-2024.3.20.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
8
|
+
roifile-2024.3.20.dist-info/entry_points.txt,sha256=xP8cwEUbAUeROLXNRanJnAIl13tagbjSSDGfVWf2vh0,49
|
|
9
|
+
roifile-2024.3.20.dist-info/top_level.txt,sha256=QlfLomxPxuYNU0TTR7MXoVBAEAXCj2WJyKvoCJxNwek,8
|
|
10
|
+
roifile-2024.3.20.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
roifile/__init__.py,sha256=leII9J_JWVpi0O9sAnG8s4SpcdwdqEeisFEFsnmN56c,101
|
|
2
|
-
roifile/__main__.py,sha256=Mfn3wm-4cRRChIRonbEcZZT7eRX6RFlS31S8wS1JAVM,132
|
|
3
|
-
roifile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
roifile/roifile.py,sha256=2pyfow3BNUF-S3tr6M5jwxTFQ4vfVDQ0v9rl7bleMRk,44238
|
|
5
|
-
roifile-2023.8.30.dist-info/LICENSE,sha256=TZQ7rylWoUOIsKyBDs85pF-6cpBRh_-A6ssn1nPBHos,1559
|
|
6
|
-
roifile-2023.8.30.dist-info/METADATA,sha256=LKItVCq1LTaZYUnKflTG839ctXzrH9dYVJo-BCBEncE,5119
|
|
7
|
-
roifile-2023.8.30.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
8
|
-
roifile-2023.8.30.dist-info/entry_points.txt,sha256=xP8cwEUbAUeROLXNRanJnAIl13tagbjSSDGfVWf2vh0,49
|
|
9
|
-
roifile-2023.8.30.dist-info/top_level.txt,sha256=QlfLomxPxuYNU0TTR7MXoVBAEAXCj2WJyKvoCJxNwek,8
|
|
10
|
-
roifile-2023.8.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|