• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

angelolab / tmi / 4106902596

pending completion
4106902596

Pull #20

github

GitHub
Merge 5de60795e into 9fb6cee18
Pull Request #20: GitHub Actions

294 of 306 branches covered (96.08%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

431 of 443 relevant lines covered (97.29%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.1
/src/tmi/load_utils.py
1
import os
1✔
2
import pathlib
1✔
3
import re
1✔
4
import warnings
1✔
5
from typing import List, Optional, OrderedDict, Union
1✔
6

7
import numpy as np
1✔
8
import skimage.io as io
1✔
9
import xarray as xr
1✔
10
import xmltodict
1✔
11
from tifffile import TiffFile, TiffPageSeries, TiffWriter
1✔
12

13
from tmi import image_utils, io_utils, misc_utils, tiff_utils
1✔
14
from tmi.settings import EXTENSION_TYPES
1✔
15

16

17
def load_imgs_from_mibitiff(data_dir, mibitiff_files=None, channels=None, delimiter=None):
1✔
18
    """Load images from a series of MIBItiff files.
19

20
    This function takes a set of MIBItiff files and load the images into an xarray. The type used
21
    to store the images will be the same as that of the MIBIimages stored in the MIBItiff files.
22

23
    Args:
24
        data_dir (str):
25
            directory containing MIBItiffs
26
        mibitiff_files (list):
27
            list of MIBItiff files to load. If None, all MIBItiff files in data_dir are loaded.
28
        channels (list):
29
            optional list of channels to load. Defaults to `None`, in which case, all channels in
30
            the first MIBItiff are used.
31
        delimiter (str):
32
            optional delimiter-character/string which separate fov names from the rest of the file
33
            name. Defaults to None.
34

35
    Returns:
36
        xarray.DataArray:
37
            xarray with shape [fovs, x_dim, y_dim, channels]
38
    """
39

40
    io_utils.validate_paths(data_dir)
1✔
41

42
    if not mibitiff_files:
1✔
43
        mibitiff_files = io_utils.list_files(data_dir, substrs=[".tiff"])
1✔
44
        mibitiff_files.sort()
1✔
45

46
    if len(mibitiff_files) == 0:
1✔
47
        raise ValueError("No mibitiff files specified in the data directory %s" % data_dir)
1✔
48

49
    # extract fov names w/ delimiter agnosticism
50
    fovs = io_utils.remove_file_extensions(mibitiff_files)
1✔
51
    fovs = io_utils.extract_delimited_names(fovs, delimiter=delimiter)
1✔
52

53
    mibitiff_files = [os.path.join(data_dir, mt_file) for mt_file in mibitiff_files]
1✔
54

55
    test_img = io.imread(mibitiff_files[0], plugin="tifffile")
1✔
56

57
    # The dtype is always the type of the image being loaded in.
58
    dtype = test_img.dtype
1✔
59

60
    # if no channels specified, get them from first MIBItiff file
61
    if channels is None:
1✔
62
        _, channel_tuples = tiff_utils.read_mibitiff(mibitiff_files[0])
1✔
63
        channels = [channel_tuple[1] for channel_tuple in channel_tuples]
1✔
64

65
    if len(channels) == 0:
1✔
66
        raise ValueError("No channels provided in channels list")
1✔
67

68
    # extract images from MIBItiff file
69
    img_data = []
1✔
70
    for mibitiff_file in mibitiff_files:
1✔
71
        img_data.append(tiff_utils.read_mibitiff(mibitiff_file, channels)[0])
1✔
72
    img_data = np.stack(img_data, axis=0)
1✔
73

74
    if np.min(img_data) < 0:
1✔
75
        warnings.warn("You have images with negative values loaded in.")
×
76

77
    img_data = img_data.astype(dtype)
1✔
78

79
    # create xarray with image data
80
    img_xr = xr.DataArray(
1✔
81
        img_data,
82
        coords=[fovs, range(img_data[0].data.shape[0]), range(img_data[0].data.shape[1]), channels],
83
        dims=["fovs", "rows", "cols", "channels"],
84
    )
85

86
    return img_xr
1✔
87

88

89
def load_imgs_from_tree(
1✔
90
    data_dir, img_sub_folder=None, fovs=None, channels=None, max_image_size=None
91
):
92
    """Takes a set of imgs from a directory structure and loads them into an xarray.
93

94
    Args:
95
        data_dir (str):
96
            directory containing folders of images
97
        img_sub_folder (str):
98
            optional name of image sub-folder within each fov
99
        fovs (str, list):
100
            optional list of folders to load imgs from, or the name of a single folder. Default
101
            loads all folders
102
        channels (list):
103
            optional list of imgs to load, otherwise loads all imgs
104
        max_image_size (int or None):
105
            The length (in pixels) of the largest image that will be loaded. All other images will
106
            be padded to bring them up to the same size.
107

108
    Returns:
109
        xarray.DataArray:
110
            xarray with shape [fovs, x_dim, y_dim, tifs]
111
    """
112

113
    io_utils.validate_paths(data_dir)
1✔
114

115
    if fovs is None:
1✔
116
        # get all fovs
117
        fovs = io_utils.list_folders(data_dir)
1✔
118
        fovs.sort()
1✔
119

120
    if len(fovs) == 0:
1✔
121
        raise ValueError(f"No fovs found in directory, {data_dir}")
1✔
122

123
    # If the fov provided is a single string (`fov_1` instead of [`fov_1`])
124
    if type(fovs) is str:
1✔
125
        fovs = [fovs]
1✔
126
    if img_sub_folder is None:
1✔
127
        # no img_sub_folder, change to empty string to read directly from base folder
128
        img_sub_folder = ""
1✔
129

130
    # get imgs from first fov if no img names supplied
131
    if channels is None:
1✔
132
        channels = io_utils.list_files(
1✔
133
            dir_name=os.path.join(data_dir, fovs[0], img_sub_folder),
134
            substrs=EXTENSION_TYPES["IMAGE"],
135
        )
136

137
        # if taking all channels from directory, sort them alphabetically
138
        channels.sort()
1✔
139
    # otherwise, fill channel names with correct file extension
140
    elif not all([img.endswith(tuple(EXTENSION_TYPES["IMAGE"])) for img in channels]):
1✔
141
        # need this to reorder channels back because list_files may mess up the ordering
142
        channels_no_delim = [img.split(".")[0] for img in channels]
1✔
143

144
        all_channels = io_utils.list_files(
1✔
145
            dir_name=os.path.join(data_dir, fovs[0], img_sub_folder),
146
            substrs=channels_no_delim,
147
            exact_match=True,
148
        )
149

150
        # get the corresponding indices found in channels_no_delim
151
        channels_indices = [channels_no_delim.index(chan.split(".")[0]) for chan in all_channels]
1✔
152

153
        # verify if channels from user input are present in `all_channels`
154
        all_channels_no_delim = [channel.split(".")[0] for channel in all_channels]
1✔
155

156
        misc_utils.verify_same_elements(
1✔
157
            all_channels_in_folder=all_channels_no_delim, all_channels_detected=channels_no_delim
158
        )
159
        # reorder back to original
160
        channels = [chan for _, chan in sorted(zip(channels_indices, all_channels))]
1✔
161

162
    if len(channels) == 0:
1✔
163
        raise ValueError("No images found in designated folder")
1✔
164

165
    test_img = io.imread(os.path.join(data_dir, fovs[0], img_sub_folder, channels[0]))
1✔
166

167
    # The dtype is always the type of the image being loaded in.
168
    dtype = test_img.dtype
1✔
169

170
    if max_image_size is not None:
1✔
171
        img_data = np.zeros((len(fovs), max_image_size, max_image_size, len(channels)), dtype=dtype)
1✔
172
    else:
173
        img_data = np.zeros(
1✔
174
            (len(fovs), test_img.shape[0], test_img.shape[1], len(channels)), dtype=dtype
175
        )
176

177
    for fov in range(len(fovs)):
1✔
178
        for img in range(len(channels)):
1✔
179
            if max_image_size is not None:
1✔
180
                temp_img = io.imread(
1✔
181
                    os.path.join(data_dir, fovs[fov], img_sub_folder, channels[img])
182
                )
183
                img_data[fov, : temp_img.shape[0], : temp_img.shape[1], img] = temp_img
1✔
184
            else:
185
                img_data[fov, :, :, img] = io.imread(
1✔
186
                    os.path.join(data_dir, fovs[fov], img_sub_folder, channels[img])
187
                )
188

189
    # check to make sure that dtype wasn't too small for range of data
190
    if np.min(img_data) < 0:
1✔
191
        warnings.warn("You have images with negative values loaded in.")
×
192

193
    row_coords, col_coords = range(img_data.shape[1]), range(img_data.shape[2])
1✔
194

195
    # remove .tiff from image name
196
    img_names = [os.path.splitext(img)[0] for img in channels]
1✔
197

198
    img_xr = xr.DataArray(
1✔
199
        img_data,
200
        coords=[fovs, row_coords, col_coords, img_names],
201
        dims=["fovs", "rows", "cols", "channels"],
202
    )
203

204
    return img_xr
1✔
205

206

207
def load_imgs_from_dir(
1✔
208
    data_dir,
209
    files=None,
210
    match_substring=None,
211
    trim_suffix=None,
212
    xr_dim_name="compartments",
213
    xr_channel_names=None,
214
    channel_indices=None,
215
):
216
    """Takes a set of images (possibly multitiffs) from a directory and loads them into an xarray.
217

218
    Args:
219
        data_dir (str):
220
            directory containing images
221
        files (list):
222
            list of files (e.g. ['fov1.tiff'. 'fov2.tiff'] to load.
223
            If None, all (.tiff, .jpg, .png) files in data_dir are loaded.
224
        match_substring (str):
225
            a filename substring that all loaded images must contain. Ignored if files argument is
226
            not None.  If None, no matching is performed.
227
            Default is None.
228
        trim_suffix (str):
229
            a filename suffix to trim from the fov name if present. If None, no characters will be
230
            trimmed.  Default is None.
231
        xr_dim_name (str):
232
            sets the name of the last dimension of the output xarray.
233
            Default: 'compartments'
234
        xr_channel_names (list):
235
            sets the name of the coordinates in the last dimension of the output xarray.
236
        channel_indices (list):
237
            optional list of indices specifying which channels to load (by their indices).
238
            if None or empty, the function loads all channels.
239
            (Ignored if data is not multitiff).
240

241
    Returns:
242
        xarray.DataArray:
243
            xarray with shape [fovs, x_dim, y_dim, tifs]
244

245
    Raises:
246
        ValueError:
247
            Raised in the following cases:
248

249
            - data_dir is not a directory, <data_dir>/img is
250
              not a file for some img in the input 'files' list, or no images are found.
251
            - channels_indices are invalid according to the shape of the images.
252
            - The length of xr_channel_names (if provided) does not match the number
253
              of channels in the input.
254
    """
255

256
    io_utils.validate_paths(data_dir)
1✔
257

258
    if files is None:
1✔
259
        imgs = io_utils.list_files(data_dir, substrs=EXTENSION_TYPES["IMAGE"])
1✔
260
        if match_substring is not None:
1✔
261
            filenames = io_utils.remove_file_extensions(imgs)
1✔
262
            imgs = [imgs[i] for i, name in enumerate(filenames) if match_substring in name]
1✔
263
        imgs.sort()
1✔
264
    else:
265
        imgs = files
1✔
266
        for img in imgs:
1✔
267
            if not os.path.isfile(os.path.join(data_dir, img)):
1✔
268
                raise ValueError(
1✔
269
                    f"Invalid value for {img}. {os.path.join(data_dir, img)} is not a file."
270
                )
271

272
    if len(imgs) == 0:
1✔
273
        raise ValueError(f"No images found in directory, {data_dir}")
1✔
274

275
    test_img = io.imread(os.path.join(data_dir, imgs[0]))
1✔
276

277
    # check data format
278
    multitiff = test_img.ndim == 3
1✔
279
    channels_first = multitiff and test_img.shape[0] == min(test_img.shape)
1✔
280

281
    # check to make sure all channel indices are valid given the shape of the image
282
    n_channels = 1
1✔
283
    if multitiff:
1✔
284
        n_channels = test_img.shape[0] if channels_first else test_img.shape[2]
1✔
285
        if channel_indices:
1✔
286
            if max(channel_indices) >= n_channels or min(channel_indices) < 0:
1✔
287
                raise ValueError(
1✔
288
                    "Invalid value for channel_indices. Indices should be"
289
                    f" between 0-{n_channels-1} for the given data."
290
                )
291
    # make sure channels_names has the same length as the number of channels in the image
292
    if xr_channel_names and n_channels != len(xr_channel_names):
1✔
293
        raise ValueError(
1✔
294
            "Invalid value for xr_channel_names. xr_channel_names"
295
            f" length should be {n_channels}, as the number of channels"
296
            " in the input data."
297
        )
298

299
    # The dtype is always the type of the image being loaded in.
300
    dtype = test_img.dtype
1✔
301

302
    # extract data
303
    img_data = []
1✔
304
    for img in imgs:
1✔
305
        v = io.imread(os.path.join(data_dir, img))
1✔
306
        if not multitiff:
1✔
307
            v = np.expand_dims(v, axis=2)
1✔
308
        elif channels_first:
1✔
309
            # covert channels_first to be channels_last
310
            v = np.moveaxis(v, 0, -1)
1✔
311
        img_data.append(v)
1✔
312
    img_data = np.stack(img_data, axis=0)
1✔
313

314
    img_data = img_data.astype(dtype)
1✔
315

316
    if channel_indices and multitiff:
1✔
317
        img_data = img_data[:, :, :, channel_indices]
1✔
318

319
    if np.min(img_data) < 0:
1✔
320
        warnings.warn("You have images with negative values loaded in.")
×
321

322
    if channels_first:
1✔
323
        row_coords, col_coords = range(test_img.shape[1]), range(test_img.shape[2])
1✔
324
    else:
325
        row_coords, col_coords = range(test_img.shape[0]), range(test_img.shape[1])
1✔
326

327
    # get fov name from imgs
328
    fovs = io_utils.remove_file_extensions(imgs)
1✔
329
    fovs = io_utils.extract_delimited_names(fovs, delimiter=trim_suffix)
1✔
330

331
    # create xarray with image data
332
    img_xr = xr.DataArray(
1✔
333
        img_data,
334
        coords=[
335
            fovs,
336
            row_coords,
337
            col_coords,
338
            xr_channel_names if xr_channel_names else range(img_data.shape[3]),
339
        ],
340
        dims=["fovs", "rows", "cols", xr_dim_name],
341
    )
342

343
    return img_xr
1✔
344

345

346
def check_fov_name_prefix(fov_list):
1✔
347
    """Checks for a prefix (usually detailing a run name) in any of the provided FOV names
348

349
    Args:
350
        fov_list (list): list of fov name
351
    Returns:
352
        tuple: (bool) whether at least one fov names has a prefix,
353
               (list / dict) if prefix, dictionary with fov name as keys and prefixes as values
354
                otherwise return a simple list of the fov names
355
    """
356

357
    # check for prefix in any of the fov names
358
    prefix = False
1✔
359
    for folder in fov_list:
1✔
360
        if re.search("R.{1,3}C.{1,3}", folder).start() != 0:
1✔
361
            prefix = True
1✔
362

363
    if prefix:
1✔
364
        # dict containing fov name and run name
365
        fov_names = {}
1✔
366
        for folder in fov_list:
1✔
367
            fov = "".join(folder.split("_")[-1:])
1✔
368
            prefix_name = "_".join(folder.split("_")[:-1])
1✔
369
            fov_names[fov] = prefix_name
1✔
370
    else:
371
        # original list of fov names
372
        fov_names = fov_list
1✔
373

374
    return prefix, fov_names
1✔
375

376

377
def get_tiled_fov_names(fov_list, return_dims=False):
1✔
378
    """Generates the complete tiled fov list when given a list of fov names
379

380
    Args:
381
        fov_list (list):
382
            list of fov names
383
        return_dims (bool):
384
            whether to also return row and col dimensions
385
    Returns:
386
        tuple: names of all fovs expected for tiled image shape, and dimensions if return_dims
387
    """
388

389
    rows, cols, expected_fovs = [], [], []
1✔
390

391
    # check for run name prefix
392
    prefix, fov_names = check_fov_name_prefix(fov_list)
1✔
393

394
    # get tiled image dimensions
395
    for fov in fov_names:
1✔
396
        fov_digits = re.findall(r"\d+", fov)
1✔
397
        rows.append(int(fov_digits[0]))
1✔
398
        cols.append(int(fov_digits[1]))
1✔
399
    row_num, col_num = max(rows), max(cols)
1✔
400

401
    # fill list of expected fov names
402
    for n in range(row_num):
1✔
403
        for m in range(col_num):
1✔
404
            fov = f"R{n + 1}C{m + 1}"
1✔
405
            # prepend run names
406
            if prefix and fov in list(fov_names.keys()):
1✔
407
                expected_fovs.append(f"{fov_names[fov]}_" + fov)
1✔
408
            else:
409
                expected_fovs.append(fov)
1✔
410

411
    if return_dims:
1✔
412
        return expected_fovs, row_num, col_num
1✔
413
    else:
414
        return expected_fovs
1✔
415

416

417
def load_tiled_img_data(
1✔
418
    data_dir, fovs, expected_fovs, channel, single_dir, file_ext="tiff", img_sub_folder=""
419
):
420
    """Takes a set of images from a directory structure and loads them into a tiled xarray.
421

422
    Args:
423
        data_dir (str):
424
            directory containing folders of images
425
        fovs (list/dict):
426
            list of fovs (or dictionary with folder and RnCm names) to load data for
427
        expected_fovs (list):
428
            list of all expected RnCm fovs names in the tiled grid
429
        channel (str):
430
            single image name to load
431
        single_dir (bool):
432
            whether the images are stored in a single directory rather than within fov subdirs
433
        file_ext (str):
434
            the file type of existing images
435
        img_sub_folder (str):
436
            optional name of image sub-folder within each fov
437

438
    Returns:
439
        xarray.DataArray:
440
            xarray with shape [fovs, x_dim, y_dim, channel]
441
    """
442

443
    io_utils.validate_paths(data_dir)
1✔
444

445
    # check for toffy fovs
446
    if type(fovs) is dict:
1✔
447
        fov_list = list(fovs.values())
1✔
448
        tiled_names = list(fovs.keys())
1✔
449
    else:
450
        fov_list = fovs
1✔
451
        tiled_names = []
1✔
452

453
    # no missing fov images, load data normally and return array
454
    if len(fov_list) == len(expected_fovs):
1✔
455
        if single_dir:
1✔
456
            img_xr = load_imgs_from_dir(
1✔
457
                data_dir,
458
                match_substring=channel,
459
                xr_dim_name="channels",
460
                trim_suffix="_" + channel,
461
                xr_channel_names=[channel],
462
            )
463
        else:
464
            img_xr = load_imgs_from_tree(
1✔
465
                data_dir, img_sub_folder, fovs=fov_list, channels=[channel]
466
            )
467
        return img_xr
1✔
468

469
    # missing fov directories, read in a test image to get data type
470
    if single_dir:
1✔
471
        test_path = os.path.join(data_dir, expected_fovs[0] + "_" + channel + "." + file_ext)
1✔
472
    else:
473
        test_path = os.path.join(
1✔
474
            os.path.join(data_dir, fov_list[0], img_sub_folder, channel + "." + file_ext)
475
        )
476
    test_img = io.imread(test_path)
1✔
477
    img_data = np.zeros(
1✔
478
        (len(expected_fovs), test_img.shape[0], test_img.shape[1], 1), dtype=test_img.dtype
479
    )
480

481
    for fov, fov_name in enumerate(expected_fovs):
1✔
482
        # load in fov data for images, leave missing fovs as zeros
483
        if fov_name in fov_list:
1✔
484
            if single_dir:
1✔
485
                temp_img = io.imread(
1✔
486
                    os.path.join(data_dir, fov_name + "_" + channel + "." + file_ext)
487
                )
488
            else:
489
                temp_img = io.imread(
1✔
490
                    os.path.join(data_dir, fov_name, img_sub_folder, channel + "." + file_ext)
491
                )
492
            # fill in specific spot in array
493
            img_data[fov, : temp_img.shape[0], : temp_img.shape[1], 0] = temp_img
1✔
494

495
        # check against tiled_names from dict for toffy dirs
496
        elif fov_name in tiled_names:
1✔
497
            folder_name = fovs[fov_name]
1✔
498
            temp_img = io.imread(
1✔
499
                os.path.join(data_dir, folder_name, img_sub_folder, channel + "." + file_ext)
500
            )
501
            # fill in specific spot in array
502
            img_data[fov, : temp_img.shape[0], : temp_img.shape[1], 0] = temp_img
1✔
503

504
    # check to make sure that dtype wasn't too small for range of data
505
    if np.min(img_data) < 0:
1✔
506
        warnings.warn("You have images with negative values loaded in.")
×
507

508
    row_coords, col_coords = range(img_data.shape[1]), range(img_data.shape[2])
1✔
509

510
    img_xr = xr.DataArray(
1✔
511
        img_data,
512
        coords=[expected_fovs, row_coords, col_coords, [channel]],
513
        dims=["fovs", "rows", "cols", "channels"],
514
    )
515

516
    return img_xr
1✔
517

518

519
def fov_to_ome(
1✔
520
    data_dir: Union[str, pathlib.Path],
521
    ome_save_dir: Union[str, pathlib.Path],
522
    img_sub_folder: Optional[Union[str, pathlib.Path]] = None,
523
    fovs: Optional[List[str]] = None,
524
    channels: Optional[List[str]] = None,
525
) -> None:
526
    """
527
    Converts a folder of FOVs into an OME-TIFF per FOV. This can be filtered out by
528
    FOV and channel name.
529

530
    Args:
531
        data_dir (Union[str, pathlib.Path]):
532
            Directory containing a folder of images for each the FOVs.
533
        ome_save_dir (Union[str, pathlib.Path]):
534
            The directory to save the OME-TIFF file to.
535
        img_sub_folder (Union[str, pathlib.Path], optional):
536
            Optional name of image sub-folder within each FOV / Single Channel TIFF folder.
537
            Defaults to None.
538
        fovs (List[str], optional):
539
            A list of FOVs to gather and save as an OME-TIFF file. Defaults to None
540
            (Converts all FOVs in `data_dir` to OME-TIFFs).
541
        channels (List[str], optional):
542
            A list of channels to convert to an OME-TIFF. Defaults to None (Converts all channels
543
            as channels in an OME-TIFF.)
544
    """
545

546
    io_utils.validate_paths([data_dir, ome_save_dir])
1✔
547

548
    # Reorder the DataArray as OME-TIFFs require [Channel, Y, X]
549
    fov_xr: xr.DataArray = load_imgs_from_tree(
1✔
550
        data_dir=data_dir, img_sub_folder=img_sub_folder, fovs=fovs, channels=channels
551
    ).transpose("fovs", "channels", "cols", "rows")
552

553
    _compression: dict = {"algorithm": "zlib", "args": {"level": 6}}
1✔
554

555
    for fov in fov_xr:
1✔
556
        fov_name: str = fov.fovs.values
1✔
557
        ome_file_path: pathlib.Path = pathlib.Path(ome_save_dir) / f"{fov_name}.ome.tiff"
1✔
558

559
        # Set metadata for the OME-TIFF
560
        _metadata = {
1✔
561
            "axes": "CYX",
562
            "Channel": {"Name": fov.channels.values.tolist()},
563
            "Name": fov_name,
564
        }
565

566
        # Write the OME-TIFF
567
        with TiffWriter(ome_file_path, ome=True) as ome_tiff:
1✔
568
            ome_tiff.write(
1✔
569
                data=fov.values,
570
                photometric="minisblack",
571
                compression=_compression["algorithm"],
572
                compressionargs=_compression["args"],
573
                metadata=_metadata,
574
            )
575

576

577
def ome_to_fov(ome: Union[str, pathlib.Path], data_dir: Union[str, pathlib.Path]) -> None:
1✔
578
    """
579
    Converts an OME-TIFF with n channels to a FOV (A folder consisting of those n channels).
580
    The folder's name is given by the Image `@Name` in the xml metadata.
581

582
    Args:
583
        ome (Union[str, pathlib.Path]): The path to the OME-TIFF file.
584
        data_dir (Union[str, pathlib.Path]): The path where the FOV will be saved.
585
    """
586

587
    # Convert `ome_tiff` to pathlib.Path if it is a string
588
    if isinstance(ome, str):
1✔
589
        ome = pathlib.Path(ome)
×
590

591
    io_utils.validate_paths(paths=[ome, data_dir])
1✔
592

593
    with TiffFile(ome, is_ome=True) as ome_tiff:
1✔
594
        # String representation of the OME-XML metadata & convert to dictionary
595
        ome_xml_metadata: OrderedDict = xmltodict.parse(ome_tiff.ome_metadata.encode())
1✔
596

597
        # An OME-TIFF's OME-XML metadata has either a single Image (dict), or a list of Images
598
        # (List[dict]). IF it's a list of images, then grab the first image (all images should
599
        # be the same, just different resolutions)
600
        if isinstance(ome_xml_metadata["OME"]["Image"], dict):
1✔
601
            image_data = ome_xml_metadata["OME"]["Image"]
1✔
602
        else:
603
            image_data = ome_xml_metadata["OME"]["Image"][0]
×
604

605
        # Get the OME-XML image name
606
        image_name: str = ome.stem.split(".")[-2]
1✔
607

608
        # Get the OME-XML channel metadata
609
        channel_metadata: List[OrderedDict] = image_data["Pixels"]["Channel"]
1✔
610
        save_dir: pathlib.Path = pathlib.Path(data_dir) / image_name
1✔
611

612
        # Corner case when only one channel. (OME-XML is a dict instead of a list of dicts)
613
        if isinstance(channel_metadata, dict):
1✔
614
            channel_metadata = [channel_metadata]
×
615

616
        # Get the channel names. Ex: {"DAPI", "CD3", "CD8"}.
617
        # No need to check for ordering, as the OME-TIFF Channel data is ordered.
618
        channels: List[str] = (
1✔
619
            [c["@Name"] for c in channel_metadata]
620
            if "@Name" in channel_metadata[0].keys()
621
            else [f"Channel {c}" for c in range(len(channel_metadata))]
622
        )
623

624
        # Get the TIFF pages for the current image
625
        ome_tiff_img_pages: TiffPageSeries = ome_tiff.series[0].pages
1✔
626

627
        for ome_tiff_page, channel in zip(ome_tiff_img_pages, channels):
1✔
628
            save_dir.mkdir(parents=True, exist_ok=True)
1✔
629

630
            image_utils.save_image(
1✔
631
                fname=save_dir / f"{channel}.tiff",
632
                data=ome_tiff_page.asarray().transpose(),
633
                compression_level=6,
634
            )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc