Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft of multi-tiff multi-page imaging extractor #323

Draft
wants to merge 35 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c4904c8
upload draft of multi-tiff multi-page imaging extractor. This could b…
bendichter May 10, 2024
fddf198
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 10, 2024
66c59f2
Merge branch 'main' into multitiff_multipage_imaging_extractor
bendichter May 15, 2024
ccf4d90
add tests
bendichter May 15, 2024
6de2ca4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 15, 2024
85e90db
dynamic import of tifffile
bendichter May 15, 2024
93b2545
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 15, 2024
fb772d6
fix tests
bendichter May 15, 2024
31c9df4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 15, 2024
7263bd9
fix test
bendichter May 15, 2024
3b6fb6b
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 15, 2024
f760ba6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 15, 2024
7892f8d
rmv print
bendichter May 15, 2024
44d64f7
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 15, 2024
fda7b9c
use multiimaginginterface
bendichter May 16, 2024
96152ac
add parse as a dependency
bendichter May 16, 2024
dbdf11e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2024
4925b40
move parse to minimal requirements
bendichter May 16, 2024
c11fc24
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 16, 2024
435f600
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2024
c1ae185
Add docstring for match_paths
bendichter May 16, 2024
2313673
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 16, 2024
a798a7c
pass docstring tests
bendichter May 16, 2024
a904272
use named vars?
bendichter May 16, 2024
66289fb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2024
293dc1c
add docstring for module
bendichter May 16, 2024
9f36886
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 16, 2024
b47ad48
add check for if no TIFF files were found
bendichter May 16, 2024
c54c052
move multitiffmultipage extractor over to existing tiffimagingextract…
bendichter May 16, 2024
a9153f1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2024
5c7b751
make sure match_paths can sub-select
bendichter May 16, 2024
6ed3bca
Merge remote-tracking branch 'origin/multitiff_multipage_imaging_extr…
bendichter May 16, 2024
a383ecd
refactor
bendichter May 17, 2024
fb93606
update tests
bendichter May 17, 2024
eb5cc8e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements-minimal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dill>=0.3.2
scipy>=1.5.2
psutil>=5.8.0
PyYAML
parse>=1.20.0
4 changes: 2 additions & 2 deletions src/roiextractors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from importlib.metadata import version

__version__ = version("roiextractors")

from .example_datasets import toy_example
from .extraction_tools import show_video
from .extractorlist import *
from .imagingextractor import ImagingExtractor
from .segmentationextractor import SegmentationExtractor

__version__ = version("roiextractors")
4 changes: 4 additions & 0 deletions src/roiextractors/extractorlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
BrukerTiffMultiPlaneImagingExtractor,
BrukerTiffSinglePlaneImagingExtractor,
MicroManagerTiffImagingExtractor,
MultiTiffImagingExtractor,
FolderTiffImagingExtractor,
)
from .extractors.sbximagingextractor import SbxImagingExtractor
from .extractors.inscopixextractors import InscopixImagingExtractor
Expand Down Expand Up @@ -51,6 +53,8 @@
NumpyMemmapImagingExtractor,
MemmapImagingExtractor,
VolumetricImagingExtractor,
MultiTiffImagingExtractor,
FolderTiffImagingExtractor,
InscopixImagingExtractor,
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
Specialized extractor for reading TIFF files produced via Micro-Manager.
"""

from .tiffimagingextractor import TiffImagingExtractor
from .tiffimagingextractor import TiffImagingExtractor, MultiTiffImagingExtractor, FolderTiffImagingExtractor
from .scanimagetiffimagingextractor import (
ScanImageTiffImagingExtractor,
ScanImageTiffMultiPlaneImagingExtractor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
import numpy as np
from tqdm import tqdm

from ...multiimagingextractor import MultiImagingExtractor
from ...imagingextractor import ImagingExtractor
from ...extraction_tools import (
PathType,
FloatType,
raise_multi_channel_or_depth_not_implemented,
get_package,
)
from ...utils import match_paths


class TiffImagingExtractor(ImagingExtractor):
Expand Down Expand Up @@ -151,3 +153,56 @@ def write_imaging(imaging, save_path, overwrite: bool = False, chunk_size=None,
)
chunk_frames = np.squeeze(video)
tif.save(chunk_frames, contiguous=True, metadata=None)


class MultiTiffImagingExtractor(MultiImagingExtractor):
"""A ImagingExtractor for multiple TIFF files that each have multiple pages."""

extractor_name = "multi-tiff multi-page Imaging Extractor"
is_writable = False

def __init__(self, file_paths: list[str], sampling_frequency: float):
"""Create a MultiTiffImagingExtractor instance.

Parameters
----------
file_paths: list of str
List of paths to the TIFF files.
sampling_frequency : float
The frequency at which the frames were sampled, in Hz.
"""
self.file_paths = file_paths
imaging_extractors = [
TiffImagingExtractor(file_path=x, sampling_frequency=sampling_frequency) for x in self.file_paths
]
super().__init__(imaging_extractors=imaging_extractors)
self._kwargs.update({"file_paths": file_paths})


class FolderTiffImagingExtractor(MultiTiffImagingExtractor):
"""A ImagingExtractor for multiple TIFF files in a folder that each have multiple pages."""

extractor_name = "folder-tiff multi-page Imaging Extractor"
is_writable = False

def __init__(self, folder_path: PathType, pattern: str, sampling_frequency: float):
"""Create a FolderTiffImagingExtractor instance.

Parameters
----------
folder_path: PathType
Path to the folder containing the TIFF files.
pattern : str
The f-string pattern to match the TIFF files in the folder.
sampling_frequency : float
The frequency at which the frames were sampled, in Hz.
"""
folder_path = Path(folder_path)
file_paths = match_paths(str(folder_path), pattern)
super().__init__(file_paths=file_paths, sampling_frequency=sampling_frequency)
self._kwargs.update(
{
"folder_path": str(folder_path.absolute()),
"pattern": pattern,
}
)
38 changes: 38 additions & 0 deletions src/roiextractors/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Utility functions for the ROIExtractors package."""

import glob
import os
from typing import Dict, Any

from parse import parse


def match_paths(base: str, pattern: str, sort=True) -> Dict[str, Dict[str, Any]]:
"""
Match paths in a directory to a pattern.

Parameters
----------
base: str
The base directory to search in.
pattern: str
The f-string pattern to match the paths to.
sort: bool, default=True
Whether to sort the output by the values of the named groups in the pattern.

Returns
-------
dict
"""
full_pattern = os.path.join(base, pattern)
paths = glob.glob(os.path.join(base, "*"))
out = {}
for path in paths:
parsed = parse(full_pattern, path)
if parsed is not None:
out[path] = parsed.named

if sort:
out = dict(sorted(out.items(), key=lambda item: tuple(item[1].values())))

return out
56 changes: 56 additions & 0 deletions tests/test_internals/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os

from roiextractors.utils import match_paths
from tempfile import TemporaryDirectory


def test_match_paths():
# create temporary directory
with TemporaryDirectory() as tmpdir:
# create temporary files
files = [
"split_1.tif",
"split_2.tif",
"split_3.tif",
"split_4.tif",
"split_5.tif",
"split_6.tif",
"split_7.tif",
"split_8.tif",
"split_9.tif",
"split_10.tif",
]
for file in files:
with open(os.path.join(tmpdir, file), "w") as f:
f.write("")

# test match_paths
out = match_paths(tmpdir, "split_{split:d}.tif")
assert list(out.keys()) == [os.path.join(tmpdir, x) for x in files]
assert list([x["split"] for x in out.values()]) == list(range(1, 11))


def test_match_paths_sub_select():
# create temporary directory
with TemporaryDirectory() as tmpdir:
# create temporary files
files = [
"chanA_split_1.tif",
"chanA_split_2.tif",
"chanA_split_3.tif",
"chanA_split_4.tif",
"chanA_split_5.tif",
"chanB_split_1.tif",
"chanB_split_2.tif",
"chanB_split_3.tif",
"chanB_split_4.tif",
"chanB_split_5.tif",
]
for file in files:
with open(os.path.join(tmpdir, file), "w") as f:
f.write("")

# test match_paths
out = match_paths(tmpdir, "chanA_split_{split:d}.tif")
assert list(out.keys()) == [os.path.join(tmpdir, x) for x in files[:5]]
assert list([x["split"] for x in out.values()]) == list(range(1, 6))
64 changes: 64 additions & 0 deletions tests/test_multitiff_multipage_imaging_extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from roiextractors import MultiTiffImagingExtractor, FolderTiffImagingExtractor

from tests.setup_paths import OPHYS_DATA_PATH


def test_init_folder_tiff_imaging_extractor_multi_page():
extractor = FolderTiffImagingExtractor(
folder_path=OPHYS_DATA_PATH / "imaging_datasets" / "Tif" / "splits",
pattern="split_{split:d}.tif",
sampling_frequency=1.0,
)

assert extractor.get_num_channels() == 1
assert extractor.get_num_frames() == 2000
assert extractor.get_sampling_frequency() == 1.0
assert extractor.get_channel_names() is None
assert extractor.get_dtype() == "uint16"
assert extractor.get_image_size() == (60, 80)
assert extractor.get_video().shape == (2000, 60, 80)
assert list(extractor.file_paths) == [
str(OPHYS_DATA_PATH / "imaging_datasets" / "Tif" / "splits" / x)
for x in (
"split_1.tif",
"split_2.tif",
"split_3.tif",
Comment on lines +23 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some extra tests for this, at a minimum, should include cases of (a) multiple cameras/microscopes (the specifier for each device being something in the file name), and (b) multiple optic channels for a single one of those devices (the channel name/ID also being a part of the filename)

Not that I'm not saying the TiffFolderImagingExtractor should by default support multiple optic channels, or auto-detection of channel IDs from this pattern (that is a more complicated feature that could be done in a follow-up, or not at all); I just want to make sure that a single instance made from a pattern specification that restricts to only one channel from a folder that contains several correctly identifies list of files only for that channel

"split_4.tif",
"split_5.tif",
"split_6.tif",
"split_7.tif",
"split_8.tif",
"split_9.tif",
"split_10.tif",
)
]


def test_init_multitiff_imaging_extractor_multi_page():
extractor = MultiTiffImagingExtractor(
file_paths=[OPHYS_DATA_PATH / "imaging_datasets" / "Tif" / "splits" / f"split_{i}.tif" for i in range(1, 11)],
sampling_frequency=1.0,
)

assert extractor.get_num_channels() == 1
assert extractor.get_num_frames() == 2000
assert extractor.get_sampling_frequency() == 1.0
assert extractor.get_channel_names() is None
assert extractor.get_dtype() == "uint16"
assert extractor.get_image_size() == (60, 80)
assert extractor.get_video().shape == (2000, 60, 80)
assert list(extractor.file_paths) == [
OPHYS_DATA_PATH / "imaging_datasets" / "Tif" / "splits" / x
for x in (
"split_1.tif",
"split_2.tif",
"split_3.tif",
"split_4.tif",
"split_5.tif",
"split_6.tif",
"split_7.tif",
"split_8.tif",
"split_9.tif",
"split_10.tif",
)
]
Loading