Skip to content

Commit

Permalink
Merge pull request #5 from BMCV/dev/fix-4
Browse files Browse the repository at this point in the history
`giatools.io.imread` will automatically use `tifffile` for loading TIFF images, when available
  • Loading branch information
kostrykin authored Sep 24, 2024
2 parents e24af11 + 360476f commit f106074
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 27 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[run]

omit = tests/*
4 changes: 3 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[flake8]

extend-ignore = E221,E211,E222,E202,F541,E201,E203,E501
max-line-length = 120

extend-ignore = E221,E211,E222,E202,F541,E201,E203
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ __pycache__
/build
*.swp
tests/*-out-*
/.venv
/.venv
/.coverage
9 changes: 9 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[settings]

multi_line_output = 3

force_grid_wrap = 2

include_trailing_comma = true

skip_glob = docs/source/conf.py
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"[git-commit]": {
"editor.rulers": [50]
},

"[python]": {
"editor.rulers": [119]
}
}
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
Galaxy Image Analysis: https://github.com/BMCV/galaxy-image-analysis

Use ``python -m unittest`` in the root directory of the repository to run the test suite.

Use ``coverage run -m unittest && coverage html`` to generate a coverage report.
4 changes: 2 additions & 2 deletions giatools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
VERSION_MAJOR = 0
VERSION_MINOR = 1
VERSION_PATCH = 2
VERSION_MINOR = 2
VERSION_PATCH = 0

VERSION = '%d.%d%s' % (VERSION_MAJOR, VERSION_MINOR, '.%d' % VERSION_PATCH if VERSION_PATCH > 0 else '')
40 changes: 25 additions & 15 deletions giatools/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,36 @@
See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
"""

import contextlib

import skimage.io

import giatools.util

try:
import tifffile
except ImportError:
tifffile = None

def imread(*args, impl=skimage.io.imread, **kwargs):
"""
Wrapper around ``skimage.io.imread`` which mutes non-fatal errors.

When using ``skimage.io.imread`` to read an image file, sometimes errors can be reported although the image file will be read successfully.
In those cases, Galaxy might detect the errors and assume that the tool has failed: https://docs.galaxyproject.org/en/latest/dev/schema.html#error-detection
To prevent this, this wrapper around ``skimage.io.imread`` will mute all non-fatal errors.
@giatools.util.silent
def imread(*args, **kwargs):
"""
try:
Wrapper for loading images which mutes non-fatal errors.
# Mute stderr unless an error occurs
with contextlib.redirect_stderr(None):
return impl(*args, **kwargs)
When using ``skimage.io.imread`` to read an image file, sometimes errors can be reported although the image file
will be read successfully. In those cases, Galaxy might detect the errors on stdout or stderr, and assume that the
tool has failed: https://docs.galaxyproject.org/en/latest/dev/schema.html#error-detection To prevent this, this
wrapper around ``skimage.io.imread`` will mute all non-fatal errors.
Image loading is first attempted using `tifffile` (if available, more reliable for loading TIFF files), and if
that fails (e.g., because the file is not a TIFF file), falls back to ``skimage.io.imread``.
"""

except: # noqa: E722
# First, try to read the image using `tifffile` (will only succeed if it is a TIFF file)
if tifffile is not None:
try:
return tifffile.imread(*args, **kwargs)
except tifffile.TiffFileError:
pass # not a TIFF file

# Raise the error outside of the contextlib redirection
raise
# If the image is not a TIFF file, or `tifffile is not available`, fall back to `skimage.io.imread`
return skimage.io.imread(*args, **kwargs)
16 changes: 16 additions & 0 deletions giatools/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,26 @@
See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
"""

import contextlib
import os

import numpy as np
import skimage.util


def silent(func):
"""
Decorator that mutes the standard error stream of the decorated function.
"""

def wrapper(*args, **kwargs):
with open(os.devnull, 'w') as fnull:
with contextlib.redirect_stderr(fnull):
return func(*args, **kwargs)

return wrapper


def convert_image_to_format_of(image, format_image):
"""
Convert the first image to the format of the second image.
Expand Down
16 changes: 8 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@


setup(
name = 'giatools',
version = giatools.VERSION,
description = 'Tools required for Galaxy Image Analysis',
author = 'Leonid Kostrykin',
author_email = '[email protected]',
url = 'https://kostrykin.com',
license = 'MIT',
packages = ['giatools'],
name='giatools',
version=giatools.VERSION,
description='Tools required for Galaxy Image Analysis',
author='Leonid Kostrykin',
author_email='[email protected]',
url='https://kostrykin.com',
license='MIT',
packages=['giatools'],
)
Binary file added tests/data/input3.tif
Binary file not shown.
Binary file added tests/data/input4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions tests/test_io.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import unittest
import unittest.mock

import giatools.io

# This tests require that the `tifffile` package is installed.
assert giatools.io.tifffile is not None


class imread(unittest.TestCase):

Expand All @@ -12,3 +16,32 @@ def test_input1(self):
def test_input2(self):
img = giatools.io.imread('tests/data/input2.tif')
self.assertEqual(img.mean(), 9.543921821305842)

def test_input3(self):
"""
This is a multi-page TIFF, that sometimes fails to load properly with ``skimage.io.imread``, but works with
`tifffile`.
For details see: https://github.com/BMCV/galaxy-image-analysis/pull/132#issuecomment-2371561435
"""
img = giatools.io.imread('tests/data/input3.tif')
self.assertEqual(img.shape, (5, 198, 356))
self.assertEqual(img.mean(), 1259.6755334241288)

def test_input4(self):
"""
This is an RGB PNG file, that cannot be loaded with `tifffile`, but works with ``skimage.io.imread``.
"""
img = giatools.io.imread('tests/data/input4.png')
self.assertEqual(img.shape, (10, 10, 3))
self.assertEqual(img.mean(), 130.04)

@unittest.mock.patch('skimage.io.imread')
@unittest.mock.patch('giatools.io.tifffile', None)
def test_without_tifffile(self, mock_skimage_io_imread):
"""
Test that loading an image without `tifffile` installed falls back to ``skimage.io.imread``.
"""
img = giatools.io.imread('tests/data/input1.tif')
mock_skimage_io_imread.assert_called_once_with('tests/data/input1.tif')
self.assertIs(img, mock_skimage_io_imread.return_value)
15 changes: 15 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import itertools
import sys
import unittest

import numpy as np
import skimage.util

import giatools.util
import tests.tools


class silent(unittest.TestCase):

def test_silent(self):
@giatools.util.silent
def func():
print('Test', file=sys.stderr)
raise ValueError('This is a test error message')
with tests.tools.CaptureStderr() as stderr:
with self.assertRaises(ValueError):
func()
self.assertEqual(str(stderr), '')


class convert_image_to_format_of(unittest.TestCase):
Expand Down
19 changes: 19 additions & 0 deletions tests/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import contextlib
import io


class CaptureStderr:

def __init__(self):
self.stdout_buf = io.StringIO()

def __enter__(self):
self.redirect = contextlib.redirect_stderr(self.stdout_buf)
self.redirect.__enter__()
return self

def __exit__(self, exc_type, exc_value, traceback):
self.redirect.__exit__(exc_type, exc_value, traceback)

def __str__(self):
return self.stdout_buf.getvalue()

0 comments on commit f106074

Please sign in to comment.