pyisyntax 0.1.0__cp310-cp310-win_amd64.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.
- _pyisyntax.pyd +0 -0
- isyntax/__init__.py +5 -0
- isyntax/lowlevel/__init__.py +0 -0
- isyntax/lowlevel/io_management.py +115 -0
- isyntax/lowlevel/libisyntax.py +260 -0
- isyntax/wrapper.py +228 -0
- pyisyntax-0.1.0.dist-info/LICENSE +20 -0
- pyisyntax-0.1.0.dist-info/METADATA +81 -0
- pyisyntax-0.1.0.dist-info/RECORD +11 -0
- pyisyntax-0.1.0.dist-info/WHEEL +5 -0
- pyisyntax-0.1.0.dist-info/top_level.txt +2 -0
_pyisyntax.pyd
ADDED
|
Binary file
|
isyntax/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from io import BufferedIOBase, RawIOBase
|
|
4
|
+
from typing import Generic, NewType, TypeVar
|
|
5
|
+
|
|
6
|
+
from _pyisyntax import ffi, lib
|
|
7
|
+
|
|
8
|
+
VoidPtr = NewType("VoidPtr", object)
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SizedIO:
|
|
14
|
+
#: The IO object.
|
|
15
|
+
f: RawIOBase | BufferedIOBase
|
|
16
|
+
#: The complete length of the IO object's data (in bytes).
|
|
17
|
+
n_bytes: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ByHandleRegistry(Generic[T]):
|
|
21
|
+
class _EmptySlot:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
"""Creates a registry of objects keyed by integer handles.
|
|
26
|
+
|
|
27
|
+
Handles start at 1, autoincrement, and will automatically be recycled
|
|
28
|
+
once objects are removed from the registry.
|
|
29
|
+
"""
|
|
30
|
+
self._list = []
|
|
31
|
+
self._n_free = 0
|
|
32
|
+
self._empty_slot = self._EmptySlot()
|
|
33
|
+
|
|
34
|
+
def pop(self, handle: int) -> T:
|
|
35
|
+
element = self._list[handle - 1]
|
|
36
|
+
self._list[handle - 1] = self._empty_slot
|
|
37
|
+
self._n_free += 1
|
|
38
|
+
if len(self._list) == handle:
|
|
39
|
+
while len(self._list) > 0 and self._list[-1] is self._empty_slot:
|
|
40
|
+
self._list.pop()
|
|
41
|
+
self._n_free -= 1
|
|
42
|
+
return element
|
|
43
|
+
|
|
44
|
+
def add(self, element: T) -> int:
|
|
45
|
+
if self._n_free == 0:
|
|
46
|
+
self._list.append(element)
|
|
47
|
+
return len(self._list)
|
|
48
|
+
index = self._list.index(self._empty_slot)
|
|
49
|
+
self._list[index] = element
|
|
50
|
+
self._n_free -= 1
|
|
51
|
+
# Handles start at 1.
|
|
52
|
+
return index + 1
|
|
53
|
+
|
|
54
|
+
def __getitem__(self, handle: int) -> T:
|
|
55
|
+
return self._list[handle - 1]
|
|
56
|
+
|
|
57
|
+
def items(self) -> Iterator[tuple[int, T]]:
|
|
58
|
+
for i, element in enumerate(self._list):
|
|
59
|
+
if element is not self._empty_slot:
|
|
60
|
+
yield i + 1, element
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_io_registry = ByHandleRegistry[SizedIO]()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def init_python_io_hooks() -> None:
|
|
67
|
+
@ffi.def_extern()
|
|
68
|
+
def python_file_set_pos(handle: int, offset: int) -> bool:
|
|
69
|
+
_io_registry[handle].f.seek(offset)
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@ffi.def_extern()
|
|
74
|
+
def python_file_read_into(handle: int, dest: VoidPtr, bytes_to_read: int) -> int:
|
|
75
|
+
bytes_read = _io_registry[handle].f.readinto(ffi.buffer(dest, bytes_to_read))
|
|
76
|
+
if bytes_read is None:
|
|
77
|
+
raise RuntimeError
|
|
78
|
+
return max(bytes_read, 1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@ffi.def_extern()
|
|
82
|
+
def python_file_get_size(handle: int) -> int:
|
|
83
|
+
return _io_registry[handle].n_bytes
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@ffi.def_extern()
|
|
87
|
+
def python_file_close(handle: int) -> None:
|
|
88
|
+
_io_registry.pop(handle).f.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
lib.init_python_platform_utils(
|
|
92
|
+
lib.python_file_set_pos,
|
|
93
|
+
lib.python_file_read_into,
|
|
94
|
+
lib.python_file_get_size,
|
|
95
|
+
lib.python_file_close,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register_io(f: RawIOBase | BufferedIOBase, n_bytes: int) -> int:
|
|
100
|
+
"""Registers a Python IO object for use with the underlying C library.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
f: IO object to register.
|
|
104
|
+
n_bytes: Size of the IO object's data (in bytes).
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
RuntimeError: When the IO object is not readable.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
New handle assigned to the IO object.
|
|
111
|
+
"""
|
|
112
|
+
if not f.readable:
|
|
113
|
+
msg = "IO object must be readable"
|
|
114
|
+
raise RuntimeError(msg)
|
|
115
|
+
return _io_registry.add(SizedIO(f, n_bytes))
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, NewType
|
|
5
|
+
|
|
6
|
+
from _pyisyntax import ffi, lib
|
|
7
|
+
|
|
8
|
+
from isyntax.lowlevel.io_management import init_python_io_hooks, register_io
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from cffi import FFI
|
|
12
|
+
from typing_extensions import Buffer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Pointer = NewType("Pointer", "FFI.CData")
|
|
16
|
+
ISyntaxPtr = NewType("ISyntaxPtr", Pointer)
|
|
17
|
+
ISyntaxImagePtr = NewType("ISyntaxImagePtr", Pointer)
|
|
18
|
+
ISyntaxLevelPtr = NewType("ISyntaxLevelPtr", Pointer)
|
|
19
|
+
ISyntaxCachePtr = NewType("ISyntaxCachePtr", Pointer)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
LIBISYNTAX_OK = 0
|
|
23
|
+
# Generic error that the user should not expect to recover from.
|
|
24
|
+
LIBISYNTAX_FATAL = 1
|
|
25
|
+
# One of the arguments passed to a function is invalid.
|
|
26
|
+
LIBISYNTAX_INVALID_ARGUMENT = 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ISyntaxPixelFormat(IntEnum):
|
|
30
|
+
RGBA = lib.LIBISYNTAX_PIXEL_FORMAT_RGBA
|
|
31
|
+
BGRA = lib.LIBISYNTAX_PIXEL_FORMAT_BGRA
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LibISyntaxError(Exception):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LibISyntaxFatalError(LibISyntaxError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LibISyntaxInvalidArgumentError(LibISyntaxError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LibISyntaxUnknownError(LibISyntaxError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NullPointerError(Exception):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_error(status: int) -> None:
|
|
55
|
+
if status == LIBISYNTAX_OK:
|
|
56
|
+
return
|
|
57
|
+
if status == LIBISYNTAX_FATAL:
|
|
58
|
+
raise LibISyntaxFatalError
|
|
59
|
+
if status == LIBISYNTAX_INVALID_ARGUMENT:
|
|
60
|
+
raise LibISyntaxInvalidArgumentError
|
|
61
|
+
raise LibISyntaxUnknownError
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def free(ptr: Pointer) -> None:
|
|
65
|
+
lib.free(ptr)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _do_init() -> None:
|
|
69
|
+
check_error(lib.libisyntax_init())
|
|
70
|
+
init_python_io_hooks()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def init() -> None:
|
|
74
|
+
ffi.init_once(_do_init, "libisyntax_init")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def open_from_registered_handle(handle: int, *, is_init_allocators: bool = False) -> ISyntaxPtr:
|
|
78
|
+
init()
|
|
79
|
+
|
|
80
|
+
isyntax = ffi.new("isyntax_t**")
|
|
81
|
+
check_error(lib.libisyntax_open(
|
|
82
|
+
ffi.new("char[]", str(handle).encode("utf-8")),
|
|
83
|
+
is_init_allocators,
|
|
84
|
+
isyntax,
|
|
85
|
+
))
|
|
86
|
+
return isyntax[0]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def open_from_filename(filename: str | Path, *, is_init_allocators: bool = False) -> ISyntaxPtr:
|
|
90
|
+
filename = Path(filename)
|
|
91
|
+
f = filename.open("rb")
|
|
92
|
+
handle = register_io(f, os.fstat(f.fileno()).st_size)
|
|
93
|
+
return open_from_registered_handle(handle, is_init_allocators=is_init_allocators)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def close(isyntax: ISyntaxPtr) -> None:
|
|
97
|
+
# If a null pointer gets through the program will segfault.
|
|
98
|
+
if isyntax == ffi.NULL:
|
|
99
|
+
raise NullPointerError
|
|
100
|
+
lib.libisyntax_close(isyntax)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_tile_width(isyntax: ISyntaxPtr) -> int:
|
|
104
|
+
return lib.libisyntax_get_tile_width(isyntax)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_tile_height(isyntax: ISyntaxPtr) -> int:
|
|
108
|
+
return lib.libisyntax_get_tile_height(isyntax)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_wsi_image_index(isyntax: ISyntaxPtr) -> int:
|
|
112
|
+
return lib.libisyntax_get_wsi_image_index(isyntax)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_image(isyntax: ISyntaxPtr, wsi_image_index: int) -> ISyntaxImagePtr:
|
|
116
|
+
return lib.libisyntax_get_image(isyntax, wsi_image_index)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def image_get_level_count(wsi_image: ISyntaxImagePtr) -> int:
|
|
120
|
+
return lib.libisyntax_image_get_level_count(wsi_image)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def image_get_level(wsi_image: ISyntaxImagePtr, index: int) -> ISyntaxLevelPtr:
|
|
124
|
+
return lib.libisyntax_image_get_level(wsi_image, index)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def level_get_scale(level: ISyntaxLevelPtr) -> int:
|
|
128
|
+
return lib.libisyntax_level_get_scale(level)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def level_get_width_in_tiles(level: ISyntaxLevelPtr) -> int:
|
|
132
|
+
return lib.libisyntax_level_get_width_in_tiles(level)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def level_get_height_in_tiles(level: ISyntaxLevelPtr) -> int:
|
|
136
|
+
return lib.libisyntax_level_get_height_in_tiles(level)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def level_get_width(level: ISyntaxLevelPtr) -> int:
|
|
140
|
+
return lib.libisyntax_level_get_width(level)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def level_get_height(level: ISyntaxLevelPtr) -> int:
|
|
144
|
+
return lib.libisyntax_level_get_height(level)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def level_get_mpp_x(level: ISyntaxLevelPtr) -> float:
|
|
148
|
+
return lib.libisyntax_level_get_mpp_x(level)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def level_get_mpp_y(level: ISyntaxLevelPtr) -> float:
|
|
152
|
+
return lib.libisyntax_level_get_mpp_y(level)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cache_create(debug_name: str | None, cache_size: int) -> ISyntaxCachePtr:
|
|
156
|
+
isyntax_cache = ffi.new("isyntax_cache_t**")
|
|
157
|
+
if debug_name is None:
|
|
158
|
+
debug_name_or_null = ffi.NULL
|
|
159
|
+
else:
|
|
160
|
+
debug_name_or_null = ffi.new("char[]", debug_name.encode("utf-8"))
|
|
161
|
+
check_error(lib.libisyntax_cache_create(
|
|
162
|
+
debug_name_or_null,
|
|
163
|
+
cache_size,
|
|
164
|
+
isyntax_cache,
|
|
165
|
+
))
|
|
166
|
+
return isyntax_cache[0]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cache_inject(isyntax_cache: ISyntaxCachePtr, isyntax: ISyntaxPtr) -> None:
|
|
170
|
+
check_error(lib.libisyntax_cache_inject(isyntax_cache, isyntax))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cache_destroy(isyntax_cache: ISyntaxCachePtr) -> None:
|
|
174
|
+
if isyntax_cache == ffi.NULL:
|
|
175
|
+
raise NullPointerError
|
|
176
|
+
lib.libisyntax_cache_destroy(isyntax_cache)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def tile_read(
|
|
180
|
+
isyntax: ISyntaxPtr,
|
|
181
|
+
isyntax_cache: ISyntaxCachePtr,
|
|
182
|
+
level: int,
|
|
183
|
+
tile_x: int,
|
|
184
|
+
tile_y: int,
|
|
185
|
+
pixels_buffer: "Buffer | FFI.buffer",
|
|
186
|
+
pixel_format: ISyntaxPixelFormat,
|
|
187
|
+
) -> None:
|
|
188
|
+
check_error(lib.libisyntax_tile_read(
|
|
189
|
+
isyntax,
|
|
190
|
+
isyntax_cache,
|
|
191
|
+
level,
|
|
192
|
+
tile_x,
|
|
193
|
+
tile_y,
|
|
194
|
+
ffi.from_buffer("uint32_t[]", pixels_buffer, require_writable=True),
|
|
195
|
+
pixel_format,
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def read_region(
|
|
200
|
+
isyntax: ISyntaxPtr,
|
|
201
|
+
isyntax_cache: ISyntaxCachePtr,
|
|
202
|
+
level: int,
|
|
203
|
+
x: int,
|
|
204
|
+
y: int,
|
|
205
|
+
width: int,
|
|
206
|
+
height: int,
|
|
207
|
+
pixels_buffer: "Buffer | FFI.buffer",
|
|
208
|
+
pixel_format: ISyntaxPixelFormat,
|
|
209
|
+
) -> None:
|
|
210
|
+
check_error(lib.libisyntax_read_region(
|
|
211
|
+
isyntax,
|
|
212
|
+
isyntax_cache,
|
|
213
|
+
level,
|
|
214
|
+
x,
|
|
215
|
+
y,
|
|
216
|
+
width,
|
|
217
|
+
height,
|
|
218
|
+
ffi.from_buffer("uint32_t[]", pixels_buffer, require_writable=True),
|
|
219
|
+
pixel_format,
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def read_label_image_jpeg(isyntax: ISyntaxPtr) -> memoryview:
|
|
224
|
+
jpeg_buffer_ptr = ffi.new("uint8_t**")
|
|
225
|
+
jpeg_size_ptr = ffi.new("uint32_t*")
|
|
226
|
+
check_error(lib.libisyntax_read_label_image_jpeg(
|
|
227
|
+
isyntax,
|
|
228
|
+
jpeg_buffer_ptr,
|
|
229
|
+
jpeg_size_ptr,
|
|
230
|
+
))
|
|
231
|
+
jpeg_buffer = ffi.gc(jpeg_buffer_ptr[0], free)
|
|
232
|
+
jpeg_size = jpeg_size_ptr[0]
|
|
233
|
+
return memoryview(ffi.buffer(jpeg_buffer, jpeg_size))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def read_macro_image_jpeg(isyntax: ISyntaxPtr) -> memoryview:
|
|
237
|
+
jpeg_buffer_ptr = ffi.new("uint8_t**")
|
|
238
|
+
jpeg_size_ptr = ffi.new("uint32_t*")
|
|
239
|
+
check_error(lib.libisyntax_read_macro_image_jpeg(
|
|
240
|
+
isyntax,
|
|
241
|
+
jpeg_buffer_ptr,
|
|
242
|
+
jpeg_size_ptr,
|
|
243
|
+
))
|
|
244
|
+
jpeg_buffer = ffi.gc(jpeg_buffer_ptr[0], free)
|
|
245
|
+
jpeg_size = jpeg_size_ptr[0]
|
|
246
|
+
return memoryview(ffi.buffer(jpeg_buffer, jpeg_size))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def read_icc_profile(isyntax: ISyntaxPtr, image: ISyntaxImagePtr) -> memoryview:
|
|
250
|
+
profile_buffer_ptr = ffi.new("uint8_t**")
|
|
251
|
+
profile_size_ptr = ffi.new("uint32_t*")
|
|
252
|
+
check_error(lib.libisyntax_read_icc_profile(
|
|
253
|
+
isyntax,
|
|
254
|
+
image,
|
|
255
|
+
profile_buffer_ptr,
|
|
256
|
+
profile_size_ptr,
|
|
257
|
+
))
|
|
258
|
+
profile_buffer = ffi.gc(profile_buffer_ptr[0], free)
|
|
259
|
+
profile_size = profile_size_ptr[0]
|
|
260
|
+
return memoryview(ffi.buffer(profile_buffer, profile_size))
|
isyntax/wrapper.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from io import BufferedIOBase, RawIOBase
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from isyntax.lowlevel import libisyntax
|
|
10
|
+
from isyntax.lowlevel.io_management import register_io
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ISyntaxCache:
|
|
14
|
+
def __init__(self, debug_name: str | None = None, cache_size: int = 2000) -> None:
|
|
15
|
+
self.ptr = libisyntax.cache_create(debug_name, cache_size)
|
|
16
|
+
self.destroyed = False
|
|
17
|
+
|
|
18
|
+
def inject(self, isyntax: "ISyntax") -> None:
|
|
19
|
+
libisyntax.cache_inject(self.ptr, isyntax.ptr)
|
|
20
|
+
|
|
21
|
+
def destroy(self) -> None:
|
|
22
|
+
libisyntax.cache_destroy(self.ptr)
|
|
23
|
+
self.destroyed = True
|
|
24
|
+
|
|
25
|
+
def __del__(self) -> None:
|
|
26
|
+
if not self.destroyed:
|
|
27
|
+
self.destroy()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ISyntaxLevel:
|
|
31
|
+
def __init__(self, ptr: libisyntax.ISyntaxLevelPtr) -> None:
|
|
32
|
+
self.ptr = ptr
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def scale(self) -> int:
|
|
36
|
+
return libisyntax.level_get_scale(self.ptr)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def width_in_tiles(self) -> int:
|
|
40
|
+
return libisyntax.level_get_width_in_tiles(self.ptr)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def height_in_tiles(self) -> int:
|
|
44
|
+
return libisyntax.level_get_height_in_tiles(self.ptr)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def width(self) -> int:
|
|
48
|
+
return libisyntax.level_get_width(self.ptr)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def height(self) -> int:
|
|
52
|
+
return libisyntax.level_get_height(self.ptr)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def mpp_x(self) -> float:
|
|
56
|
+
return libisyntax.level_get_mpp_x(self.ptr)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def mpp_y(self) -> float:
|
|
60
|
+
return libisyntax.level_get_mpp_y(self.ptr)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ISyntaxImage:
|
|
64
|
+
def __init__(self, ptr: libisyntax.ISyntaxImagePtr) -> None:
|
|
65
|
+
self.ptr = ptr
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def level_count(self) -> int:
|
|
69
|
+
return libisyntax.image_get_level_count(self.ptr)
|
|
70
|
+
|
|
71
|
+
def get_level(self, level_index: int) -> ISyntaxLevel:
|
|
72
|
+
return ISyntaxLevel(libisyntax.image_get_level(self.ptr, level_index))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def levels(self) -> Iterator[ISyntaxLevel]:
|
|
76
|
+
for i in range(self.level_count):
|
|
77
|
+
yield self.get_level(i)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ISyntax:
|
|
81
|
+
def __init__(self, f: RawIOBase | BufferedIOBase, n_bytes: int) -> None:
|
|
82
|
+
self.io_handle = register_io(f, n_bytes)
|
|
83
|
+
self.ptr = libisyntax.open_from_registered_handle(self.io_handle, is_init_allocators=False)
|
|
84
|
+
self.closed = False
|
|
85
|
+
self._cache = None
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def open(cls: type["ISyntax"], filename: str | Path) -> "ISyntax":
|
|
89
|
+
filename = Path(filename)
|
|
90
|
+
f = filename.open("rb")
|
|
91
|
+
n_bytes = os.fstat(f.fileno()).st_size
|
|
92
|
+
return cls(f, n_bytes)
|
|
93
|
+
|
|
94
|
+
def close(self) -> None:
|
|
95
|
+
if self.closed:
|
|
96
|
+
return
|
|
97
|
+
libisyntax.close(self.ptr)
|
|
98
|
+
self.closed = True
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def tile_width(self) -> int:
|
|
102
|
+
return libisyntax.get_tile_width(self.ptr)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def tile_height(self) -> int:
|
|
106
|
+
return libisyntax.get_tile_height(self.ptr)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def wsi_index(self) -> int:
|
|
110
|
+
return libisyntax.get_wsi_image_index(self.ptr)
|
|
111
|
+
|
|
112
|
+
def get_image(self, image_index: int) -> ISyntaxImage:
|
|
113
|
+
return ISyntaxImage(libisyntax.get_image(self.ptr, image_index))
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def wsi(self) -> ISyntaxImage:
|
|
117
|
+
return self.get_image(self.wsi_index)
|
|
118
|
+
|
|
119
|
+
def get_cache(self) -> ISyntaxCache:
|
|
120
|
+
if self._cache is None:
|
|
121
|
+
self._cache = ISyntaxCache(cache_size=2000)
|
|
122
|
+
self._cache.inject(self)
|
|
123
|
+
return self._cache
|
|
124
|
+
|
|
125
|
+
def read_tile(self, tile_x: int, tile_y: int, level: int = 0) -> np.ndarray:
|
|
126
|
+
"""Reads RGBA pixel data from the specified tile.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
tile_x: Tile column.
|
|
130
|
+
tile_y: Tile row.
|
|
131
|
+
level: Level number. Defaults to 0.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Region RGBA pixel data in a [tile_height, tile_width, 4] array.
|
|
135
|
+
"""
|
|
136
|
+
cache = self.get_cache()
|
|
137
|
+
buf = np.empty((self.tile_height, self.tile_width, 4), dtype=np.uint8)
|
|
138
|
+
libisyntax.tile_read(self.ptr, cache.ptr, level, tile_x, tile_y,
|
|
139
|
+
buf.data, libisyntax.ISyntaxPixelFormat.RGBA)
|
|
140
|
+
return buf
|
|
141
|
+
|
|
142
|
+
def read_region(self, x: int, y: int, width: int, height: int, level: int = 0) -> np.ndarray:
|
|
143
|
+
"""Reads RGBA pixel data from the specified region.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
x: Left edge position of the region in the target level reference frame.
|
|
147
|
+
y: Top edge position of the region in the target level reference frame.
|
|
148
|
+
width: Width of the region.
|
|
149
|
+
height: Height of the region.
|
|
150
|
+
level: Level number. Defaults to 0.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Region RGBA pixel data in a [height, width, 4] array.
|
|
154
|
+
"""
|
|
155
|
+
cache = self.get_cache()
|
|
156
|
+
buf = np.empty((height, width, 4), dtype=np.uint8)
|
|
157
|
+
libisyntax.read_region(self.ptr, cache.ptr, level, x, y, width, height,
|
|
158
|
+
buf.data, libisyntax.ISyntaxPixelFormat.RGBA)
|
|
159
|
+
return buf
|
|
160
|
+
|
|
161
|
+
def read_label_image_jpeg(self) -> memoryview:
|
|
162
|
+
"""Reads the associated label image as a JPEG-compressed image.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Compressed JPEG image data.
|
|
166
|
+
"""
|
|
167
|
+
return libisyntax.read_label_image_jpeg(self.ptr)
|
|
168
|
+
|
|
169
|
+
def read_macro_image_jpeg(self) -> memoryview:
|
|
170
|
+
"""Reads the associated macro image as a JPEG-compressed image.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Compressed JPEG image data.
|
|
174
|
+
"""
|
|
175
|
+
return libisyntax.read_macro_image_jpeg(self.ptr)
|
|
176
|
+
|
|
177
|
+
def read_icc_profile(self, image_index: int) -> memoryview:
|
|
178
|
+
"""Reads the ICC color profile for an image.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The ICC color profile.
|
|
182
|
+
"""
|
|
183
|
+
return libisyntax.read_icc_profile(self.ptr, self.get_image(image_index).ptr)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def level_count(self) -> int:
|
|
187
|
+
return self.wsi.level_count
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def dimensions(self) -> tuple[int, int]:
|
|
191
|
+
level0 = self.wsi.get_level(0)
|
|
192
|
+
return (level0.width, level0.height)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def level_dimensions(self) -> list[tuple[int, int]]:
|
|
196
|
+
return [(level.width, level.height) for level in self.wsi.levels]
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def level_downsamples(self) -> list[int]:
|
|
200
|
+
return [1 << level.scale for level in self.wsi.levels]
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def level_tiles(self) -> list[tuple[int, int]]:
|
|
204
|
+
return [(level.width_in_tiles, level.height_in_tiles) for level in self.wsi.levels]
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def mpp_x(self) -> float:
|
|
208
|
+
level0 = self.wsi.get_level(0)
|
|
209
|
+
return level0.mpp_x
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def mpp_y(self) -> float:
|
|
213
|
+
level0 = self.wsi.get_level(0)
|
|
214
|
+
return level0.mpp_y
|
|
215
|
+
|
|
216
|
+
def __enter__(self) -> "ISyntax":
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def __exit__(
|
|
220
|
+
self,
|
|
221
|
+
exc_type: type[BaseException] | None,
|
|
222
|
+
exc: BaseException | None,
|
|
223
|
+
traceback: TracebackType | None,
|
|
224
|
+
) -> None:
|
|
225
|
+
self.close()
|
|
226
|
+
|
|
227
|
+
def __del__(self) -> None:
|
|
228
|
+
self.close()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2024 Aiden Nibali
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyisyntax
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for libisyntax
|
|
5
|
+
Author-email: Aiden Nibali <dismaldenizen@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: cffi >=1.12
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: conda-lock ; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-mock ; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff ==0.2.1 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: twine ; extra == 'dev'
|
|
18
|
+
|
|
19
|
+
# pyisyntax
|
|
20
|
+
|
|
21
|
+
A Python library for working with pathology images in the iSyntax file format,
|
|
22
|
+
powered by [libisyntax](https://github.com/amspath/libisyntax).
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Read and display a region of the WSI via
|
|
27
|
+
[Pillow](https://pillow.readthedocs.io/).
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from isyntax import ISyntax
|
|
31
|
+
import PIL.Image
|
|
32
|
+
|
|
33
|
+
with ISyntax.open("my_file.isyntax") as isyntax:
|
|
34
|
+
# Read pixels from the specified region into a numpy array
|
|
35
|
+
pixels = isyntax.read_region(500, 500, 400, 200, level=4)
|
|
36
|
+
# Convert numpy array into a PIL image
|
|
37
|
+
pil_image = PIL.Image.fromarray(pixels)
|
|
38
|
+
# Show the image
|
|
39
|
+
pil_image.show()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Extract and save the associated macro image.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from isyntax import ISyntax
|
|
46
|
+
|
|
47
|
+
with ISyntax.open("my_file.isyntax") as isyntax:
|
|
48
|
+
# The macro image will be returned as compressed JPEG data.
|
|
49
|
+
jpeg_data = isyntax.read_macro_image_jpeg()
|
|
50
|
+
# This JPEG data can be written directly to a file.
|
|
51
|
+
with open("macro_image.jpg", "wb") as f:
|
|
52
|
+
f.write(jpeg_data)
|
|
53
|
+
# Alternatively, you could decompress the data using Pillow:
|
|
54
|
+
# pil_image = PIL.Image.open(io.BytesIO(jpeg_data), formats=["JPEG"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
### Dependency management
|
|
60
|
+
|
|
61
|
+
To set up a development environment from the lock file:
|
|
62
|
+
|
|
63
|
+
1. Ensure that you have
|
|
64
|
+
[micromamba](https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html)
|
|
65
|
+
installed.
|
|
66
|
+
2. Create the environment:
|
|
67
|
+
```console
|
|
68
|
+
$ micromamba create -n pyisyntax -f conda-lock.yml --category main --category dev
|
|
69
|
+
```
|
|
70
|
+
3. Activate the environment:
|
|
71
|
+
```console
|
|
72
|
+
$ micromamba activate pyisyntax
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
To modify pyisyntax project dependencies:
|
|
76
|
+
|
|
77
|
+
1. Edit pyproject.toml.
|
|
78
|
+
2. Update the lock file using conda-lock:
|
|
79
|
+
```console
|
|
80
|
+
$ conda-lock lock -f pyproject.toml -p linux-64 --micromamba
|
|
81
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
_pyisyntax.pyd,sha256=Sn67vwQVGNa3JTMFRcbW2A7DHQ6TVEoc-EoZBr-zwVY,164864
|
|
2
|
+
isyntax/__init__.py,sha256=m4BuPQ8bPeEoltBIpWdyHiZjXh4rTrnXyQxa71YtQNA,66
|
|
3
|
+
isyntax/wrapper.py,sha256=b5M21yIb9D7D6bR16y_lcqAHaCFeXHWqUIkv_ntAb8I,6887
|
|
4
|
+
isyntax/lowlevel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
isyntax/lowlevel/io_management.py,sha256=gz0M0xy70kPTJeAdH2ETUB8vYBVq9WJCmhGIH2stkOQ,3227
|
|
6
|
+
isyntax/lowlevel/libisyntax.py,sha256=zR75XPpPsGrOmJMEMktWCKfUmukvzj34MRff6n3qA8Q,6947
|
|
7
|
+
pyisyntax-0.1.0.dist-info/LICENSE,sha256=eKo8d-7o2VI1N6WpXbAPFDjlUt-zPcTh15qDBbmdvnc,1056
|
|
8
|
+
pyisyntax-0.1.0.dist-info/METADATA,sha256=9sO6DZIg16JTmxZReQMtpCUWxqdU-LkQ4AxEL0KGBOQ,2371
|
|
9
|
+
pyisyntax-0.1.0.dist-info/WHEEL,sha256=5JPYeYl5ZdvdSkrGS4u21mmpPzpFx42qrXOSIgWf4pg,102
|
|
10
|
+
pyisyntax-0.1.0.dist-info/top_level.txt,sha256=AN4WEBKUcjQivO8_X2eugc6S8ViTNFJO-xKLOMbTbAw,19
|
|
11
|
+
pyisyntax-0.1.0.dist-info/RECORD,,
|