oct-to-tiff 0.4.0__py3-none-any.whl → 0.6.0__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.
oct_to_tiff/cli.py CHANGED
@@ -1,25 +1,30 @@
1
1
  import argparse
2
2
  import logging
3
- import xml.etree.ElementTree as ET
3
+ from importlib.metadata import version
4
4
  from pathlib import Path
5
+ from typing import Any
5
6
 
7
+ import defusedxml.ElementTree as DET
6
8
  import numpy as np
9
+ import numpy.typing as npt
7
10
  import tifffile
11
+ from roifile import ROI_TYPE, ImagejRoi, roiwrite
8
12
 
9
- logging.basicConfig(
10
- format="%(asctime)s %(name)s:%(funcName)s %(levelname)s - %(message)s"
11
- )
12
13
  logger = logging.getLogger(__name__)
13
14
 
14
15
 
15
16
  def reshape_volume(
16
- volume, frames_per_data_group, total_data_groups, oct_window_height, xy_scan_length
17
- ):
17
+ volume: npt.NDArray[Any],
18
+ frames_per_data_group: int,
19
+ total_data_groups: int,
20
+ oct_window_height: int,
21
+ xy_scan_length: int,
22
+ ) -> npt.NDArray[Any]:
18
23
  """Reshape a 1-dimensional array to a 3-dimensional array.
19
24
 
20
25
  Parameters
21
26
  ----------
22
- volume : ndarray
27
+ volume : npt.NDArray[Any]
23
28
  A 1-dimensional array.
24
29
  frames_per_data_group : int
25
30
  The number of frames per data group.
@@ -32,7 +37,7 @@ def reshape_volume(
32
37
 
33
38
  Returns
34
39
  -------
35
- volume : ndarray
40
+ volume : npt.NDArray[Any]
36
41
  A 3-dimensional array.
37
42
 
38
43
  """
@@ -47,32 +52,20 @@ def reshape_volume(
47
52
  return volume
48
53
 
49
54
 
50
- def rotate_volume(volume):
51
- """Rotate a 3-dimensional array 90 degrees left (anti-clockwise) about the z-axis.
52
-
53
- Parameters
54
- ----------
55
- volume : ndarray
56
- A 3-dimensional array.
57
-
58
- Returns
59
- -------
60
- volume : ndarray
61
- A rotated version of the input volume.
62
-
63
- """
64
- volume = np.rot90(volume, k=1, axes=(1, 2))
65
- return volume
66
-
67
-
68
- def write_volume(output_path, volume, pixel_size_x, pixel_size_y, pixel_size_z):
55
+ def write_volume(
56
+ output_path: Path,
57
+ volume: npt.NDArray[Any],
58
+ pixel_size_x: float,
59
+ pixel_size_y: float,
60
+ pixel_size_z: float,
61
+ ) -> None:
69
62
  """Write a 3-dimensional array to the output path as an OME-TIFF file, including voxel size in the metadata.
70
63
 
71
64
  Parameters
72
65
  ----------
73
66
  output_path : Path
74
67
  The specified output path.
75
- volume : ndarray
68
+ volume : npt.NDArray[Any]
76
69
  A 3-dimensional array.
77
70
  pixel_size_x : float
78
71
  The pixel (voxel) width in mm.
@@ -98,33 +91,61 @@ def write_volume(output_path, volume, pixel_size_x, pixel_size_y, pixel_size_z):
98
91
  )
99
92
 
100
93
 
101
- def extract_boundaries(input_path):
94
+ def boundaries_to_arrays(input_path: str | Path) -> list[npt.NDArray[np.int_]]:
102
95
  """Extract segmentation lines.
103
96
 
104
97
  Parameters
105
98
  ----------
106
- input_path : Path
99
+ input_path : str | Path
107
100
  The specified input path.
108
101
 
102
+ Returns
103
+ -------
104
+ arrays : list[npt.NDArray[np.int_]]
105
+ A list of 2-dimensional arrays.
109
106
  """
110
- tree = ET.parse(input_path)
107
+ input_path = Path(input_path)
108
+ tree = DET.parse(input_path)
111
109
  root = tree.getroot()
112
110
 
113
- array_size = int(root.findtext("./Curve_Set/Image/Curve/ARRAY"))
111
+ array_size = int(root.findtext("./Curve_Set/Image/Curve/ARRAY", 0))
114
112
  data_points = [
115
- int(point.text) for point in root.findall("./Curve_Set/Image/Curve/D")
113
+ int(point.text) if point.text else 0
114
+ for point in root.findall("./Curve_Set/Image/Curve/D")
116
115
  ]
117
- scan_length = np.arange(len(data_points))
118
- num_files = len(data_points) // array_size
119
- for i in range(num_files):
116
+ num_arrays = len(data_points) // array_size
117
+
118
+ arrays = []
119
+ for i in range(num_arrays):
120
120
  start = i * array_size
121
121
  end = start + array_size
122
- table = np.column_stack([scan_length[start:end], data_points[start:end]])
123
- table_path = f"{input_path.parent}/{input_path.stem}_{i+1}.txt"
124
- np.savetxt(table_path, table, delimiter="\t", fmt="%d")
122
+ array = np.column_stack([np.arange(array_size), data_points[start:end]])
123
+ arrays.append(array)
124
+
125
+ return arrays
126
+
127
+
128
+ def arrays_to_rois(arrays: list[npt.NDArray[np.int_]], output_path: Path) -> None:
129
+ """
130
+ Convert a list of 2-dimensional arrays to ImageJ ROIs (ZIP file).
125
131
 
132
+ Parameters
133
+ ----------
134
+ arrays : list[npt.NDArray[np.int_]]
135
+ A list of 2-dimensional arrays.
136
+ output_path : Path
137
+ The specified output path.
138
+ """
139
+ rois = []
140
+ for array in arrays:
141
+ roi = ImagejRoi.frompoints(array)
142
+ roi.roitype = ROI_TYPE(4) # FREELINE
143
+ rois.append(roi)
144
+
145
+ roiwrite(output_path, rois, mode="w")
126
146
 
127
- def main():
147
+
148
+ def main() -> None:
128
149
  parser = argparse.ArgumentParser(
129
150
  description="Convert optical coherence tomography angiography (OCTA) data."
130
151
  )
@@ -162,9 +183,25 @@ def main():
162
183
  action="store_true",
163
184
  help="extract segmentation lines",
164
185
  )
165
- parser.add_argument("--version", action="version", version="%(prog)s 0.4.0")
186
+ parser.add_argument(
187
+ "--log-level",
188
+ default="WARNING",
189
+ metavar="LEVEL",
190
+ help="sets the logging level (default: %(default)s)",
191
+ )
192
+ parser.add_argument(
193
+ "--version", action="version", version="%(prog)s " + version("oct_to_tiff")
194
+ )
166
195
  args = parser.parse_args()
167
196
 
197
+ numeric_level = getattr(logging, args.log_level.upper(), None)
198
+ if not isinstance(numeric_level, int):
199
+ raise ValueError(f"Invalid log level: {args.log_level}")
200
+ logging.basicConfig(
201
+ level=numeric_level,
202
+ format="%(asctime)s %(name)s:%(funcName)s %(levelname)s - %(message)s",
203
+ )
204
+
168
205
  input_path = args.input
169
206
  if args.output:
170
207
  dir_name = args.output
@@ -172,18 +209,22 @@ def main():
172
209
  else:
173
210
  dir_name = input_path.parent
174
211
  file_name = input_path.stem
175
- file_extension = ".ome.tif"
212
+ if args.boundaries:
213
+ file_extension = "_rois.zip"
214
+ else:
215
+ file_extension = ".ome.tif"
176
216
  output_path = dir_name / (file_name + file_extension)
177
217
 
178
218
  if Path.is_file(output_path):
179
219
  if args.overwrite:
180
- pass
220
+ logger.warning(f"Overwriting {output_path}")
181
221
  else:
182
222
  logger.error(f"{output_path} already exists.")
183
223
  return
184
224
 
185
225
  if args.boundaries:
186
- extract_boundaries(input_path)
226
+ arrays = boundaries_to_arrays(input_path)
227
+ arrays_to_rois(arrays, output_path)
187
228
  return
188
229
 
189
230
  with open(input_path, "rb") as f:
@@ -227,6 +268,32 @@ def main():
227
268
  pixel_size_x = 0.007797
228
269
  pixel_size_y = 0.003071
229
270
  pixel_size_z = 0.040000
271
+
272
+ volume = reshape_volume(
273
+ volume,
274
+ frames_per_data_group,
275
+ total_data_groups,
276
+ oct_window_height,
277
+ xy_scan_length,
278
+ )
279
+ volume = np.rot90(volume, k=1, axes=(1, 2))
280
+ volume_main = volume[:101]
281
+ volume_align = volume[101:frames_per_data_group]
282
+ align_path = dir_name / (file_name + "_Align.ome.tif")
283
+ pixel_size_x_align = 0.003899
284
+ pixel_size_y_align = 0.003071
285
+ pixel_size_z_align = 0.040000
286
+ write_volume(
287
+ output_path, volume_main, pixel_size_x, pixel_size_y, pixel_size_z
288
+ )
289
+ write_volume(
290
+ align_path,
291
+ volume_align,
292
+ pixel_size_x_align,
293
+ pixel_size_y_align,
294
+ pixel_size_z_align,
295
+ )
296
+ return
230
297
  elif "3D Disc" in file_name:
231
298
  volume = np.frombuffer(f.read(), dtype=np.single)
232
299
  frames_per_data_group = 106
@@ -435,7 +502,7 @@ def main():
435
502
  )
436
503
 
437
504
  if not args.en_face and not args.seg_curve:
438
- volume = rotate_volume(volume)
505
+ volume = np.rot90(volume, k=1, axes=(1, 2))
439
506
 
440
507
  write_volume(output_path, volume, pixel_size_x, pixel_size_y, pixel_size_z)
441
508
 
@@ -1,30 +1,43 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: oct-to-tiff
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A command line tool for converting optical coherence tomography angiography (OCTA) data.
5
- Home-page: https://github.com/camlloyd/oct-to-tiff
6
- Author: Cameron Lloyd
7
- Author-email: lloyd@med.unideb.hu
5
+ Author-email: Cameron Lloyd <lloyd@med.unideb.hu>
6
+ Maintainer-email: Cameron Lloyd <lloyd@med.unideb.hu>
7
+ License-Expression: BSD-3-Clause
8
+ Project-URL: Homepage, https://github.com/camlloyd/oct-to-tiff
8
9
  Project-URL: Bug Tracker, https://github.com/camlloyd/oct-to-tiff/issues
10
+ Project-URL: Changelog, https://github.com/camlloyd/oct-to-tiff/blob/main/CHANGELOG.md
11
+ Keywords: angiography,cli,oct,octa
9
12
  Classifier: Development Status :: 3 - Alpha
10
13
  Classifier: Intended Audience :: Science/Research
11
- Classifier: License :: OSI Approved :: BSD License
12
14
  Classifier: Natural Language :: English
13
15
  Classifier: Operating System :: OS Independent
14
16
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
- Requires-Python: >=3.8
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.11
20
22
  Description-Content-Type: text/markdown
21
23
  License-File: LICENSE.txt
24
+ Requires-Dist: defusedxml
22
25
  Requires-Dist: numpy
26
+ Requires-Dist: roifile
23
27
  Requires-Dist: tifffile
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy; extra == "dev"
30
+ Requires-Dist: ruff; extra == "dev"
31
+ Requires-Dist: types-defusedxml; extra == "dev"
32
+ Dynamic: license-file
24
33
 
25
34
  # oct-to-tiff
26
35
 
27
36
  [![DOI](https://zenodo.org/badge/382486199.svg)](https://zenodo.org/badge/latestdoi/382486199)
37
+ [![PyPI - Version](https://img.shields.io/pypi/v/oct-to-tiff)](https://pypi.org/project/oct-to-tiff)
38
+ [![PyPI - License](https://img.shields.io/pypi/l/oct-to-tiff)](https://github.com/camlloyd/oct-to-tiff/blob/main/LICENSE.txt)
39
+ [![PyPI Downloads](https://static.pepy.tech/badge/oct-to-tiff)](https://pepy.tech/projects/oct-to-tiff)
40
+
28
41
 
29
42
  A command line tool for converting optical coherence tomography angiography (OCTA) data.
30
43
 
@@ -33,9 +46,9 @@ via pip:
33
46
 
34
47
  pip install oct-to-tiff
35
48
 
36
- via conda:
49
+ via mamba:
37
50
 
38
- conda install -c conda-forge oct-to-tiff
51
+ mamba install -c conda-forge oct-to-tiff
39
52
 
40
53
  ## Getting started
41
54
  oct-to-tiff /path/to/image.OCT
@@ -51,6 +64,12 @@ By default, the output file will be written with the same name as the input file
51
64
 
52
65
  To specify a custom output directory, see [Optional arguments](#optional-arguments) below.
53
66
 
67
+ ## Batch processing
68
+ ``` bash
69
+ for file in *.OCT; do oct-to-tiff "${file}"; done
70
+ ```
71
+ will convert all OCT volumes in the current directory to OME-TIFF files, including voxel size in the metadata.
72
+
54
73
  ## Supported scan patterns
55
74
 
56
75
  This tool has been developed by reverse engineering data from the Optovue RTVue XR Avanti System.
@@ -113,6 +132,13 @@ Sets the correct voxel size for scan patterns with adjustable length.
113
132
 
114
133
  oct-to-tiff /path/to/image.OCT --size 4.5
115
134
 
135
+ #### `--log-level LEVEL`
136
+ **Description**: sets the logging level (default: `WARNING`)
137
+
138
+ **Usage**:
139
+
140
+ oct-to-tiff /path/to/image.OCT --log-level INFO
141
+
116
142
  #### `--version`
117
143
  **Description**: show program's version number and exit.
118
144
 
@@ -156,8 +182,8 @@ Requires `--size SIZE`.
156
182
 
157
183
  ## Contributing
158
184
 
159
- This project uses [black](https://github.com/psf/black) for formatting and [isort](https://github.com/PyCQA/isort) for sorting imports.
185
+ This project uses [Ruff](https://github.com/astral-sh/ruff) for linting and formatting.
160
186
 
161
187
  ## Requirements
162
188
 
163
- Requires Python 3.8 or higher.
189
+ Requires Python 3.11 or higher.
@@ -0,0 +1,8 @@
1
+ oct_to_tiff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ oct_to_tiff/cli.py,sha256=GmIMXLuNfAEZaijX2_93XB95V0Mdr1yb73Q_j8eBiEI,17232
3
+ oct_to_tiff-0.6.0.dist-info/licenses/LICENSE.txt,sha256=8KO0dluzLStmIF0HM18BOP9T2miIcX8q-GOfMOw6Ngk,1521
4
+ oct_to_tiff-0.6.0.dist-info/METADATA,sha256=7atS8U7OhVhVpU48NjbGzqMtbGhlA_l4Dh6jZUiu010,4913
5
+ oct_to_tiff-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ oct_to_tiff-0.6.0.dist-info/entry_points.txt,sha256=mKWNdkTThZm2JKtLwp4PGMpXkvAWk-xnQDAOyRsCo9Y,53
7
+ oct_to_tiff-0.6.0.dist-info/top_level.txt,sha256=_ovqm7f48yb_IAiVqWzB3VPnHXwzkkEqQQ_ZJz_McSY,12
8
+ oct_to_tiff-0.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- oct_to_tiff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- oct_to_tiff/cli.py,sha256=Ct0yG2iALYWAyYMZJZDa7FusQbm6yoYXHQYF5sC8zxc,15115
3
- oct_to_tiff-0.4.0.dist-info/LICENSE.txt,sha256=8KO0dluzLStmIF0HM18BOP9T2miIcX8q-GOfMOw6Ngk,1521
4
- oct_to_tiff-0.4.0.dist-info/METADATA,sha256=RZGGDg3QPgTxAi3OxfVz-KjXCtP0Bh0yfmfIekIKtlE,3903
5
- oct_to_tiff-0.4.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
6
- oct_to_tiff-0.4.0.dist-info/entry_points.txt,sha256=mKWNdkTThZm2JKtLwp4PGMpXkvAWk-xnQDAOyRsCo9Y,53
7
- oct_to_tiff-0.4.0.dist-info/top_level.txt,sha256=_ovqm7f48yb_IAiVqWzB3VPnHXwzkkEqQQ_ZJz_McSY,12
8
- oct_to_tiff-0.4.0.dist-info/RECORD,,