Skip to content

WIP: Derive baseclass from imageio #273

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
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ sudo: false
matrix:
include:
- python: "2.7"
env: DEPS="numpy=1.8* slicerator tifffile" BUILD_DOCS=false
env: DEPS="numpy=1.8* slicerator pillow tifffile jinja2" DEPSPIP="imageio" BUILD_DOCS=false
- python: "2.7"
env: DEPS="numpy slicerator scipy pillow matplotlib scikit-image jinja2 av libtiff tifffile jpype1" DEPSPIP="moviepy imageio" BUILD_DOCS=false
- python: "3.6"
env: DEPS="numpy slicerator tifffile" BUILD_DOCS=false
env: DEPS="numpy slicerator pillow tifffile jinja2" DEPSPIP="imageio" BUILD_DOCS=false
- python: "3.6"
env: DEPS="numpy slicerator scipy pillow matplotlib scikit-image jinja2 av tifffile libtiff jpype1 ipython sphinx sphinx_rtd_theme numpydoc" DEPSPIP="moviepy imageio" BUILD_DOCS=true

Expand Down
118 changes: 72 additions & 46 deletions pims/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@
from pims.display import (export, play, scrollable_stack, to_rgb, normalize,
plot_to_frame, plots_to_frame)
from itertools import chain
from functools import wraps

import six
import glob
import os
from warnings import warn

# has to be here for API stuff
from pims.base_frames import WrapImageIOReader
from pims.image_sequence import ImageSequence, ImageSequenceND, ReaderSequence # noqa
from pims.image_reader import ImageReader, ImageReaderND # noqa
from .cine import Cine # noqa
from .norpix_reader import NorpixSeq # noqa
from pims.tiff_stack import TiffStack_tifffile # noqa
from .spe_stack import SpeStack
from imageio import formats, get_reader


def not_available(requirement):
Expand All @@ -28,6 +30,17 @@ def raiser(*args, **kwargs):
"This reader requires {0}.".format(requirement))
return raiser


def register_fmt(reader, name, description, extensions=None, modes=None):
"""Registers a Format with the format manager. Returns a reader
for backwards compatibility."""
formats.add_format(reader(name, description, extensions, modes))
@wraps(reader)
def wrapper(filename, **kwargs):
return WrapImageIOReader(get_reader(filename, name, **kwargs))
return wrapper


if export is None:
export = not_available("PyAV or MoviePy")

Expand Down Expand Up @@ -75,12 +88,17 @@ def raiser(*args, **kwargs):
Video = not_available("PyAV, MoviePy, or ImageIO")

import pims.tiff_stack
from pims.tiff_stack import (TiffStack_pil, TiffStack_libtiff,
TiffStack_tifffile)
from pims.tiff_stack import TiffStack_pil, TiffStack_libtiff, \
FormatTiffStack_tifffile
# First, check if each individual class is available
# and drop in placeholders as needed.
if not pims.tiff_stack.tifffile_available():
TiffStack_tiffile = not_available("tifffile")
TiffStack_tifffile = not_available("tifffile")
else:
TiffStack_tifffile = register_fmt(FormatTiffStack_tifffile,
'TIFF_tifffile',
'Reads TIFF files through tifffile.py.',
'tif tiff lsm stk', 'iIvV')
if not pims.tiff_stack.libtiff_available():
TiffStack_libtiff = not_available("libtiff")
if not pims.tiff_stack.PIL_available():
Expand All @@ -97,17 +115,20 @@ def raiser(*args, **kwargs):
TiffStack = not_available("tifffile, libtiff, or PIL/Pillow")



try:
import pims.bioformats
if pims.bioformats.available():
Bioformats = pims.bioformats.BioformatsReader
Bioformats = register_fmt(pims.bioformats.BioformatsFormat,
'Bioformats',
'Reads multidimensional images from a file supported by bioformats.',
'lsm ipl dm3 seq nd2 ics ids ipw tif tiff'
' jpg bmp lif lei', 'iIvV')
else:
raise ImportError()
except (ImportError, IOError):
BioformatsRaw = not_available("JPype")
Bioformats = not_available("JPype")


try:
from pims_nd2 import ND2_Reader as ND2Reader_SDK

Expand Down Expand Up @@ -161,51 +182,56 @@ def open(sequence, **kwargs):
>>> frame_count = len(video) # Number of frames in video
>>> frame_shape = video.frame_shape # Pixel dimensions of video
"""
# check if it is an ImageSequence
files = glob.glob(sequence)
if len(files) > 1:
# todo: test if ImageSequence can read the image type,
# delegate to subclasses as needed
return ImageSequence(sequence, **kwargs)

_, ext = os.path.splitext(sequence)
if ext is None or len(ext) < 2:
raise UnknownFormatError(
"Could not detect your file type because it did not have an "
"extension. Try specifying a loader class, e.g. "
"Video({0})".format(sequence))
ext = ext.lower()[1:]

# list all readers derived from the pims baseclasses
all_handlers = chain(_recursive_subclasses(FramesSequence),
_recursive_subclasses(FramesSequenceND))
# keep handlers that support the file ext. use set to avoid duplicates.
eligible_handlers = set(h for h in all_handlers
if ext and ext in map(_drop_dot, h.class_exts()))
if len(eligible_handlers) < 1:
raise UnknownFormatError(
"Could not autodetect how to load a file of type {0}. "
"Try manually "
"specifying a loader class, e.g. Video({1})".format(ext, sequence))

def sort_on_priority(handlers):
# This uses optional priority information from subclasses
# > 10 means that it will be used instead of than built-in subclasses
def priority(cls):
try:
return cls.class_priority
except AttributeError:
return 10
return sorted(handlers, key=priority, reverse=True)

exceptions = ''
for handler in sort_on_priority(eligible_handlers):
try:
return handler(sequence, **kwargs)
except Exception as e:
message = '{0} errored: {1}'.format(str(handler), str(e))
warn(message)
exceptions += message + '\n'
raise UnknownFormatError("All handlers returned exceptions:\n" + exceptions)
# the rest of the Reader choosing logic is handled by imageio
return WrapImageIOReader(get_reader(sequence, **kwargs))
#
#
# _, ext = os.path.splitext(sequence)
# if ext is None or len(ext) < 2:
# raise UnknownFormatError(
# "Could not detect your file type because it did not have an "
# "extension. Try specifying a loader class, e.g. "
# "Video({0})".format(sequence))
# ext = ext.lower()[1:]
#
# # list all readers derived from the pims baseclasses
# all_handlers = chain(_recursive_subclasses(FramesSequence),
# _recursive_subclasses(FramesSequenceND))
# # keep handlers that support the file ext. use set to avoid duplicates.
# eligible_handlers = set(h for h in all_handlers
# if ext and ext in map(_drop_dot, h.class_exts()))
# if len(eligible_handlers) < 1:
# raise UnknownFormatError(
# "Could not autodetect how to load a file of type {0}. "
# "Try manually "
# "specifying a loader class, e.g. Video({1})".format(ext, sequence))
#
# def sort_on_priority(handlers):
# # This uses optional priority information from subclasses
# # > 10 means that it will be used instead of than built-in subclasses
# def priority(cls):
# try:
# return cls.class_priority
# except AttributeError:
# return 10
# return sorted(handlers, key=priority, reverse=True)
#
# exceptions = ''
# for handler in sort_on_priority(eligible_handlers):
# try:
# return handler(sequence, **kwargs)
# except Exception as e:
# message = '{0} errored: {1}'.format(str(handler), str(e))
# warn(message)
# exceptions += message + '\n'
# raise UnknownFormatError("All handlers returned exceptions:\n" + exceptions)


class UnknownFormatError(Exception):
Expand Down
120 changes: 120 additions & 0 deletions pims/base_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .frame import Frame
from abc import ABCMeta, abstractmethod, abstractproperty
from warnings import warn
from imageio.core import Format


class FramesStream(with_metaclass(ABCMeta, object)):
Expand Down Expand Up @@ -606,3 +607,122 @@ def __repr__(self):
s += "Axis '{0}' size: {1}\n".format(dim, self._sizes[dim])
s += """Pixel Datatype: {dtype}""".format(dtype=self.pixel_type)
return s


def guess_axes(image):
shape = image.shape
ndim = len(shape)

if ndim == 2:
return 'yx'
elif ndim == 3 and shape[2] in [3, 4]:
return 'yxc'
elif ndim == 3:
return 'zyx'
elif ndim == 4 and shape[3] in [3, 4]:
return 'zyxc'
else:
raise ValueError("Cannot interpret dimensions for an image of "
"shape {0}".format(shape))


def default_axes(sizes, mode):
if 'i' in mode: # single image
if sizes.get('z', 1) == 1 and sizes.get('t', 1) == 1:
bundle_axes = 'yx'
iter_axes = ''
else:
raise ValueError("This file cannot be opened as single image.")
elif 'I' in mode: # multiple images
if sizes.get('z', 1) == 1 and 't' in sizes:
bundle_axes = 'yx'
iter_axes = 't'
else:
raise ValueError("This file cannot be opened as multiple images.")
elif 'v' in mode: # single volume
if 'z' in sizes and sizes.get('t', 1) == 1:
bundle_axes = 'zyx'
iter_axes = ''
else:
raise ValueError("This file cannot be opened as single volume.")
elif 'V' in mode: # multiple volumes
if 'z' in sizes and 't' in sizes:
bundle_axes = 'zyx'
iter_axes = 't'
else:
raise ValueError("This file cannot be opened as multiple volumes.")
else:
if 'z' in sizes:
bundle_axes = 'zyx'
else:
bundle_axes = 'yx'
if 't' in sizes:
iter_axes = 't'
else:
iter_axes = ''

if 'c' in sizes:
bundle_axes += 'c'

return bundle_axes, iter_axes


def wrap_get_data(get_data_func, axis='t'):
# takes only kwargs (one index per named axis)
def get_frame(**kwargs):
index = kwargs[axis]
im, md = get_data_func(index)
return Frame(im, frame_no=index, metadata=md)
return get_frame


class WrapImageIOReader(FramesSequenceND):
def __init__(self, imageio_reader):
if not isinstance(imageio_reader, Format.Reader):
raise ValueError("Can only wrap ImageIO readers")

super(WrapImageIOReader, self).__init__()
self.rdr = imageio_reader
self.update_nd()

def update_nd(self):
self._clear_axes()
self._get_frame_dict = dict()

try:
info = self.rdr.pims_info
except AttributeError:
info = None

if info is None:
# guess everything from the first frame
tmp, _ = self.rdr._get_data(0)
self._dtype = tmp.dtype

axes = guess_axes(tmp)
for name, size in zip(axes, tmp.shape):
self._init_axis(name, size)
self._init_axis('t', self.rdr._get_length())
self._register_get_frame(wrap_get_data(self.rdr._get_data, 't'), axes)
else:
self._dtype = info['dtype']
for axis, size in info['sizes'].items():
self._init_axis(axis, size)

for name, axes in info['read_methods'].items():
method = getattr(self.rdr, name)
self._register_get_frame(method, axes)

self.bundle_axes, self.iter_axes = default_axes(self.sizes,
self.rdr.request.mode)

def __getattr__(self, key):
return getattr(self.rdr, key)

@property
def pixel_type(self):
return self._dtype

@property
def dtype(self):
return self._dtype
Loading