Skip to content

Commit 6aba946

Browse files
authored
Merge pull request #25 from computational-cell-analytics/refactor-and-linter
Code was refactored, linted, and the docstring style was unified. Additionally, the postprocessing has been expanded to include the calculation of the local Ripley's K function, neighbors in a given radius, and the average distance to n nearest neighbors.
2 parents ef8a5da + 17bae38 commit 6aba946

19 files changed

+1042
-414
lines changed

flamingo_tools/data_conversion.py

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55

66
from glob import glob
77
from pathlib import Path
8-
from typing import Optional, List, Dict
8+
from typing import Optional, List, Dict, Tuple
99

1010
import numpy as np
1111
import pybdv
12-
import tifffile
1312

1413
from cluster_tools.utils.volume_utils import write_format_metadata
1514
from elf.io import open_file
1615
from skimage.transform import rescale
1716

17+
from .file_utils import read_tif, read_raw
18+
1819

1920
def _read_resolution_and_unit_flamingo(mdata_path):
2021
resolution = None
@@ -60,7 +61,27 @@ def _read_start_position_flamingo(path):
6061
return start_position
6162

6263

63-
def read_metadata_flamingo(metadata_path, offset=None, parse_affine=False):
64+
def read_metadata_flamingo(
65+
metadata_path: str,
66+
offset: Optional[np.ndarray] = None,
67+
parse_affine: bool = False
68+
) -> Tuple[List[float], str, List[float]]:
69+
"""Read acquisition metadata from a flamingo metadata file.
70+
71+
This will read the resolution, the physical unit, and optionally the
72+
voxel grid transformation from the metadata file. The voxel grid transformation
73+
places tile at their correct tile position.
74+
75+
Args:
76+
metadata_path: The path to the metadata file.
77+
offset: The spatial offset of this data.
78+
parse_affine: Whether to read the affine transformation from the metadata.
79+
80+
Returns:
81+
The resolution / voxel size of the data.
82+
The physical unit of the voxel size.
83+
The affine voxel grid transformation of the data.
84+
"""
6485
resolution, unit = None, None
6586

6687
resolution, unit = _read_resolution_and_unit_flamingo(metadata_path)
@@ -109,7 +130,7 @@ def _pos_to_trafo(pos):
109130

110131

111132
# TODO derive the scale factors from the shape rather than hard-coding it to 5 levels
112-
def derive_scale_factors(shape):
133+
def _derive_scale_factors(shape):
113134
scale_factors = [[2, 2, 2]] * 5
114135
return scale_factors
115136

@@ -147,11 +168,25 @@ def _to_ome_zarr(data, out_path, scale_factors, timepoint, setup_id, attributes,
147168
)
148169

149170

150-
def flamingo_filename_parser(file_path, name_mapping):
171+
def flamingo_filename_parser(file_path: str, name_mapping: Optional[Dict]) -> Tuple[int, Dict[str, str], str]:
172+
"""Parse the name of flamingo output files.
173+
174+
This maps the filenames to the corresponding timepoint, the BigStitcher
175+
compatible attributes, and the id (name) of the attributes.
176+
177+
Args:
178+
file_path: The path to the flamingo data.
179+
name_mapping: Optional mapping of parsed attributes to their actual names.
180+
181+
Returns:
182+
The timepoint of this data.
183+
The dictionary mapping attribute names to their values.
184+
The normalized attribute names.
185+
"""
151186
filename = os.path.basename(file_path)
152187

153188
# Extract the timepoint.
154-
match = re.search(r'_t(\d+)_', filename)
189+
match = re.search(r"_t(\d+)_", filename)
155190
if match:
156191
timepoint = int(match.group(1))
157192
else:
@@ -163,25 +198,25 @@ def flamingo_filename_parser(file_path, name_mapping):
163198
name_mapping = {}
164199

165200
# Extract the channel.
166-
match = re.search(r'_C(\d+)_', filename)
201+
match = re.search(r"_C(\d+)_", filename)
167202
channel = int(match.group(1)) if match else 0
168203
channel_mapping = name_mapping.get("channel", {})
169204
attributes["channel"] = {"id": channel, "name": channel_mapping.get(channel, str(channel))}
170205

171206
# Extract the tile.
172-
match = re.search(r'_R(\d+)_', filename)
207+
match = re.search(r"_R(\d+)_", filename)
173208
tile = int(match.group(1)) if match else 0
174209
tile_mapping = name_mapping.get("tile", {})
175210
attributes["tile"] = {"id": tile, "name": tile_mapping.get(tile, str(tile))}
176211

177212
# Extract the illumination.
178-
match = re.search(r'_I(\d+)_', filename)
213+
match = re.search(r"_I(\d+)_", filename)
179214
illumination = int(match.group(1)) if match else 0
180215
illumination_mapping = name_mapping.get("illumination", {})
181216
attributes["illumination"] = {"id": illumination, "name": illumination_mapping.get(illumination, str(illumination))}
182217

183218
# Extract D. TODO what is this?
184-
match = re.search(r'_D(\d+)_', filename)
219+
match = re.search(r"_D(\d+)_", filename)
185220
D = int(match.group(1)) if match else 0
186221
D_mapping = name_mapping.get("D", {})
187222
attributes["D"] = {"id": D, "name": D_mapping.get(D, str(D))}
@@ -207,35 +242,11 @@ def _write_missing_views(out_path):
207242
tree.write(xml_path)
208243

209244

210-
def _parse_shape(metadata_file):
211-
depth, height, width = None, None, None
212-
213-
with open(metadata_file, "r") as f:
214-
for line in f.readlines():
215-
line = line.strip().rstrip("\n")
216-
if line.startswith("AOI width"):
217-
width = int(line.split(" ")[-1])
218-
if line.startswith("AOI height"):
219-
height = int(line.split(" ")[-1])
220-
if line.startswith("Number of planes saved"):
221-
depth = int(line.split(" ")[-1])
222-
223-
assert depth is not None
224-
assert height is not None
225-
assert width is not None
226-
return (depth, height, width)
227-
228-
229245
def _load_data(file_path, metadata_file):
230246
if Path(file_path).suffix == ".raw":
231-
shape = _parse_shape(metadata_file)
232-
data = np.memmap(file_path, mode="r", dtype="uint16", shape=shape)
247+
data = read_raw(file_path, metadata_file)
233248
else:
234-
try:
235-
data = tifffile.memmap(file_path, mode="r")
236-
except ValueError:
237-
print(f"Could not memmap the data from {file_path}. Fall back to load it into memory.")
238-
data = tifffile.imread(file_path)
249+
data = read_tif(file_path)
239250
return data
240251

241252

@@ -360,7 +371,7 @@ def convert_lightsheet_to_bdv(
360371
print(f"Converting tp={timepoint}, channel={attributes['channel']}, tile={attributes['tile']}")
361372
data = _load_data(file_path, metadata_file)
362373
if scale_factors is None:
363-
scale_factors = derive_scale_factors(data.shape)
374+
scale_factors = _derive_scale_factors(data.shape)
364375

365376
if convert_to_ome_zarr:
366377
_to_ome_zarr(data, out_path, scale_factors, timepoint, setup_id, attributes, unit, resolution)
@@ -387,6 +398,8 @@ def convert_lightsheet_to_bdv(
387398

388399

389400
def convert_lightsheet_to_bdv_cli():
401+
"""@private
402+
"""
390403
import argparse
391404

392405
parser = argparse.ArgumentParser(

flamingo_tools/file_utils.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import warnings
2+
from typing import Optional, Union
3+
4+
import imageio.v3 as imageio
5+
import numpy as np
6+
import tifffile
7+
import zarr
8+
from elf.io import open_file
9+
10+
11+
def _parse_shape(metadata_file):
12+
depth, height, width = None, None, None
13+
14+
with open(metadata_file, "r") as f:
15+
for line in f.readlines():
16+
line = line.strip().rstrip("\n")
17+
if line.startswith("AOI width"):
18+
width = int(line.split(" ")[-1])
19+
if line.startswith("AOI height"):
20+
height = int(line.split(" ")[-1])
21+
if line.startswith("Number of planes saved"):
22+
depth = int(line.split(" ")[-1])
23+
24+
assert depth is not None
25+
assert height is not None
26+
assert width is not None
27+
return (depth, height, width)
28+
29+
30+
def read_raw(file_path: str, metadata_file: str) -> np.memmap:
31+
"""Read a raw file written by the flamingo microscope.
32+
33+
Args:
34+
file_path: The file path to the raw file.
35+
metadata_file: The file path to the metadata describing the raw file.
36+
The metadata will be used to determine the shape of the data.
37+
38+
Returns:
39+
The memory-mapped data.
40+
"""
41+
shape = _parse_shape(metadata_file)
42+
return np.memmap(file_path, mode="r", dtype="uint16", shape=shape)
43+
44+
45+
def read_tif(file_path: str) -> Union[np.ndarray, np.memmap]:
46+
"""Read a tif file.
47+
48+
Tries to memory map the file. If not possible will load the complete file into memory
49+
and raise a warning.
50+
51+
Args:
52+
file_path: The file path to the tif file.
53+
54+
Returns:
55+
The memory-mapped data. If not possible to memmap, the data in memory.
56+
"""
57+
try:
58+
x = tifffile.memmap(file_path)
59+
except ValueError:
60+
warnings.warn(f"Cannot memmap the tif file at {file_path}. Fall back to loading it into memory.")
61+
x = imageio.imread(file_path)
62+
return x
63+
64+
65+
def read_image_data(input_path: Union[str, zarr.storage.FSStore], input_key: Optional[str]) -> np.typing.ArrayLike:
66+
"""Read flamingo image data, stored in various formats.
67+
68+
Args:
69+
input_path: The file path to the data, or a zarr S3 store for data remotely accessed on S3.
70+
The data can be stored as a tif file, or a zarr/n5 container.
71+
Access via S3 is only supported for a zarr container.
72+
input_key: The key (= internal path) for a zarr or n5 container.
73+
Set it to None if the data is stored in a tif file.
74+
75+
Returns:
76+
The data, loaded either as a numpy mem-map, a numpy array, or a zarr / n5 array.
77+
"""
78+
if input_key is None:
79+
input_ = read_tif(input_path)
80+
elif isinstance(input_path, str):
81+
input_ = open_file(input_path, "r")[input_key]
82+
else:
83+
with zarr.open(input_path, mode="r") as f:
84+
input_ = f[input_key]
85+
return input_

0 commit comments

Comments
 (0)