Skip to content

Commit

Permalink
Add obsep support and minor fixes (#230)
Browse files Browse the repository at this point in the history
  • Loading branch information
Czaki committed Mar 23, 2021
1 parent ef913e8 commit f273356
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 43 deletions.
2 changes: 1 addition & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ stages:
displayName: TestBuild

- job: pyinstaller
condition: not(startsWith(variables['Build.SourceBranch'], 'refs/heads/feature'))
# condition: not(startsWith(variables['Build.SourceBranch'], 'refs/heads/feature'))
strategy:
matrix:
macos:
Expand Down
7 changes: 3 additions & 4 deletions build_utils/minimal-req.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ defusedxml==0.6.0
h5py==2.8.0
imagecodecs==2020.5.30
imageio==2.5.0
napari==0.4.2
numpy==1.16.0
napari==0.4.3
numpy==1.16.5
oiffile==2019.1.1
openpyxl==2.4.9
packaging==17.1
Expand All @@ -24,6 +24,5 @@ sympy==1.1.1
tifffile==2020.2.16
xlrd==1.1.0
xlsxwriter
dataclasses==0.7 ;python_version < '3.7'
typing-extensions==3.7.4 ;python_version < '3.8'
typing-extensions==3.7.4.2 ;python_version < '3.8'
PyQt5!=5.15.0,==5.12.3
40 changes: 20 additions & 20 deletions launcher.spec
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,25 @@ napari.plugins.plugin_manager.discover()

hiddenimports = ["imagecodecs._" + x for x in imagecodecs._extensions()] +\
["imagecodecs._shared"] + [x.__name__ for x in napari.plugins.plugin_manager.plugins.values()] + \
["pkg_resources.py2_warn", "scipy.special.cython_special", "ipykernel.datapub"]
["pkg_resources.py2_warn", "scipy.special.cython_special", "ipykernel.datapub"] + [
"numpy.core._dtype_ctypes",
"sentry_sdk.integrations.logging",
"sentry_sdk.integrations.stdlib",
"sentry_sdk.integrations.excepthook",
"sentry_sdk.integrations.dedupe",
"sentry_sdk.integrations.atexit",
"sentry_sdk.integrations.modules",
"sentry_sdk.integrations.argv",
"sentry_sdk.integrations.threading",
"numpy.random.common",
"numpy.random.bounded_integers",
"numpy.random.entropy",
"PartSegCore.register",
"defusedxml.cElementTree",
"vispy.app.backends._pyqt5",
"scipy.spatial.transform._rotation_groups",
"magicgui.backends._qtpy",
]

try:
from sentry_sdk.integrations import _AUTO_ENABLING_INTEGRATIONS
Expand Down Expand Up @@ -106,25 +124,7 @@ a = Analysis(
+ collect_data_files("vispy")
+ collect_data_files("napari")
+ pyzmq_data,
hiddenimports=hiddenimports
+ [
"numpy.core._dtype_ctypes",
"sentry_sdk.integrations.logging",
"sentry_sdk.integrations.stdlib",
"sentry_sdk.integrations.excepthook",
"sentry_sdk.integrations.dedupe",
"sentry_sdk.integrations.atexit",
"sentry_sdk.integrations.modules",
"sentry_sdk.integrations.argv",
"sentry_sdk.integrations.threading",
"numpy.random.common",
"numpy.random.bounded_integers",
"numpy.random.entropy",
"PartSegCore.register",
"defusedxml.cElementTree",
"vispy.app.backends._pyqt5",
"scipy.spatial.transform._rotation_groups"
],
hiddenimports=hiddenimports,
# + ["plugins." + x.name for x in plugins.get_plugins()],
hookspath=[],
runtime_hooks=[],
Expand Down
5 changes: 4 additions & 1 deletion package/PartSeg/common_gui/algorithms_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ def recursive_get_values(self):

def set_value(self, val):
"""set value of widget """
return self._setter(self._widget, val)
try:
return self._setter(self._widget, val)
except TypeError:
pass

def get_field(self) -> QWidget:
"""
Expand Down
3 changes: 2 additions & 1 deletion package/PartSeg/common_gui/main_window.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import os
from pathlib import Path
from typing import List, Optional, Type
Expand Down Expand Up @@ -72,7 +73,7 @@ def set_data(self, data):
if image:
if isinstance(image, Image):
# noinspection PyProtectedMember
data = data._replace(image=image)
data = dataclasses.replace(data, image=image)
else:
return
if data is None:
Expand Down
1 change: 1 addition & 0 deletions package/PartSeg/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def get_plugins():
napari.plugins.plugin_manager.register(load_roi_project)
napari.plugins.plugin_manager.register(napari_plugin_engine)
napari.plugins.plugin_manager.register(save_mask_roi)

else:
packages = pkgutil.iter_modules(__path__, __name__ + ".")
packages2 = itertools.chain(
Expand Down
2 changes: 1 addition & 1 deletion package/PartSegCore/mask/io_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ class LoadStackImage(LoadBase):

@classmethod
def get_name(cls):
return "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif)"
return "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"

@classmethod
def get_short_name(cls):
Expand Down
3 changes: 2 additions & 1 deletion package/PartSegCore/napari_plugins/save_mask_roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from napari_plugin_engine import napari_hook_implementation

from PartSegCore.mask.io_functions import MaskProjectTuple, SaveROI
from PartSegImage.image import NAPARI_SCALING


@napari_hook_implementation
Expand All @@ -14,5 +15,5 @@ def napari_write_labels(path: str, data: Any, meta: dict) -> Optional[str]:
ext = os.path.splitext(path)[1]
if ext in SaveROI.get_extensions():
project = MaskProjectTuple(file_path="", image=None, roi=data)
SaveROI.save(path, project, parameters={})
SaveROI.save(path, project, parameters={"spacing": numpy.divide(meta["scale"], NAPARI_SCALING)[-3:]})
return path
10 changes: 9 additions & 1 deletion package/PartSegImage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@

from . import tifffile_fixes # noqa: F401
from .image import Image
from .image_reader import CziImageReader, GenericImageReader, OifImagReader, TiffFileException, TiffImageReader
from .image_reader import (
CziImageReader,
GenericImageReader,
ObsepImageReader,
OifImagReader,
TiffFileException,
TiffImageReader,
)
from .image_writer import ImageWriter

__all__ = (
Expand All @@ -13,6 +20,7 @@
"TiffFileException",
"CziImageReader",
"OifImagReader",
"ObsepImageReader",
"GenericImageReader",
)

Expand Down
16 changes: 12 additions & 4 deletions package/PartSegImage/image.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import typing
import warnings
from collections.abc import Iterable
Expand All @@ -10,6 +11,8 @@
_DEF = object()
FRAME_THICKNESS = 2

NAPARI_SCALING = 10 ** 9


def minimal_dtype(val: int):
"""
Expand Down Expand Up @@ -158,13 +161,18 @@ def merge(self, image: "Image", axis: typing.Union[str, int]) -> "Image":
data = self.reorder_axes(image.get_data(), image.axis_order)
data = np.concatenate((self.get_data(), data), axis=axis)
channel_names = self.channel_names
reg = re.compile(r"channel \d+")
for name in image.channel_names:
match = reg.match(name)
new_name = name
if match:
name = "channel"
new_name = f"channel {len(channel_names) + 1}"
i = 1
while new_name in channel_names:
new_name = f"{new_name} ({i})"
new_name = f"{name} ({i})"
i += 1
if i > 10000:
if i > 10000: # pragma: no cover
raise ValueError("fail when try to fix channel name")
channel_names.append(new_name)

Expand Down Expand Up @@ -482,7 +490,7 @@ def spacing(self) -> Spacing:
return tuple(self._image_spacing[1:])
return self._image_spacing

def normalized_scaling(self, factor=10 ** 9) -> Spacing:
def normalized_scaling(self, factor=NAPARI_SCALING) -> Spacing:
if self.is_2d:
return (1, 1) + tuple(np.multiply(self.spacing, factor))
return (1,) + tuple(np.multiply(self.spacing, factor))
Expand All @@ -498,7 +506,7 @@ def set_spacing(self, value: Spacing):
return
if self.is_2d and len(value) + 1 == len(self._image_spacing):
value = (1.0,) + tuple(value)
if len(value) != len(self._image_spacing):
if len(value) != len(self._image_spacing): # pragma: no cover
raise ValueError("Correction of spacing fail.")
self._image_spacing = tuple(value)

Expand Down
59 changes: 54 additions & 5 deletions package/PartSegImage/image_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import numpy as np
import tifffile.tifffile
from czifile.czifile import CziFile
from defusedxml import ElementTree
from oiffile import OifFile
from tifffile import TiffFile

Expand Down Expand Up @@ -149,7 +150,7 @@ class GenericImageReader(BaseImageReader):

def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext=None) -> Image:
if ext is None:
if isinstance(image_path, str):
if isinstance(image_path, (str, Path)):
ext = os.path.splitext(image_path)[1]
else:
ext = ".tif"
Expand All @@ -158,6 +159,8 @@ def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext
return CziImageReader.read_image(image_path, mask_path, self.callback_function, self.default_spacing)
if ext in [".oif", ".oib"]:
return OifImagReader.read_image(image_path, mask_path, self.callback_function, self.default_spacing)
if ext == ".obsep":
return ObsepImageReader.read_image(image_path, mask_path, self.callback_function, self.default_spacing)
return TiffImageReader.read_image(image_path, mask_path, self.callback_function, self.default_spacing)


Expand Down Expand Up @@ -238,6 +241,52 @@ def update_array_shape(cls, array: np.ndarray, axes: str):
return super().update_array_shape(array, axes)


class ObsepImageReader(BaseImageReader):
def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext=None) -> Image:
directory = Path(os.path.dirname(image_path))
xml_doc = ElementTree.parse(image_path).getroot()
channels = xml_doc.findall("net/node/node/attribute[@name='image type']")
if not channels:
raise ValueError("Information about channel images not found")
possible_extensions = [".tiff", ".tif", ".TIFF", ".TIF"]
channel_list = []
for channel in channels:
try:
name = next(iter(channel)).attrib["val"]
except StopIteration: # pragma: no cover
raise ValueError("Missed information about channel name in obsep file")
for ex in possible_extensions:
if (directory / (name + ex)).exists():
name += ex
break
else: # pragma: no cover
raise ValueError(f"Not found file for key {name}")
channel_list.append(TiffImageReader.read_image(directory / name, default_spacing=self.default_spacing))
for channel in channels:
try:
name = next(iter(channel)).attrib["val"] + "_deconv"
except StopIteration: # pragma: no cover
raise ValueError("Missed information about channel name in obsep file")
for ex in possible_extensions:
if (directory / (name + ex)).exists():
name += ex
break
if (directory / name).exists():
channel_list.append(TiffImageReader.read_image(directory / name, default_spacing=self.default_spacing))

image = channel_list[0]
for el in channel_list[1:]:
image = image.merge(el, "C")

z_spacing = (
float(xml_doc.find("net/node/attribute[@name='step width']/double").attrib["val"]) * name_to_scalar["um"]
)

image.set_spacing((z_spacing,) + image.spacing[1:])
image.file_path = str(image_path)
return image


class TiffImageReader(BaseImageReader):
"""
TIFF/LSM files reader. Base reading with :py:meth:`BaseImageReader.read_image`
Expand Down Expand Up @@ -305,7 +354,7 @@ def report_func():
self.image_file.close()
if self.mask_file is not None:
self.mask_file.close()
if not isinstance(image_path, str):
if not isinstance(image_path, (str, Path)):
image_path = ""
return self.image_class(
image_data,
Expand All @@ -332,10 +381,10 @@ def verify_mask(self):
continue
try:
j = image_series.axes.index(pos)
except ValueError:
except ValueError: # pragma: no cover
raise ValueError("Incompatible shape of mask and image (axes)")
# TODO add verification if problem with T/Z/I
if image_series.shape[j] != mask_series.shape[i]:
if image_series.shape[j] != mask_series.shape[i]: # pragma: no cover
raise ValueError("Incompatible shape of mask and image")
# TODO Add verification if mask have to few dimensions

Expand All @@ -361,7 +410,7 @@ def read_resolution_from_tags(self):
scalar = name_to_scalar["centimeter"]
elif unit == 2:
scalar = name_to_scalar["cal"]
else:
else: # pragma: no cover
raise KeyError(f"wrong scalar {tags['ResolutionUnit']}, {tags['ResolutionUnit'].value}")

x_spacing = tags["XResolution"].value[1] / tags["XResolution"].value[0] * scalar
Expand Down
2 changes: 1 addition & 1 deletion package/tests/test_PartSeg/test_main_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_open_mask(self, qtbot, monkeypatch, tmp_path):
main_window = LauncherMainWindow("Launcher")
qtbot.addWidget(main_window)
main_window._launch_mask()
with qtbot.waitSignal(main_window.prepare.finished):
with qtbot.waitSignal(main_window.prepare.finished, timeout=10 ** 4):
main_window.prepare.start()
# qtbot.addWidget(main_window.wind)
QCoreApplication.processEvents()
Expand Down
24 changes: 23 additions & 1 deletion package/tests/test_PartSegImage/test_image_reader.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import math
import os.path
import shutil
from glob import glob

import numpy as np
import pytest
import tifffile

import PartSegData
from PartSegImage import CziImageReader, GenericImageReader, Image, OifImagReader, TiffImageReader
from PartSegImage import CziImageReader, GenericImageReader, Image, ObsepImageReader, OifImagReader, TiffImageReader


class TestImageClass:
Expand Down Expand Up @@ -84,6 +86,26 @@ def test_set_spacing(self):
reader.set_default_spacing((5, 7))
assert reader.default_spacing == (10 ** -6, 5, 7)

def test_obsep_read(self, data_test_dir):
image = ObsepImageReader.read_image(os.path.join(data_test_dir, "obsep", "test.obsep"))
assert image.channels == 2
assert np.allclose(image.spacing, (500 * 10 ** -9, 64 * 10 ** -9, 64 * 10 ** -9))
assert image.channel_names == ["channel 1", "channel 2"]

def test_obsep_deconv_read(self, data_test_dir, tmp_path):
for el in glob(os.path.join(data_test_dir, "obsep", "*")):
shutil.copy(os.path.join(data_test_dir, "obsep", el), tmp_path)
image = GenericImageReader.read_image(tmp_path / "test.obsep")
assert image.channels == 2
assert np.allclose(image.spacing, (500 * 10 ** -9, 64 * 10 ** -9, 64 * 10 ** -9))
assert image.channel_names == ["channel 1", "channel 2"]
shutil.copy(tmp_path / "Cy5.TIF", tmp_path / "Cy5_decon2.TIF")
image = GenericImageReader.read_image(tmp_path / "test.obsep")
assert image.channels == 2
shutil.copy(tmp_path / "Cy5.TIF", tmp_path / "Cy5_deconv.TIF")
image = GenericImageReader.read_image(tmp_path / "test.obsep")
assert image.channels == 3


class CustomImage(Image):
axis_order = "TCXYZ"
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ install_requires =
h5py>=2.8.0
imagecodecs>=2020.5.30
imageio>=2.5.0
napari>=0.4.2
numpy>=1.16.0
napari>=0.4.3
numpy>=1.16.5
oiffile>=2019.1.1
openpyxl>=2.4.9
packaging>=17.1
Expand Down

0 comments on commit f273356

Please sign in to comment.